From b780c37a570ab8af1dc48a8f15ec9688759f602e Mon Sep 17 00:00:00 2001 From: Lei OT Date: Mon, 11 Aug 2025 16:36:53 +0800 Subject: [PATCH 01/15] =?UTF-8?q?feat(=E4=BA=A7=E5=93=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?):=20=E5=A4=8D=E5=88=B6=E6=8C=87=E5=AE=9A=E4=BA=A7=E5=93=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/locales/zh/products.json | 4 +- src/components/ProductsSelector.jsx | 56 ++++++++++ src/views/products/Detail/CopyProducts.jsx | 120 ++++++++++++++++----- 3 files changed, 151 insertions(+), 29 deletions(-) create mode 100644 src/components/ProductsSelector.jsx diff --git a/public/locales/zh/products.json b/public/locales/zh/products.json index 7bfe0e1..b611f82 100644 --- a/public/locales/zh/products.json +++ b/public/locales/zh/products.json @@ -1,5 +1,6 @@ { "ProductType": "项目类型", + "ProductName": "产品名称", "ContractRemarks": "合同备注", "versionHistory": "查看历史", "versionPublished": "已发布的", @@ -84,7 +85,8 @@ "withQuote": "是否复制报价", "requiredVendor": "请选择目标供应商", "requiredTypes": "请选择产品类型", - "requiredDept": "请选择所属小组" + "requiredDept": "请选择所属小组", + "copyTo": "复制到" }, "Validation": { "adultPrice": "请输入成人价", diff --git a/src/components/ProductsSelector.jsx b/src/components/ProductsSelector.jsx new file mode 100644 index 0000000..a3d79cf --- /dev/null +++ b/src/components/ProductsSelector.jsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; +import { Select, Spin } 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) => ({ lable: type_name, title: type_name, key: type_name, options: byTypes[type_name].map(row => ({...row, label: `${row.info.code} : ${row.info.title}`, value: row.info.id})) })); +}; + +const ProductsSelector = ({ params, ...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 () => {}; + }, []); + + return ( + <> + + + ); +}; +export default ProductsSelector; diff --git a/src/views/products/Detail/CopyProducts.jsx b/src/views/products/Detail/CopyProducts.jsx index 40fbca5..feccf17 100644 --- a/src/views/products/Detail/CopyProducts.jsx +++ b/src/views/products/Detail/CopyProducts.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { App, Form, Modal, DatePicker, Divider, Switch } from 'antd'; +import { App, Form, Modal, DatePicker, Divider, Switch, Space, Flex } from 'antd'; import { isEmpty, objectMapper } from '@/utils/commons'; import { useTranslation } from 'react-i18next'; @@ -13,43 +13,102 @@ import { copyAgencyDataAction } from '@/stores/Products/Index'; 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 [typeDisabled, setTypeDisabled] = useState(false); useEffect(() => { onFormInstanceReady(form); }, []); const onValuesChange = (changeValues, allValues) => {}; + return ( -
- {action === '#' && - - } - - - - {action === '#' && - - + + +
+ {action === '#' && ( + + + + + + )} + ({ + warningOnly: true, + validator: async () => { + if (!isEmpty(getFieldValue('products_list'))) { + // setTypeDisabled(true); + return Promise.reject(new Error(t(`⚠勾选了复制的产品名称之后 🔽, 将忽略选择的类型 🔼`))); + } + // setTypeDisabled(false); + return Promise.resolve(); + }, + }), + ]}> + + + + + + {t('products:CopyFormMsg.copyTo')}: + {action === '#' && ( + + + + )} + + + + + + + + + + {/* disabledDate={(current) => current <= dayjs([source.sourceYear, 12, 31])} */} + + + + + + +
+ + + {() => ( +
+ {!isEmpty(form.getFieldValue('products_list')) && 已选择的产品 预览:} + {(form.getFieldValue('products_list') || []).map((item, index) => ( +
+ {index + 1}. {item.label} +
+ ))} +
+ )}
-
} - - - - - - {/* disabledDate={(current) => current <= dayjs([source.sourceYear, 12, 31])} */} - - - - +
); }; @@ -76,9 +135,10 @@ 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, ...omittedValue } = values; + const { agency, year, products_list, ...omittedValue } = values; dest = { ...omittedValue, ...objectMapper(values, destinationObject) }; for (const key in dest) { if (Object.prototype.hasOwnProperty.call(dest, key)) { @@ -101,15 +161,18 @@ export const CopyProductsFormModal = ({ source, action = '#' | 'o', open, onSubm const handleCopyAgency = async (param) => { param.target_agency = isEmpty(param.target_agency) ? source.sourceAgency.travel_agency_id : param.target_agency; setCopyLoading(true); - // console.log(param); + // debug: + // console.log('ready params', param); + // setCopyLoading(false); + // throw new Error('暂不支持复制'); // 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')); @@ -122,7 +185,7 @@ export const CopyProductsFormModal = ({ source, action = '#' | 'o', open, onSubm }; return ( - { From e9fecc13bcffbc6de313e56fced12ec46daa6230 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Wed, 13 Aug 2025 10:33:11 +0800 Subject: [PATCH 02/15] =?UTF-8?q?perf(=E4=BA=A7=E5=93=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?):=20=E4=B8=8D=E5=88=86=E4=BA=BA=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useProductsSets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useProductsSets.js b/src/hooks/useProductsSets.js index d960e91..e57733d 100644 --- a/src/hooks/useProductsSets.js +++ b/src/hooks/useProductsSets.js @@ -207,5 +207,5 @@ export const PackageTypes = [ ]; export const formatGroupSize = (min, max) => { - return max === 1000 ? min === 0 ? '不分人等' : `${min}人以上` : `${min}-${max}`; + return max === 1000 ? min <= 1 ? '不分人等' : `${min}人以上` : `${min}-${max}`; }; From afd23641c336e7198824e6482cfefd9a4ef2049b Mon Sep 17 00:00:00 2001 From: LiaoYijun Date: Wed, 13 Aug 2025 11:32:03 +0800 Subject: [PATCH 03/15] fix: antd.Tooltip Warning --- src/views/products/Detail/ProductInfoQuotation.jsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/views/products/Detail/ProductInfoQuotation.jsx b/src/views/products/Detail/ProductInfoQuotation.jsx index 853e459..54fbe5d 100644 --- a/src/views/products/Detail/ProductInfoQuotation.jsx +++ b/src/views/products/Detail/ProductInfoQuotation.jsx @@ -258,11 +258,10 @@ const ProductInfoQuotation = ({ editable, ...props }) => { {t("products:unit_name")}{" "} - {" "} + ), dataIndex: "unit_id", @@ -283,11 +282,11 @@ const ProductInfoQuotation = ({ editable, ...props }) => { {t("products:use_dates")}{" "} - {" "} + ), dataIndex: "use_dates", From 182cec31fc290c612c9c654352d0727de8e246fb Mon Sep 17 00:00:00 2001 From: LiaoYijun Date: Wed, 13 Aug 2025 16:24:13 +0800 Subject: [PATCH 04/15] =?UTF-8?q?perf:=20=E9=87=87=E8=B4=AD=E5=B9=B4?= =?UTF-8?q?=E4=BB=BD=E6=9C=80=E6=97=A9=202022?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/products/PickYear.jsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/views/products/PickYear.jsx b/src/views/products/PickYear.jsx index 28c1eba..399ec16 100644 --- a/src/views/products/PickYear.jsx +++ b/src/views/products/PickYear.jsx @@ -21,15 +21,10 @@ function PickYear() { variant="underlined" needConfirm inputReadOnly={true} - minDate={dayjs().add(-30, "year")} + minDate={dayjs('2022-01-01')} maxDate={dayjs().add(2, "year")} allowClear={false} picker="year" - styles={{ - root: { - color: 'red' - } - }} open={true} onOk={(date) => { const useYear = date.year(); From be49d9cd02f38e43a336f29b4c89afbce7b04837 Mon Sep 17 00:00:00 2001 From: LiaoYijun Date: Fri, 15 Aug 2025 10:47:00 +0800 Subject: [PATCH 05/15] =?UTF-8?q?perf:=20=E5=88=A0=E9=99=A4=E6=8A=A5?= =?UTF-8?q?=E4=BB=B7=E4=BD=BF=E7=94=A8=20Popconfirm=EF=BC=9B=E6=96=B0?= =?UTF-8?q?=E6=8A=A5=E4=BB=B7=E9=BB=98=E8=AE=A4=E4=B8=8D=E5=88=86=E4=BA=BA?= =?UTF-8?q?=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../products/Detail/ProductInfoQuotation.jsx | 63 ++++++++----------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/src/views/products/Detail/ProductInfoQuotation.jsx b/src/views/products/Detail/ProductInfoQuotation.jsx index 54fbe5d..47cd2e0 100644 --- a/src/views/products/Detail/ProductInfoQuotation.jsx +++ b/src/views/products/Detail/ProductInfoQuotation.jsx @@ -13,6 +13,7 @@ import { DatePicker, Space, App, + Popconfirm, Tooltip, } from "antd"; import { useTranslation } from "react-i18next"; @@ -20,7 +21,6 @@ import { CloseOutlined, StarTwoTone, PlusOutlined, - ExclamationCircleFilled, QuestionCircleOutlined, } from "@ant-design/icons"; import { useDatePresets } from "@/hooks/useDatePresets"; @@ -179,7 +179,7 @@ const ProductInfoQuotation = ({ editable, ...props }) => { const [isBatchSetupModalOpen, setBatchSetupModalOpen] = useState(false); const [groupAllSize, setGroupAllSize] = useState(false); const [groupMaxUnlimit, setGroupMaxUnlimit] = useState(false); - const { modal, notification } = App.useApp(); + const { notification } = App.useApp(); const [quotationForm] = Form.useForm(); const [batchSetupForm] = Form.useForm(); @@ -206,6 +206,7 @@ const ProductInfoQuotation = ({ editable, ...props }) => { }; const onNewQuotation = () => { + setGroupAllSize(false); // 新报价不分人等 const emptyQuotation = newEmptyQuotation(defaultUseDates); quotationForm.setFieldsValue(emptyQuotation); setQuotationModalOpen(true); @@ -224,24 +225,6 @@ const ProductInfoQuotation = ({ editable, ...props }) => { setBatchSetupModalOpen(false); }; - const onDeleteQuotation = (quotation) => { - modal.confirm({ - title: "请确认", - icon: , - 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 @@ -256,10 +239,7 @@ const ProductInfoQuotation = ({ editable, ...props }) => { title: ( <> {t("products:unit_name")}{" "} - + @@ -282,7 +262,7 @@ const ProductInfoQuotation = ({ editable, ...props }) => { {t("products:use_dates")}{" "} @@ -290,7 +270,6 @@ const ProductInfoQuotation = ({ editable, ...props }) => { ), dataIndex: "use_dates", - // width: '6rem', render: (_, record) => `${record.use_dates_start}-${record.use_dates_end}`, }, @@ -301,25 +280,37 @@ const ProductInfoQuotation = ({ editable, ...props }) => { dataIndex: "operation", width: "10rem", render: (_, quotation) => { - // const _rowEditable = [-1,3].includes(quotation.audit_state_id); - const _rowEditable = true; // test: 0 return ( - + + ); }, From 640fa4b593b1f80ab15b7b8edf09a388010e4ba6 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 15 Aug 2025 11:37:49 +0800 Subject: [PATCH 06/15] =?UTF-8?q?feat:=20=E4=BA=A7=E5=93=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86:=20=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/locales/en/common.json | 2 +- public/locales/en/products.json | 1 + public/locales/zh/common.json | 2 +- public/locales/zh/products.json | 1 + src/hooks/useProductsFormat.js | 139 ++++ src/hooks/useProductsQuotationFormat.js | 333 ++++++++ src/hooks/useProductsSets.js | 4 +- src/views/products/Audit.jsx | 13 +- src/views/products/Detail/Header.jsx | 4 + .../Detail/ProductQuotationLogPopover.jsx | 19 +- .../ProductQuotationSnapshotPopover.jsx | 299 +++++++ src/views/products/Print/AgencyPreview.jsx | 756 ++++++++++++++++++ 12 files changed, 1563 insertions(+), 10 deletions(-) create mode 100644 src/hooks/useProductsFormat.js create mode 100644 src/hooks/useProductsQuotationFormat.js create mode 100644 src/views/products/Detail/ProductQuotationSnapshotPopover.jsx create mode 100644 src/views/products/Print/AgencyPreview.jsx diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 4e8321a..7ef228f 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -19,7 +19,7 @@ "Back": "Back", "Download": "Download", "Upload": "Upload", - "preview": "Preview", + "Preview": "Preview", "Total": "Total", "Action": "Action", "Import": "Import", diff --git a/public/locales/en/products.json b/public/locales/en/products.json index 20507cb..c2f8bbf 100644 --- a/public/locales/en/products.json +++ b/public/locales/en/products.json @@ -4,6 +4,7 @@ "ContractRemarks": "合同备注", "versionHistory": "Version History", "versionPublished": "Published", + "versionSnapshot": "Snapshot", "type": { "Experience": "Experience", "Car": "Transport Services", diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 0a57df9..8c15a37 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -19,7 +19,7 @@ "Back": "返回", "Download": "下载", "Upload": "上传", - "preview": "预览", + "Preview": "预览", "Total": "总数", "Action": "操作", "Import": "导入", diff --git a/public/locales/zh/products.json b/public/locales/zh/products.json index b611f82..02b67ba 100644 --- a/public/locales/zh/products.json +++ b/public/locales/zh/products.json @@ -4,6 +4,7 @@ "ContractRemarks": "合同备注", "versionHistory": "查看历史", "versionPublished": "已发布的", + "versionSnapshot": "快照", "type": { "Experience": "综费", "Car": "车费", diff --git a/src/hooks/useProductsFormat.js b/src/hooks/useProductsFormat.js new file mode 100644 index 0000000..91dfe7e --- /dev/null +++ b/src/hooks/useProductsFormat.js @@ -0,0 +1,139 @@ +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, + }; +}; diff --git a/src/hooks/useProductsQuotationFormat.js b/src/hooks/useProductsQuotationFormat.js new file mode 100644 index 0000000..7f395e0 --- /dev/null +++ b/src/hooks/useProductsQuotationFormat.js @@ -0,0 +1,333 @@ +import { flush, groupBy, isEmpty, isNotEmpty, pick, unique, uniqWith } from '@/utils/commons'; +import dayjs from 'dayjs'; +import { formatGroupSize } from './useProductsSets'; +// 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(startDate, 'year') >= 1; + const isLongerThan12M = end.diff(startDate, 'month') >= 11; + + return isFullYear || isLongerThanYear || isLongerThan12M; +}; + +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 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; + }, {}); + + 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: + + const _quotation = rowp.quotation.map((quoteItem) => { + quoteItem.quote_size = pkey; + quoteItem.quote_col_key = formatGroupSize(quoteItem.group_size_min, quoteItem.group_size_max); + 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 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 }); + accDate.push(_colFromDateRow); + return accDate; + }, []); + return { ...accBy, [sizeKeys]: _rowsFromDate }; + }, {}); + return { ...accBy, [byKey]: transposeTables }; + }, {}); + // console.log(_quotationTransposeBySize); + + return { + ...rowp, + _quotationTransposeBySize, + 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}`)); + + // const transposeDataSS = chunkSS + + return { + chunk: chunkSS, + dataSource: chunkSS, + SSRange, + PSRange, + ...compactSizeSets, // { SSsizeSetKey, sizeSets } + }; +}; + +/** + * 按人等拆分表格 + * @use D J B R 8 + */ +const splitTable_SizeSets = (chunkData) => { + const { SSRange, PSRange, SSsizeSetKey, SSsizeSetsMap, chunk } = chunkData; + const bySizeSetKey = groupBy(chunk, 'sizeSetsSS'); // next: city + // agencyProducts.J. + // console.log('bySizeSetKey', bySizeSetKey); + const tables = Object.keys(bySizeSetKey).map((sizeSetsStr) => { + const _thisSSsetProducts = bySizeSetKey[sizeSetsStr]; + const _subTable = _thisSSsetProducts.map(({ info, sizeSetsSS, _quotationTransposeBySize, ...pitem }) => { + const transpose = _quotationTransposeBySize['#'][sizeSetsSS]; + const _pRow = transpose.map((quote, qi) => ({ ...quote, rowSpan: qi === 0 ? transpose.length : 0 })); + return { info, sizeSetsSS, rows: _pRow, transpose }; + }); + return { cols: SSsizeSetsMap[sizeSetsStr], colsKey: sizeSetsStr, data: _subTable }; + }); + // console.log('---- tables', tables); + const tablesQuote = tables.map(({ cols, colsKey, 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, data: _table }; + }); + // console.log('---- tablesQuote', tablesQuote); + return tablesQuote; +}; + +/** + * 按季度分列 [平季, 旺季] + * @use Q 7 6 + */ +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'].map((k) => ele[k]).join('@')); + // console.log('---- bySeasonValue', bySeasonValue); + + 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; + accv.push(valRow); + return accv; + }, []); + + return { ...accp, [_s]: valUnderSeason }; + }, {}); + // console.log('---- rowSeason', rowSeason); + return { info: pitem.info, ...rowSeason }; + }); + // console.log('---- tablesQuote', tablesQuote); + return tablesQuote; +}; + +export const splitTable_D = (use_year, dataSource) => { + const chunked = chunkBy(use_year, dataSource); + // console.log(chunked); + return addCityRow4Split(splitTable_SizeSets(chunked)); +}; + +export const splitTable_J = (use_year, dataSource) => { + const chunked = chunkBy(use_year, dataSource); + // console.log(chunked); + return addCityRow4Split(splitTable_SizeSets(chunked)); +}; + +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) => { + const chunked = chunkBy(use_year, dataSource); + // console.log(chunked); + return addCityRow4Split(splitTable_SizeSets(chunked)); +}; + +export const splitTable_8 = (use_year, dataSource) => { + const chunked = chunkBy(use_year, dataSource); + // console.log(chunked); + return addCityRow4Split(splitTable_SizeSets(chunked)); +}; + +export const splitTable_6 = (use_year, dataSource) => { + const chunked = chunkBy(use_year, dataSource, ['quote_season']); + return (splitTable_Season(chunked)); +}; + +export const splitTable_B = (use_year, dataSource) => { + const chunked = chunkBy(use_year, dataSource); + // console.log(chunked); + return addCityRow4Split(splitTable_SizeSets(chunked)); +}; + +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 }); + return acc.concat(byCity[cityIdName]); + }, []); + return withCityRow; +}; + +export const addCityRow4Split = (splitTables) => { + const tables = splitTables.map(table => { + return { ...table, data: addCityRow4Season(table.data)} + }); + return tables; +}; + diff --git a/src/hooks/useProductsSets.js b/src/hooks/useProductsSets.js index e57733d..01cb409 100644 --- a/src/hooks/useProductsSets.js +++ b/src/hooks/useProductsSets.js @@ -206,6 +206,6 @@ export const PackageTypes = [ { key: '35014', value: '35014', label: '其它(餐补等)' }, ]; -export const formatGroupSize = (min, max) => { - return max === 1000 ? min <= 1 ? '不分人等' : `${min}人以上` : `${min}-${max}`; +export const formatGroupSize = (min, max, suffix = false) => { + return max === 1000 ? min <= 1 ? '不分人等' : `${min}人以上` : (`${min}-${max}`+(suffix ? '人' : '')); }; diff --git a/src/views/products/Audit.jsx b/src/views/products/Audit.jsx index d93de56..a08c6e9 100644 --- a/src/views/products/Audit.jsx +++ b/src/views/products/Audit.jsx @@ -132,20 +132,23 @@ const PriceTable = ({ productType, dataSource, refresh }) => { // title: '', // key: 'action2', // width: '6rem', - // className: 'bg-white', + // className: 'bg-white align-bottom', // onCell: (r, index) => ({ rowSpan: r.rowSpan }), // render: (_, r) => { // const showPublicBtn = null; // r.pendingQuotation ? : null; - // const btn2 = r.showPublicBtn ? ( - // + // + // // ) : null; - // return
{btn2}
; + // return
{btn2}
; // }, // }, ]; diff --git a/src/views/products/Detail/Header.jsx b/src/views/products/Detail/Header.jsx index 5d0cce9..faba56f 100644 --- a/src/views/products/Detail/Header.jsx +++ b/src/views/products/Detail/Header.jsx @@ -24,6 +24,8 @@ import AgencyContract from "../Print/AgencyContract"; import { saveAs } from "file-saver"; import { Packer } from "docx"; +import AgencyPreview from "../Print/AgencyPreview"; + const Header = ({ refresh, ...props }) => { const location = useLocation(); const isEditPage = location.pathname.includes("edit"); @@ -235,6 +237,8 @@ const Header = ({ refresh, ...props }) => { /> + + {/* */} {/* todo: export, 审核完成之后才能导出 */} + + } + content={ + <> + + ( + onClickSnapshotItem(item)} className={viewSnapshotItem.version === item.version ? 'active' : ''}> + {item.version} + + )} + 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' + /> +
+ + + + + } + trigger={['click']} + open={open} + onOpenChange={(v) => { + setOpen(v); + invokeOpenChange(v); + if (v === false) { + setLogData([]); + setViewSnapshotItem([]); + } + }}> + + + ); +}; +export default ProductQuotationSnapshotPopover; diff --git a/src/views/products/Print/AgencyPreview.jsx b/src/views/products/Print/AgencyPreview.jsx new file mode 100644 index 0000000..74ad1ea --- /dev/null +++ b/src/views/products/Print/AgencyPreview.jsx @@ -0,0 +1,756 @@ +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { Button, Drawer, Card, Table } from 'antd'; +import { useTranslation } from 'react-i18next'; +import useProductsStore, { getAgencyAllExtrasAction } from '@/stores/Products/Index'; +import { useProductsTypes, formatGroupSize } from '@/hooks/useProductsSets'; +import { chunkBy, splitTable_6, splitTable_7, splitTable_8, splitTable_B, splitTable_D, splitTable_J, splitTable_Q, splitTable_R } from '@/hooks/useProductsQuotationFormat'; +import { groupBy } from '@/utils/commons'; + +const AgencyPreview = ({ params, ...props }) => { + const { t } = useTranslation(); + const { travel_agency_id, use_year, audit_state } = useParams(); + const productsTypes = useProductsTypes(); + + const [agencyProducts] = useProductsStore((state) => [state.agencyProducts]); + + const [previewMode, setPreviewMode] = useState(false); + const [tables, setTables] = useState([]); + const [extras, setExtras] = useState([]); + + const handlePreview = async () => { + setPreviewMode(true); + + const agencyExtras = await getAgencyAllExtrasAction(params); + setExtras(agencyExtras); + // console.log(agencyExtras) + + // 只显示有产品的类型; 展开产品的价格表, 合并名称列; 转化为价格主表, 携带产品属性信息 + const hasDataTypes = Object.keys(agencyProducts); + const _show = productsTypes + .filter((kk) => hasDataTypes.includes(kk.value)) + .map((typeKey) => { + const typeProducts = agencyProducts[typeKey.value]; + const chunked = chunkBy(use_year, typeProducts); + // console.log(typeKey.label, chunked); + // return {...chunked, typeKey}; + const { SSRange, PSRange, SSsizeSetKey, SSsizeSetsMap, chunk } = chunked; + const bySizeSetKey = groupBy(chunk, 'sizeSetsSS'); // next: city + // return bySizeSetKey + const tables = Object.keys(bySizeSetKey).map((sizeSetsStr) => { + const _thisSSsetProducts = bySizeSetKey[sizeSetsStr]; + return { cols: SSsizeSetsMap[sizeSetsStr], colsKey: sizeSetsStr, data: _thisSSsetProducts }; + }); + return { tables, typeKey }; + }); + // console.log(_show); + setTables(_show); + + // const chunkR = chunkBy(use_year, agencyProducts['D'], []); // 'city_id', + // console.log(chunkR); + }; + + const cityRowHighlights = (record) => (record.info.isCityRow ? 'font-bold text-center ' : '') + + const renderTable_6 = () => { + // console.log('666666'); + if (!('6' in agencyProducts)) { + return null; + } + const tablesQuote = splitTable_6(use_year, agencyProducts['6']); + const table2Rows = tablesQuote.reduce((acc, {info, SS}) => { + return acc.concat(SS.map((v, i) => ({...v, info, rowSpan: i===0 ? SS.length : 0}))); + }, []); + // console.log('table2Rows', table2Rows) + return ( + <> +
{ + return { rowSpan: record.rowSpan }; + }, + }, + { + title: '人等', + dataIndex: 'dateText', + key: 'dateText', + width: '5rem', + render: (_, r) =>
{formatGroupSize(r.group_size_min, r.group_size_max, true)}
, + }, + { + title: '-', + dataIndex: 'SS', + key: 'SS', + width: '9rem', + align: 'center', + children: [ + { title: '成人', dataIndex: ['adult_cost'], key: 'adult_cost', width: '9rem', align: 'center', }, + { title: '儿童', dataIndex: ['child_cost'], key: 'child_cost', width: '9rem', align: 'center', }, + ], + }, + ]} + /> + + ); + }; + + const renderTable_B = () => { + // console.log('BBBBBBBBBBBB'); + if (!('B' in agencyProducts)) { + return null; + } + const tablesQuote = splitTable_B(use_year, agencyProducts.B); + return ( + <> + {tablesQuote.map(({ cols, colsKey, data }, ti) => ( +
{ + return { rowSpan: record.rowSpan }; + }, + }, + { + title: '往返公里数', + dataIndex: ['info', 'km'], + key: 'product_title', + width: '6rem', maxWidth: '6rem', className: 'max-w-4', + onCell: (record) => { + return { rowSpan: record.rowSpan }; + }, + }, + { + title: '时段', + dataIndex: 'dateText', + key: 'dateText', + width: '5rem', + fixed: 'left', + render: (_, { quote_season, use_dates_start, use_dates_end, rowSpan }) => + quote_season === 'SS' ? (rowSpan > 1 ? ( +
+ 平季
(除特殊时段外)
+
+ ) : '') : ( +
+ 特殊时段 +
+ {use_dates_start.replace(`${use_year}-`, '')}~{use_dates_end.replace(`${use_year}-`, '')} +
+
+ ), + }, + ...cols.map((col) => ({ + title: formatGroupSize(...col), + // dataIndex: [formatGroupSize(...col), 'adult_cost'], + key: col[0], + children: [ + { title: '成人', dataIndex: [formatGroupSize(...col), 'adult_cost'], key: 'adult_cost', width: '4rem' }, + { title: '儿童', dataIndex: [formatGroupSize(...col), 'child_cost'], key: 'child_cost', width: '4rem' }, + ], + })), + ]} + dataSource={data} + size={'small'} + pagination={false} + scroll={{ x: 'max-content' }} + /> + ))} + + ); + }; + + const renderTable_J = () => { + // console.clear(); + // console.log('jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj'); + if (!('J' in agencyProducts)) { + return null; + } + const tablesQuote = splitTable_J(use_year, agencyProducts.J); + return ( + <> + {tablesQuote.map(({ cols, colsKey, data }, ti) => ( +
{ + return { rowSpan: record.rowSpan }; + }, + }, + { + title: '时段', + dataIndex: 'dateText', + key: 'dateText', + width: '9rem', + fixed: 'left', + render: (_, { quote_season, use_dates_start, use_dates_end, rowSpan }) => + quote_season === 'SS' ? (rowSpan > 1 ? ( +
+ 平季
(除特殊时段外)
+
+ ) : '') : ( +
+ 特殊时段 +
+ {use_dates_start.replace(`${use_year}-`, '')}~{use_dates_end.replace(`${use_year}-`, '')} +
+
+ ), + }, + // { title: '时段', dataIndex: 'dateText', key: 'dateText', width: '9rem', render: (_, { quote_season, use_dates_start, use_dates_end}) => quote_season === 'SS' ? (
平季
(除特殊时段外)
) : (
特殊时段
{use_dates_start.replace(`${use_year}-`, '')}~{use_dates_end.replace(`${use_year}-`, '')}
)}, + // `特殊时段\n${use_dates_start.replace(`${use_year}-`, '')}~${use_dates_end.replace(`${use_year}-`, '')}` }, + ...cols.map((col) => ({ + title: formatGroupSize(...col), + // dataIndex: [formatGroupSize(...col), 'adult_cost'], + key: col[0], + children: [ + { title: '成人', dataIndex: [formatGroupSize(...col), 'adult_cost'], key: 'adult_cost', width: '4rem' }, + { title: '儿童', dataIndex: [formatGroupSize(...col), 'child_cost'], key: 'child_cost', width: '4rem' }, + ], + })), + ]} + dataSource={data} + size={'small'} + pagination={false} + scroll={{ x: 'max-content' }} + // className='mt-4' + /> + ))} + + ); + }; + + const renderTable_D = () => { + // console.log('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD'); + if (!('D' in agencyProducts)) { + return null; + } + const tablesQuote = splitTable_D(use_year, agencyProducts.D); + return ( + <> + {tablesQuote.map(({ cols, colsKey, data }, ti) => ( +
{ + return { rowSpan: record.rowSpan }; + }, + }, + { + title: '时段', + dataIndex: 'dateText', + key: 'dateText', + width: '9rem', + fixed: 'left', + render: (_, { quote_season, use_dates_start, use_dates_end, rowSpan }) => + quote_season === 'SS' ? (rowSpan > 1 ? ( +
+ 平季
(除特殊时段外)
+
+ ) : '') : ( +
+ 特殊时段 +
+ {use_dates_start.replace(`${use_year}-`, '')}~{use_dates_end.replace(`${use_year}-`, '')} +
+
+ ), + }, + ...cols.map((col) => ({ + title: formatGroupSize(...col), + // dataIndex: [formatGroupSize(...col), 'adult_cost'], + key: col[0], + children: [ + { title: '成人', dataIndex: [formatGroupSize(...col), 'adult_cost'], key: 'adult_cost' }, + { title: '儿童', dataIndex: [formatGroupSize(...col), 'child_cost'], key: 'child_cost' }, + ], + })), + ]} + dataSource={data} + size={'small'} + pagination={false} + scroll={{ x: 'max-content' }} + // className='mt-4' + /> + ))} + + ); + }; + + const renderTable_Q = () => { + // console.log('QQQQQQ'); + if (!('Q' in agencyProducts)) { + return null; + } + const tablesQuote = splitTable_Q(use_year, agencyProducts.Q); + // console.log('tablesQuote', tablesQuote); + return ( + <> +
{ + // return { rowSpan: record.rowSpan }; + // }, + }, + { + title: '平季(除特殊时段外)', + dataIndex: 'SS', + key: 'SS', + width: '9rem', + children: [ + { + title: '成人', + dataIndex: [0, 'adult_cost'], + key: 'adult_cost', + width: '9rem', + render: (_, { SS }) => ( +
+ {(SS || []).length === 1 ? ( +
+ {SS[0].adult_cost} +
+ ) : ( + (SS || []).map((ele, qi) => ( +
+
+ + {formatGroupSize(ele.group_size_min, ele.group_size_max, true)}, {ele.unit_name} + + {ele.adult_cost} +
+
+ )) + )} +
+ ), + }, + { + title: '儿童', + dataIndex: [0, 'child_cost'], + key: 'child_cost', + width: '9rem', + render: (_, { SS }) => ( +
+ {(SS || []).length === 1 ? ( +
+ {SS[0].child_cost} +
+ ) : ( + (SS || []).map((ele, qi) => ( +
+
+ + {formatGroupSize(ele.group_size_min, ele.group_size_max, true)}, {ele.unit_name} + + {ele.child_cost} +
+
+ )) + )} +
+ ), + }, + ], + }, + { + title: '旺季', + dataIndex: 'PS', + key: 'PS', + width: '9rem', + children: [ + { + title: '成人', + dataIndex: [0, 'adult_cost'], + key: 'adult_cost', + width: '9rem', + render: (_, { PS }) => ( +
+ {(PS || []).map((ele, pi) => ( +
+ {ele.adult_cost} +
+ { + ele.rows.map((d, di) => ( +
+ ({d.use_dates_start.replace(`${use_year}-`, '')}~{d.use_dates_end.replace(`${use_year}-`, '')}) +
+ )) + } +
+
+ ))} +
+ ), + }, + { + title: '儿童', + dataIndex: [0, 'child_cost'], + key: 'child_cost', + width: '9rem', + render: (_, { PS }) => ( +
+ {(PS || []).map((ele, pi) => ( +
+ {ele.child_cost} +
+ {ele.rows.map((d, di) => ( +
+ ({d.use_dates_start.replace(`${use_year}-`, '')}~{d.use_dates_end.replace(`${use_year}-`, '')}) +
+ ))} +
+
+ ))} +
+ ), + }, + ], + }, + ]} + /> + + ); + }; + + const renderTable_7 = () => { + // console.log('7777777777777'); + if (!('7' in agencyProducts)) { + return null; + } + const tablesQuote = splitTable_7(use_year, agencyProducts['7']); + // console.log('tableQuote', tablesQuote) + return ( + <> +
{ + // return { rowSpan: record.rowSpan }; + // }, + }, + { + title: ( + <> + 平季(除特殊时段外) +
+ - + 成人 + 儿童 +
+ + ), + dataIndex: 'SS', + key: 'SS', + width: '9rem', + render: (_, { SS }) => ( +
+ {(SS || []).length === 1 ? ( +
+ {SS[0].unit_id === '0' ? '' : `${formatGroupSize(SS[0].group_size_min, SS[0].group_size_max, true)}, ${SS[0].unit_name}`} + {SS[0].adult_cost} + {SS[0].child_cost} +
+ ) : ( + (SS || []).map((ele, qi) => +
+
+ {formatGroupSize(ele.group_size_min, ele.group_size_max, true)}, {ele.unit_name} + {ele.adult_cost} + {ele.child_cost} +
+
) + )} +
+ ), + }, + { + title: ( + <> + 旺季 +
+ 成人 + 儿童 +
+ + ), + dataIndex: 'PS', + key: 'PS', + width: '9rem', + align: 'center', + render: (_, { PS }) => ( +
+ {(PS || []).map((ele, pi) => ( +
+
+ {ele.adult_cost} + {ele.child_cost} +
+
+ {ele.rows.map((d, di) => ( +
+ ({d.use_dates_start.replace(`${use_year}-`, '')}~{d.use_dates_end.replace(`${use_year}-`, '')}) +
+ ))} +
+ {/*
( {ele.rows.map((d) => `${d.use_dates_start.replace(`${use_year}-`, '')}~${d.use_dates_end.replace(`${use_year}-`, '')}`).join('; ')} )
*/} +
+ ))} +
+ ), + }, + { title: '特殊项目', width: '9rem', key: 'extras', render: (_, r) => { + const _extras = extras[r.info.id] || []; + return (
{_extras.map(e =>
{e.info.product_title}【{e.info.product_type_name}】
)}
); + }}, + ]} + /> + + ); + }; + + const renderTable_R = () => { + // console.log('RRRRRRRRRRRRR'); + if (!('R' in agencyProducts)) { + return null; + } + const tablesQuote = splitTable_R(use_year, agencyProducts.R); + return ( + <> + {tablesQuote.map(({ cols, colsKey, data }, ti) => ( +
{ + return { rowSpan: record.rowSpan }; + }, + }, + { + title: '时段', + dataIndex: 'dateText', + key: 'dateText', + width: '5rem', + fixed: 'left', + render: (_, { quote_season, use_dates_start, use_dates_end, rowSpan }) => + quote_season === 'SS' ? (rowSpan > 1 ? ( +
+ 平季
(除特殊时段外)
+
+ ) : '') : ( +
+ 特殊时段 +
+ {use_dates_start.replace(`${use_year}-`, '')}~{use_dates_end.replace(`${use_year}-`, '')} +
+
+ ), + }, + ...cols.map((col) => ({ + title: formatGroupSize(...col), + // dataIndex: [formatGroupSize(...col), 'adult_cost'], + key: col[0], + children: [ + { title: '成人', dataIndex: [formatGroupSize(...col), 'adult_cost'], key: 'adult_cost', width: '4rem' }, + { title: '儿童', dataIndex: [formatGroupSize(...col), 'child_cost'], key: 'child_cost', width: '4rem' }, + ], + })), + { title: '司陪餐补(元/团/餐)', dataIndex: 'extra', key: 'extra', width: '4rem', + onCell: (record, index) => { + return { rowSpan: index === 0 ? data.length : 0 }; + }, + render: _ => (<>不分人等) + }, + ]} + dataSource={data} + size={'small'} + pagination={false} + scroll={{ x: 'max-content' }} + /> + ))} + + ); + }; + + const renderTable_8 = () => { + // console.log('888888'); + if (!('8' in agencyProducts)) { + return null; + } + const tablesQuote = splitTable_8(use_year, agencyProducts['8']); + return ( + <> + {tablesQuote.map(({ cols, colsKey, data }, ti) => ( +
{ + return { rowSpan: record.rowSpan }; + }, + }, + { + title: '时段', + dataIndex: 'dateText', + key: 'dateText', + width: '5rem', + fixed: 'left', + render: (_, { quote_season, use_dates_start, use_dates_end, rowSpan }) => + quote_season === 'SS' ? (rowSpan > 1 ? ( +
+ 平季
(除特殊时段外)
+
+ ) : '') : ( +
+ 特殊时段 +
+ {use_dates_start.replace(`${use_year}-`, '')}~{use_dates_end.replace(`${use_year}-`, '')} +
+
+ ), + }, + ...cols.map((col) => ({ + title: formatGroupSize(...col), + // dataIndex: [formatGroupSize(...col), 'adult_cost'], + key: col[0], + children: [ + { title: '成人', dataIndex: [formatGroupSize(...col), 'adult_cost'], key: 'adult_cost', width: '4rem' }, + { title: '儿童', dataIndex: [formatGroupSize(...col), 'child_cost'], key: 'child_cost', width: '4rem' }, + ], + })), + ]} + dataSource={data} + size={'small'} + pagination={false} + scroll={{ x: 'max-content' }} + /> + ))} + + ); + } + + const typeTableMap = { + '6': { render: renderTable_6 }, + 'B': { render: renderTable_B }, + 'J': { render: renderTable_J }, + 'Q': { render: renderTable_Q }, + '7': { render: renderTable_7 }, + 'R': { render: renderTable_R }, + '8': { render: renderTable_8 }, + 'D': { render: renderTable_D }, + }; + + const renderTableByType = (allTables) => { + return allTables.map(({ typeKey, tables }) => { + return ( + +
+ {typeTableMap[typeKey.value].render()} +
+
+ ); + }); + }; + + return ( + <> + + setPreviewMode(false)} > +
+ {renderTableByType(tables)} +
+
+ + ); +}; +export default AgencyPreview; From a89ec7e0a16a6fd8c81acba7bbd49d30ee83ac7c Mon Sep 17 00:00:00 2001 From: LiaoYijun Date: Fri, 15 Aug 2025 14:04:04 +0800 Subject: [PATCH 07/15] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E8=AF=A6=E6=83=85=E9=A1=B5=E5=90=8E=E9=80=80=E5=AF=BC?= =?UTF-8?q?=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/reservation/Detail.jsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/views/reservation/Detail.jsx b/src/views/reservation/Detail.jsx index 59b99d1..e344063 100644 --- a/src/views/reservation/Detail.jsx +++ b/src/views/reservation/Detail.jsx @@ -1,13 +1,12 @@ -import { useParams } from 'react-router-dom' +import { useParams, useNavigate } from 'react-router-dom' import { useEffect, useState } from 'react' -import { Row, Col, Space, Button, Table, Input, Typography, Modal, Tag, App } from 'antd' +import { Row, Col, Space, Button, Table, Input, Typography, Modal, Tag, App, Flex } from 'antd' import { - FileOutlined + FileOutlined, ArrowLeftOutlined } from '@ant-design/icons' import { usingStorage } from '@/hooks/usingStorage' import useReservationStore from '@/stores/Reservation' import { useTranslation } from 'react-i18next' -import BackBtn from '@/components/BackBtn' const { Title, Paragraph } = Typography const { TextArea } = Input @@ -71,6 +70,7 @@ function Detail() { ); } + const navigate = useNavigate(); const [isModalOpen, setIsModalOpen] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false); const [confirmText, setConfirmText] = useState(''); @@ -161,14 +161,7 @@ function Detail() { /> - - - {t('group:RefNo')}: {reservationDetail.referenceNumber}; {t('group:ArrivalDate')}: {reservationDetail.arrivalDate}; - - - - - +