diff --git a/src/views/products/Audit.jsx b/src/views/products/Audit.jsx index c317af1..d0bc1a4 100644 --- a/src/views/products/Audit.jsx +++ b/src/views/products/Audit.jsx @@ -1,10 +1,10 @@ import { useEffect, useState } from 'react'; import { useParams, Link } from 'react-router-dom'; -import { App, Empty, Button, Collapse, Table, Space, Alert, Tooltip, Popover, Typography } from 'antd'; -import { useProductsTypes, useProductsAuditStatesMapVal, formatGroupSize } from '@/hooks/useProductsSets'; +import { App, Empty, Button, Collapse, Table, Space, Alert } from 'antd'; +import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets'; import SecondHeaderWrapper from '@/components/SecondHeaderWrapper'; import { useTranslation } from 'react-i18next'; -import useProductsStore, { getPPLogAction, getPPRunningAction, postProductsQuoteAuditAction, } from '@/stores/Products/Index'; +import useProductsStore, { postProductsQuoteAuditAction, } from '@/stores/Products/Index'; import { cloneDeep, groupBy, isEmpty, isNotEmpty } from '@/utils/commons'; import useAuthStore from '@/stores/Auth'; import RequireAuth from '@/components/RequireAuth'; @@ -12,165 +12,8 @@ import { PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFE import Header from './Detail/Header'; import dayjs from 'dayjs'; import { usingStorage } from '@/hooks/usingStorage'; -import { ClockCircleFilled, ClockCircleOutlined, PlusCircleFilled, PlusCircleOutlined } from '@ant-design/icons'; - -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 {}; - } -}; - - 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 PriceLogPopover = ({ title, fetchData, triggerProps={}, ...props}) => { - const { t } = useTranslation('products'); - const [open, setOpen] = useState(false); - const [logData, setLogData] = useState([]); - const getData = async () => { - const data = await fetchData(); - setLogData(data); - }; - const columns = [...columnsSets(t, false), { title: '时间', dataIndex: 'updatetime', key: 'updatetime'}]; - return ( - - {title} - - - } - content={ - <> - - - } - trigger={['click']} - placement='bottom' - className='' - rootClassName='w-5/6' - open={open} - onOpenChange={(v) => { - setOpen(v); - }}> - - - ); -}; +import { ClockCircleOutlined, PlusCircleFilled } from '@ant-design/icons'; +import ProductQuotationLogPopover, { columnsSets } from './Detail/ProductQuotationLogPopover'; const PriceTable = ({ productType, dataSource, refresh }) => { const { t } = useTranslation('products'); @@ -210,16 +53,6 @@ const PriceTable = ({ productType, dataSource, refresh }) => { }); }; - const getLog = async ({ product_id, price_id }) => { - const data = await getPPLogAction({ travel_agency_id, product_id, price_id }); - return data; - }; - - const getRunning = async ({ product_id }) => { - const data = await getPPRunningAction({ travel_agency_id, product_id_list: product_id }); - return data?.[0]?.quotation || []; - }; - const rowStyle = (r, tri) => { const trCls = tri%2 !== 0 ? ' bg-stone-50' : ''; // 奇偶行 const [infoI, quoteI] = r.rowSpanI; @@ -255,7 +88,7 @@ const PriceTable = ({ productType, dataSource, refresh }) => { - getLog({ travel_agency_id, product_id: r.info.id, price_id: r.id })} {...{ travel_agency_id, product_id: r.info.id, price_id: r.id }} /> + ) : null, @@ -267,13 +100,12 @@ const PriceTable = ({ productType, dataSource, refresh }) => { render: (_, r) => { const showPublicBtn = null; // r.pendingQuotation ? : null; const btn2 = r.pendingQuotation ? ( - getRunning({ travel_agency_id, product_id: r.info.id, price_id: r.id })} - {...{ travel_agency_id, product_id: r.info.id, price_id: r.id }} /> ) : null; return
{btn2}
; diff --git a/src/views/products/Detail/ProductQuotationLogPopover.jsx b/src/views/products/Detail/ProductQuotationLogPopover.jsx new file mode 100644 index 0000000..26aed6b --- /dev/null +++ b/src/views/products/Detail/ProductQuotationLogPopover.jsx @@ -0,0 +1,211 @@ +import { useState } from 'react'; +import { Button, Table, Popover, Typography } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { getPPLogAction, getPPRunningAction } from '@/stores/Products/Index'; +import { formatGroupSize } from '@/hooks/useProductsSets'; +import { isEmpty, isNotEmpty } from '@/utils/commons'; + +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'), + fetchData: async (params) => { + const data = await getPPLogAction(params); + return data; + }, + }, + 'published': { + title: t('versionPublished'), + fetchData: async (params) => { + const { travel_agency_id, product_id, price_id } = params; + const data = await getPPRunningAction({ travel_agency_id, product_id_list: product_id }); + 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'} 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) + */ +const ProductQuotationLogPopover = ({ method, triggerProps = {}, ...props }) => { + const { travel_agency_id, product_id, price_id } = props; + + const { t } = useTranslation('products'); + const [open, setOpen] = useState(false); + const [logData, setLogData] = useState([]); + + const { title, fetchData } = useLogMethod(method); + + const getData = async () => { + const data = await fetchData({ travel_agency_id, product_id, price_id }); + setLogData(data); + }; + + const columns = [...columnsSets(t, false), { title: '时间', dataIndex: 'updatetime', key: 'updatetime' }]; + return ( + + {title} + + + } + content={ + <> +
+ + } + trigger={['click']} + placement='bottom' + className='' + rootClassName='w-5/6' + open={open} + onOpenChange={(v) => { + setOpen(v); + }}> + + + ); +}; +export default ProductQuotationLogPopover;