From 640fa4b593b1f80ab15b7b8edf09a388010e4ba6 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 15 Aug 2025 11:37:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=A7=E5=93=81=E7=AE=A1=E7=90=86:?= =?UTF-8?q?=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;