@@ -235,13 +204,8 @@ const Header = ({ refresh, ...props }) => {
/>
- {/* todo: export, 审核完成之后才能导出 */}
-
-
- {/* */}
-
+
+
{/* {activeAgencyState === 0 && ( */}
<>
diff --git a/src/views/products/Detail/ProductInfoQuotation.jsx b/src/views/products/Detail/ProductInfoQuotation.jsx
index 853e459..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,13 +239,9 @@ const ProductInfoQuotation = ({ editable, ...props }) => {
title: (
<>
{t("products:unit_name")}{" "}
-
+
- {" "}
+
>
),
dataIndex: "unit_id",
@@ -283,15 +262,14 @@ const ProductInfoQuotation = ({ editable, ...props }) => {
{t("products:use_dates")}{" "}
- {" "}
+
>
),
dataIndex: "use_dates",
- // width: '6rem',
render: (_, record) =>
`${record.use_dates_start}-${record.use_dates_end}`,
},
@@ -302,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 (
-
+
+
);
},
diff --git a/src/views/products/Detail/ProductQuotationLogPopover.jsx b/src/views/products/Detail/ProductQuotationLogPopover.jsx
index cdbc813..99a7d5b 100644
--- a/src/views/products/Detail/ProductQuotationLogPopover.jsx
+++ b/src/views/products/Detail/ProductQuotationLogPopover.jsx
@@ -23,6 +23,14 @@ const getPPRunningAction = async (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 === '') {
@@ -177,6 +185,15 @@ const useLogMethod = (method) => {
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];
};
@@ -190,7 +207,7 @@ const useLogMethod = (method) => {
*
* @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'} props.method - Determines data source - "history" for change logs or "published" for published quotations
+ * @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)
diff --git a/src/views/products/Detail/ProductQuotationSnapshotPopover.jsx b/src/views/products/Detail/ProductQuotationSnapshotPopover.jsx
new file mode 100644
index 0000000..8bae688
--- /dev/null
+++ b/src/views/products/Detail/ProductQuotationSnapshotPopover.jsx
@@ -0,0 +1,299 @@
+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 ? (
+ {`${_changed.adult_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}
+ ) : null;
+ const editCls = ifCompare && ifData ? 'text-danger' : '';
+ return (
+
+ {preValue}
+ {`${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}
+
+ );
+ },
+ },
+ {
+ 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 ? (
+ {`${_changed.child_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}
+ ) : null;
+ const editCls = ifCompare && ifData ? 'text-danger' : '';
+ return (
+
+ {preValue}
+ {`${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}
+
+ );
+ },
+ },
+ // {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)) ? (
+ {`${_changed.group_size_min ?? group_size_min} - ${_changed.group_size_max ?? group_size_max}`}
+ ) : null;
+ const editCls = colorize && ![-1, 1, 2].includes(audit_state_id) && (isNotEmpty(_changed.group_size_min) || isNotEmpty(_changed.group_size_max)) ? 'text-danger' : '';
+ return (
+
+ {preValue}
+ {formatGroupSize(group_size_min, group_size_max)}
+
+ );
+ },
+ },
+ {
+ 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)) ? (
+
+ {isNotEmpty(_changed.use_dates_start) ? {_changed.use_dates_start} : use_dates_start} ~{' '}
+ {isNotEmpty(_changed.use_dates_end) ? {_changed.use_dates_end} : use_dates_end}
+
+ ) : null;
+ const editCls = colorize && ![-1, 1, 2].includes(audit_state_id) && (isNotEmpty(_changed.use_dates_start) || isNotEmpty(_changed.use_dates_end)) ? 'text-danger' : '';
+ return (
+
+ {preValue}
+ {`${use_dates_start} ~ ${use_dates_end}`}
+
+ );
+ },
+ },
+ {
+ 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 ? {_changed.weekdayList}
: null;
+ const editCls = ifCompare && ifData ? 'text-danger' : '';
+ return (
+
+ {preValue}
+ {text || t('Unlimited')}
+
+ );
+ },
+ },
+];
+
+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 (
+
+ {title}
+ {subTitle && {subTitle}}
+
+
+ }
+ content={
+ <>
+
+ {isNotEmpty(itemLink) && (r.info?.isCityRow !== true) ? (
+
setEditingProduct({ info: r.info })}>
+ {title}
+
+ ) : (
+ title
+ )}
+
{r.info.id}
+
+ );
+ };
+
+ const renderTable_6 = () => {
+ // console.log('666666');
+ if (!('6' in agencyProducts)) {
+ return null;
+ }
+ const {tables, SSRange} = splitTable_6(use_year, agencyProducts['6'], false);
+ const table2Rows = tables.reduce((acc, {info, SS}) => {
+ return acc.concat(SS.map((v, i) => ({...v, info, rowSpan: i===0 ? SS.length : 0})));
+ }, []);
+ // console.log('tablesQuote', tablesQuote)
+ // console.log('table2Rows', table2Rows)
+ return (
+ <>
+