From 78834f8f118dac806d6d1cc45ced513eda622dee Mon Sep 17 00:00:00 2001 From: Eddie <1150970484@qq.com> Date: Wed, 20 Nov 2024 16:53:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0=E7=9A=84=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=96=87=E6=A1=A3=E7=9A=84class=20=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + package.json | 6 +- .../products/Print/AgencyContract_v1120.jsx | 1351 +++++++++++++++++ 3 files changed, 1356 insertions(+), 2 deletions(-) create mode 100644 src/views/products/Print/AgencyContract_v1120.jsx diff --git a/.gitignore b/.gitignore index ec337d7..a50d8e5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ dist-ssr *.sw? /package-lock.json +pnpm-lock.yaml diff --git a/package.json b/package.json index 790f776..b0376bd 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,10 @@ "preview": "vite preview" }, "dependencies": { + "@ant-design/icons": "^5.5.1", "@react-pdf/renderer": "^3.4.0", "antd": "^5.17.2", + "dayjs": "^1.11.13", "docx": "^8.5.0", "file-saver": "^2.0.5", "i18next": "^23.11.5", @@ -23,8 +25,8 @@ "react-i18next": "^14.1.2", "react-router-dom": "^6.10.0", "react-to-pdf": "^1.0.1", - "zustand": "^4.5.2", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz", + "zustand": "^4.5.2" }, "devDependencies": { "@types/react": "^18.0.28", diff --git a/src/views/products/Print/AgencyContract_v1120.jsx b/src/views/products/Print/AgencyContract_v1120.jsx new file mode 100644 index 0000000..e092cba --- /dev/null +++ b/src/views/products/Print/AgencyContract_v1120.jsx @@ -0,0 +1,1351 @@ +import { + AlignmentType, + Document, + Header, + Footer, + HeadingLevel, + Paragraph, + TabStopType, + Table, + TableCell, + TableRow, + TextRun, + BorderStyle, + NumberFormat, + PageNumber, + WidthType, + LevelFormat, + Tab, + PositionalTab, + PositionalTabAlignment, + PositionalTabRelativeTo, + PositionalTabLeader, +} from 'docx' +import dayjs from 'dayjs' +import { cloneDeep, flush, groupBy, isEmpty, isNotEmpty, sortBy, unique, uniqWith } from '@/utils/commons' + +// 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 shortDate = (date) => dayjs(date).format('MM.DD') + +/** + * 车辆人等配置 + * 5座, 7座, 10座, 10以上 + */ +const chunkCarSets = (numbers) => { + return numbers.reduce( + (acc, number) => { + if (number < 5) { + acc.lt5.push(number) + } else if (number < 7) { + acc.lt7.push(number) + } else if (number < 10) { + acc.lt7.push(number) + } else { + acc.gt10.push(number) + } + return acc + }, + { lt5: [], lt7: [], lt10: [], gt10: [] }, + ) +} +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)) + }) + }) +const isFlatUnique = (bigArray) => { + let flattenedArray = bigArray.flat() + return flattenedArray.every((i) => { + let count = flattenedArray.filter((j) => j === i).length + return count === 1 + }) +} +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 } + }) + // console.log('\ndataRollSS', dataRollSS); + // console.log('\n\n', dataList[0].info.product_type_id, dataRollSS); + + // 人等分组只取平季, 因为产品只一行 + 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 + // console.log('allQuotesSSS', allQuotesSS, '\n', allQuotesPS, '\n', allQuotesSSS); + + // const groupSizeSS = (allQuotesSS).reduce((aq, cq) => aq.concat([[cq.group_size_min, cq.group_size_max]]), []); + 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]) + return aq + }, {}) + const maxGroupSize = Math.max(...allQuotesSSS.map((q) => q.group_size_max)) + const maxSet = maxGroupSize === 1000 ? Infinity : maxGroupSize + // console.log('PGroupSizeSS', structuredClone(PGroupSizeSS)); + const _SSMinSet = uniqWith(Object.values(PGroupSizeSS), (a, b) => a.join(',') === b.join(',')) + // console.log('SSMinSet', _SSMinSet, maxGroupSize); + const uSSsizeSetArr = uniqueBySub(_SSMinSet) + // console.log('uSSsizeSetArr', uSSsizeSetArr); + 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 aaa', PGroupSizeSS); + 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 + }) + // console.log('PGroupSizeSS fff', SSsizeSets); + const compactSizeSets = { + SSsizeSetKey: uSSsizeSetArr.map((s) => s.join(',')).filter(isNotEmpty), + sizeSets: SSsizeSets, + } + // console.log(compactSizeSets); + const chunkSS = structuredClone(dataRollSS).map((rowp) => { + // console.log(rowp.info.id, rowp.info.product_title); + + const pkey = (PGroupSizeSS[rowp.info.id] || []).join(',') || compactSizeSets.SSsizeSetKey[0] // todo: + // console.log('pkey', pkey); + + 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() + // if (isEmpty(ssSets)) { + // console.group('quotation item'); + // console.log(rowp.info.id, rowp.info.product_title, rowp); + // console.log(ssSets); + // console.log([quoteItem.group_size_min, quoteItem.group_size_max]); + // console.groupEnd(); + // } + // console.group('quotation item'); + // console.log(ssSets); + // console.log([quoteItem.group_size_min, quoteItem.group_size_max]); + 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] + // console.log('find mmm', matchRange, findEnd, findStart, finalRange); + // console.log('matchRange ', 'find', [quoteItem.group_size_min, quoteItem.group_size_max], 'matched', finalRange); + // console.groupEnd(); + 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, + } + }) + // console.log('quotation', chunkSS); + 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 } +} + +const DOC_TITLE = '地接合同' +const pageMargins = { + top: `15mm`, + bottom: `15mm`, + left: `10mm`, + right: `10mm`, +} +const tableBorderNone = { + top: { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' }, + bottom: { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' }, + left: { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' }, + right: { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' }, +} +const tableBorderOne = { + top: { style: BorderStyle.SINGLE, space: 0, size: 6, color: 'auto' }, + bottom: { style: BorderStyle.SINGLE, space: 0, size: 6, color: 'auto' }, + left: { style: BorderStyle.SINGLE, space: 0, size: 6, color: 'auto' }, + right: { style: BorderStyle.SINGLE, space: 0, size: 6, color: 'auto' }, +} + +export default class AgencyContract { + #remarkList = [] + constructor(remarkList) { + this.#remarkList = remarkList + console.log(this.#remarkList) + } + create([headerParams, activeAgency, agencyProducts, agencyExtras]) { + const { use_year } = headerParams + const h1 = `${activeAgency.travel_agency_name}${use_year}年${DOC_TITLE}` + const yearStart = dayjs(`${use_year}-01-01`).format('YYYY.MM.DD') + const yearEnd = dayjs(`${use_year}-12-31`).format('YYYY.MM.DD') + // console.log(agencyExtras); + // console.log('*********************************************************************************************'); + + const document = new Document({ + creator: 'China Highlights', + title: h1, + subject: '2025桂林海纳价格合同模板', + // description: '', + styles: { + paragraphStyles: [ + { + id: 'Normal', + name: 'Normal', + quickFormat: true, + run: { + size: 22, + font: { name: '宋体' }, + color: '000000', + }, + paragraph: { + spacing: { + after: 10, + }, + }, + }, + { + id: 'Title', + name: 'Title', + basedOn: 'Normal', + next: 'Normal', + quickFormat: true, + run: { + size: 44, + font: { name: '宋体' }, + color: '000000', + }, + paragraph: { + spacing: { + before: 200, + after: 200, + }, + }, + }, + { + id: 'Heading1', + name: 'Heading 1', + basedOn: 'Normal', + next: 'Normal', + quickFormat: true, + run: { + size: 32, + font: { name: '宋体' }, + color: '000000', + }, + paragraph: { + spacing: { + before: 200, + after: 200, + }, + }, + }, + { + id: 'Heading2', + name: 'Heading 2', + basedOn: 'Normal', + next: 'Normal', + quickFormat: true, + run: { + size: 28, + font: { name: '宋体' }, + color: '000000', + }, + paragraph: { + spacing: { + before: 120, + after: 120, + }, + }, + }, + { + id: 'TableHeader', + name: 'Table Header', + basedOn: 'Normal', + next: 'Normal', + quickFormat: true, + run: { + size: 22, + font: { name: '宋体' }, + color: '000000', + bold: true, + }, + paragraph: { + spacing: { + before: 80, + after: 80, + }, + }, + }, + // { + // id: "MySpectacularStyle", + // name: "My Spectacular Style", + // basedOn: "Heading1", + // next: "Heading1", + // quickFormat: true, + // run: { + // italics: true, + // color: "990000", + // }, + // }, + ], + }, + numbering: { + config: [ + { reference: 'products-type', levels: [{ level: 0, text: '%1、', format: LevelFormat.CHINESE_COUNTING }] }, + { reference: 'terms', levels: [{ level: 0, text: '%1.', format: LevelFormat.DECIMAL }] }, + ], + }, + sections: [ + { + properties: { + page: { + pageNumbers: { + start: 1, + formatType: NumberFormat.DECIMAL, + }, + margin: pageMargins, + }, + }, + headers: { + default: new Header({ + children: [ + this.createPageHeaderText(`Email: xeniayou@hainatravel.com`, { italics: false }), + this.createPageHeaderText(`https://www.chinahighlights.com`, { italics: false }), + ], + }), + }, + footers: { + default: new Footer({ + children: [ + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [ + new TextRun({ + children: ['第', PageNumber.CURRENT, '页'], + size: 20, + }), + new TextRun({ + children: [', 共 ', PageNumber.TOTAL_PAGES, '页'], + size: 20, + }), + ], + }), + new Paragraph({ + alignment: AlignmentType.RIGHT, + children: [ + new TextRun({ + text: `${new Date().toLocaleString()}系统生成`, + italics: true, + size: 20, + }), + ], + }), + ], + }), + }, + children: [ + this.createTitle(h1), + this.createSubHeading(`价格有效期: ${yearStart}-${yearEnd}`, { alignment: AlignmentType.CENTER }), + + ...(isEmpty(agencyProducts['6']) + ? [] + : [ + this.createSubHeading(`综费(元/人/天)`, { numbering: { reference: 'products-type', level: 0 } }), + this.createTable_6(use_year, agencyProducts['6']), + ]), + ...(isEmpty(agencyProducts['B']) + ? [] + : [ + this.createSubHeading(`超公里(元/人)`, { numbering: { reference: 'products-type', level: 0 } }), + ...this.createTable_B(use_year, agencyProducts['B']), + ]), + ...(isEmpty(agencyProducts['J']) + ? [] + : [ + this.createSubHeading(`车费(元/团)`, { numbering: { reference: 'products-type', level: 0 } }), + ...this.createTable_JD(use_year, agencyProducts['J']), + ]), + ...(isEmpty(agencyProducts['Q']) + ? [] + : [ + this.createSubHeading(`导游`, { numbering: { reference: 'products-type', level: 0 } }), + this.createTable_Q(use_year, agencyProducts['Q']), + ]), + ...(isEmpty(agencyProducts['7']) + ? [] + : [ + this.createSubHeading(`景点(默认元/人,部分价格为元/团单独注明)`, { + numbering: { reference: 'products-type', level: 0 }, + }), + this.createTable_7(use_year, agencyProducts['7'], agencyExtras), + ]), + ...(isEmpty(agencyProducts['R']) + ? [] + : [ + this.createSubHeading(`餐费(元/人/餐)`, { numbering: { reference: 'products-type', level: 0 } }), + ...this.createTable_R(use_year, agencyProducts['R'], agencyExtras), + ]), + ...(isEmpty(agencyProducts['D']) + ? [] + : [ + this.createSubHeading(`包价线路(元/团,部分价格为元/人单独注明)`, { + numbering: { reference: 'products-type', level: 0 }, + }), + ...this.createTable_JD(use_year, agencyProducts['D']), + ]), + this.createSubHeading(`公司银行账户`, { numbering: { reference: 'products-type', level: 0 } }), + this.createInfo_Bank(), + this.createSubHeading(`联系资料`, { numbering: { reference: 'products-type', level: 0 } }), + this.createInfo_Contact(), + this.createSubHeading(`补充条款`, { numbering: { reference: 'products-type', level: 0 } }), + ...this.createInfo_Terms(), + ], + }, + ], + }) + + return document + } + + /** + * 综费 + */ + createTable_6(use_year, dataList, style = {}) { + const { chunk, SSRange, sizeSets } = chunkBy(use_year, dataList, ['quote_season']) + // console.log('\n\n\n6', chunk, '\n',sizeSets); + //当前表格的备注信息 + const [remarkItem] = this.#remarkList.filter((i) => i.product_type_id == '6') + + return new Table({ + borders: tableBorderNone, + width: { + size: 100, + type: WidthType.PERCENTAGE, + }, + rows: [ + new TableRow({ + children: [ + this.createTableHeader('项目', 30), + this.createTableHeader('人等', 20), + this.createTableHeader([...SSRange], 50), + ], + }), + ...chunk.reduce((acc, row, ri) => { + return acc.concat(this.createDetailRowSizeSS(sizeSets, row, ri)) + }, []), + + this.createTable_Memo(3, remarkItem.Memo), + ], + }) + } + /** + * 超公里 + */ + createTable_B(use_year, dataList) { + // console.log('*********************************************************************************************'); + const { chunk, sizeSets, SSRange, PSRange } = chunkBy(use_year, dataList, ['quote_size']) + //当前表格的备注信息 + const [remarkItem] = this.#remarkList.filter((i) => i.product_type_id == 'B') + const infoCol = (rowp) => + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [ + new Paragraph({ + text: `${rowp?.info?.km}`, + alignment: AlignmentType.CENTER, + }), + ], + }) + const tableS = sizeSets.reduce((acc, size) => { + const subTable = new Table({ + borders: tableBorderNone, + width: { + size: 100, + type: WidthType.PERCENTAGE, + }, + rows: [ + new TableRow({ + children: [ + this.createTableHeader('超公里项目', 20), + this.createTableHeader('往返公里数', 20), + this.createTableHeader([...SSRange], 60, { columnSpan: size.length }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [new Paragraph({ text: '' })], + }), + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [new Paragraph({ text: '' })], + }), + ...size.map( + (ss) => + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [ + new Paragraph({ + // text: `${ss[0]}`, + text: ss[1] === Infinity ? `${ss[0]}人以上` : `${unique(ss.filter((x) => x)).join('-')}人`, + alignment: AlignmentType.CENTER, + }), + ], + }), + ), + ], + }), + ...chunk + .filter((p) => p.sizeSetsSS === size.map((mr) => mr[0]).join(',')) + .reduce( + (acc, row, ri) => + acc.concat(this.createDetailRowSize(size, row, ri, { infoCol, withSize: false, defaultUnit: '0' })), + [], + ), + // .reduce((a, c) => a.concat(c), []), + + this.createTable_Memo(2 + size.length, remarkItem.Memo), + ], + }) + acc.push(subTable) + acc.push(new Paragraph({ text: `` })) + return acc + }, []) + return tableS + } + + /** + * 车费 + * 包价线路 + */ + createTable_JD(use_year, dataList) { + const chunk_By = ['quote_size'] // dataList[0].info.product_type_id === 'J' ? ['quote_size'] : ['quote_season', 'quote_size']; + const showPSCol = false // dataList[0].info.product_type_id === 'J' ? false : true; + const defaultUnit = '1' // dataList[0].info.product_type_id === 'J' ? '' : ''; + const { dataSource, sizeSets, SSRange, PSRange } = chunkBy(use_year, dataList, chunk_By) + //当前表格的备注信息 + const [remarkItem] = this.#remarkList.filter((i) => i.product_type_id == 'J') + + const tableHeader = this.createTableHeader + const tableS = sizeSets.reduce((rt, setItem) => { + const subTable = new Table({ + borders: tableBorderNone, + width: { + size: 100, + type: WidthType.PERCENTAGE, + }, + rows: [ + new TableRow({ + children: [ + tableHeader('项目', 20), + // tableHeader('', 20), + tableHeader([...SSRange], 40, { columnSpan: setItem.length }), + showPSCol ? tableHeader(['旺季', ...PSRange], 40, { columnSpan: setItem.length }) : undefined, + ], + }), + new TableRow({ + children: [ + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [new Paragraph({ text: '' })], + }), + // new TableCell({ + // borders: tableBorderOne, + // verticalAlign: AlignmentType.CENTER, + // children: [new Paragraph({ text: '时段' })], + // }), + ...setItem.map( + (ss) => + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [ + new Paragraph({ + // text: `${ss[0]}`, + text: ss[1] === Infinity ? `${ss[0]}人以上` : `${unique(ss.filter((x) => x)).join('-')}人`, + alignment: AlignmentType.CENTER, + }), + ], + }), + ), + ...(showPSCol + ? setItem.map( + (ss) => + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [ + new Paragraph({ + // text: `${ss[0]}`, + text: ss[1] === Infinity ? `${ss[0]}人以上` : `${unique(ss.filter((x) => x)).join('-')}人`, + alignment: AlignmentType.CENTER, + }), + ], + }), + ) + : []), + ], + }), + ...dataSource + .filter((p) => p.sizeSetsSS === setItem.map((mr) => mr[0]).join(',')) + .reduce( + (acc, row, ri) => + acc.concat( + this.createDetailRowSize(setItem, row, ri, { withSize: false, defaultUnit, withSeason: showPSCol }), + ), + [], + ), + // .reduce((a, c) => a.concat(c), []), + + this.createTable_Memo(1 + setItem.length * 2, remarkItem.Memo), + ], + }) + rt.push(subTable) + rt.push(new Paragraph({ text: `` })) + return rt + }, []) + return tableS + } + /** + * 导游 + */ + createTable_Q(use_year, dataList) { + const { chunk, SSRange, PSRange } = chunkBy(use_year, dataList, ['quote_season']) + //当前表格的备注信息 + const [remarkItem] = this.#remarkList.filter((i) => i.product_type_id == 'Q') + + return new Table({ + borders: tableBorderNone, + width: { + size: 100, + type: WidthType.PERCENTAGE, + }, + rows: [ + new TableRow({ + children: [ + this.createTableHeader('项目', 30), + this.createTableHeader(['平季(除去旺季以外的时间)', ...SSRange], 35), + this.createTableHeader(['旺季', ...PSRange], 35), + ], + }), + ...chunk + .map((row, ri) => this.createDetailRowSeason2(row, ri, { withSize: false })) + .reduce((a, c) => a.concat(c), []), + + this.createTable_Memo(3, remarkItem.Memo), + ], + }) + } + /** + * 景点 + */ + createTable_7(use_year, dataList, agencyExtras) { + const withExtras = dataList.map((row) => ({ ...row, extras: agencyExtras[row.info.id] || [] })) + const { chunk, sizeSets, SSRange, PSRange } = chunkBy(use_year, withExtras, ['quote_season']) + //当前表格的备注信息 + const [remarkItem] = this.#remarkList.filter((i) => i.product_type_id == '7') + const extraCol = (rowp, i) => { + return new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [ + ...(rowp.extras || []).reduce((ac, extra) => { + const allSize = unique(extra.quotation.map((eq) => `${eq.group_size_min}-${eq.group_size_max}`)) + const allDates = unique(extra.quotation.map((eq) => `${eq.use_dates_start}-${eq.use_dates_end}`)) + // const compactBy = allSize.length > 1 ? 'size' : allDates.length > 1 ? 'dates' : 'all'; + const compactBy = allSize.length < allDates.length ? 'size' : 'dates' + const compactQuotation = groupBy( + extra.quotation, + (ExtraQuoteItem) => + `${ExtraQuoteItem.adult_cost}@${ExtraQuoteItem.unit_id}.` + + (compactBy === 'size' + ? `[${ExtraQuoteItem.group_size_min}.${ExtraQuoteItem.group_size_max}]` + : `${ExtraQuoteItem.use_dates_start},${ExtraQuoteItem.use_dates_end}`), + ) + // console.log(compactQuotation); + const thisE = new Paragraph({ + children: [new TextRun({ text: `${extra.info.product_title}`, bold: true })], + }) + const thisP = Object.entries(compactQuotation).map(([key, [firstQ, ...others]]) => { + const date1 = shortDate(firstQ.use_dates_start) + const date2 = shortDate(firstQ.use_dates_end) + const othersDates = others + .map((oq) => `${shortDate(oq.use_dates_start)}~${shortDate(oq.use_dates_end)}`) + .join(', ') + const otherStr = isEmpty(othersDates) ? '' : `, ${othersDates}` + const sizeStr = + [0, 1].includes(firstQ.group_size_min) && firstQ.group_size_max === 1000 + ? '不分人等,' + : `${firstQ.group_size_min}-${firstQ.group_size_max}人,` + return new Paragraph({ + children: [ + // new TextRun({ text: `${extra.info.product_title} ${firstQ.unit_name}`, bold: true }), + new TextRun({ + text: + `${sizeStr} ${firstQ.unit_name}, ${firstQ.adult_cost}` + + // + (`, ${firstQ.group_size_min}-${firstQ.group_size_max}人`) + (compactBy !== 'dates' ? `, ${date1}~${date2}${otherStr}` : ''), + }), + ], + }) + }) + return ac.concat([thisE, ...thisP]) + // return new Paragraph({ text: `${extra.info.product_title} ${extra.quotation?.[0]?.adult_cost || ''} ${extra.quotation?.[0]?.unit_name || ''}` }); + }, []), + ], + }) + } + + return new Table({ + borders: tableBorderNone, + width: { + size: 100, + type: WidthType.PERCENTAGE, + }, + rows: [ + new TableRow({ + children: [ + this.createTableHeader('景点', 25), + this.createTableHeader(['平季', ...SSRange], 25), + this.createTableHeader(['旺季', ...PSRange], 25), + this.createTableHeader('附加费用', 25), + ], + }), + ...chunk.reduce( + (acc, row, ri) => + acc.concat(this.createDetailRowSeason2(row, ri, { withSize: true, defaultUnit: '0', extraCol })), + [], + ), + // ...chunk.map((row, ri) => this.createDetailRowSeason2(row, ri, { withSize: true, defaultUnit: '0', extraCol })).reduce((a, c) => a.concat(c), []), + + this.createTable_Memo(1 + sizeSets.length * 2 + 1, remarkItem.Memo), + ], + }) + } + /** + * 餐费 + */ + createTable_R(use_year, dataList, agencyExtras) { + const withExtras = dataList.map((row) => ({ ...row, extras: agencyExtras[row.info.id] || [] })) + const { chunk, sizeSets, SSRange, PSRange } = chunkBy(use_year, withExtras, ['quote_size']) + //当前表格的备注信息 + const [remarkItem] = this.#remarkList.filter((i) => i.product_type_id == 'R') + const extraCol = (rowp, i) => + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [ + ...(rowp.extras || []).map( + (extra) => + new Paragraph({ + text: `${extra.info.product_title} ${extra.quotation?.[0]?.adult_cost || ''} ${ + extra.quotation?.[0]?.unit_name || '' + }`, + }), + ), + // new Paragraph({ text: '' }) + ], + }) + + const tableS = sizeSets.reduce((acc, size) => { + const thisChunk = chunk.filter((p) => p.sizeSetsSS === size.map((mr) => mr[0]).join(',')) + const currentChunkExtras = uniqWith(thisChunk.map((p) => p.extras).flat(), (a, b) => a.info.id === b.info.id) + const mergedUniqueExtra = currentChunkExtras.map( + (extra) => + new Paragraph({ + text: `${extra.info.product_title} ${extra.quotation?.[0]?.adult_cost || ''} ${ + extra.quotation?.[0]?.unit_name || '' + }`, + }), + ) + + const subTable = new Table({ + borders: tableBorderNone, + width: { + size: 100, + type: WidthType.PERCENTAGE, + }, + rows: [ + new TableRow({ + children: [ + this.createTableHeader('社会餐厅午餐/晚餐', 25), + this.createTableHeader([...SSRange, ...PSRange], 50, { columnSpan: size.length }), + this.createTableHeader('司陪餐补', 25), + ], + }), + new TableRow({ + children: [ + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [new Paragraph({ text: '' })], + }), + ...size.map( + (ss) => + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [ + new Paragraph({ + // text: `${ss[0]}`, + text: ss[1] === Infinity ? `${ss[0]}人以上` : `${unique(ss.filter((x) => x)).join('-')}人`, + alignment: AlignmentType.CENTER, + }), + ], + }), + ), + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + rowSpan: thisChunk.length + 1, + children: mergedUniqueExtra, + }), // [new Paragraph({ text: `${thisChunk.length+1}` })] + // new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, children: [new Paragraph({ text: '' })] }), + ], + }), + ...thisChunk.reduce( + (acc, row, ri) => acc.concat(this.createDetailRowSize(size, row, ri, { withSize: false })), + [], + ), + + this.createTable_Memo(1 + size.length + 1, remarkItem.Memo), + ], + }) + acc.push(subTable) + acc.push(new Paragraph({ text: `` })) + return acc + }, []) + return tableS + } + /** + * 包价线路 + * @deprecated + */ + createTable_D(use_year, dataList) {} + + createDetailRowSizeSS = ( + sizeSets, + rowp, + ii, + { withSeason = false, withSize = true, infoCol = () => {}, extraCol = () => {} } = {}, + ) => { + const thisSizeCol = rowp.sizeSetsSS + .split(',') + .map((min, i, _arr) => [Number(min), _arr[i + 1] ? Number(_arr[i + 1]) - 1 : Infinity]) + + const sizeLength = thisSizeCol.length + + const quote_chunk = (rowp.quote_chunk?.SS || []).reduce((acc, cur, i) => { + return { ...acc, [cur.quote_size]: cur.adult_cost } + }, {}) + + const data = thisSizeCol.map((sizeRow, ri) => { + + return new TableRow({ + children: flush([ + // new TableCell({ + // borders: tableBorderOne, + // rowSpan: 3, + // verticalAlign: AlignmentType.CENTER, + // children: [new Paragraph(String(ii + 1))], + // }), + ...(ri === 0 + ? [ + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + rowSpan: sizeLength, + children: [ + new Paragraph({ + text: `${rowp?.info?.product_title}`, + alignment: AlignmentType.LEFT, + }), + ], + }), + infoCol(rowp), + ] + : []), + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [ + new Paragraph({ + text: + sizeRow[1] === Infinity ? `${sizeRow[0]}人以上` : `${unique(sizeRow.filter((x) => x)).join('-')}人`, + alignment: AlignmentType.CENTER, + }), + ], + }), + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [ + new Paragraph({ + text: `${quote_chunk?.[sizeRow.join('-')] || ''}`, + alignment: AlignmentType.CENTER, + }), + ], + }), + extraCol(rowp, ii), + ]), + }) + }) + console.log(data) + return data + } + + createDetailRowSize( + sizeSets, + rowp, + ii, + { withSeason = false, withSize = true, infoCol = () => {}, extraCol = () => {}, defaultUnit = '0' } = {}, + ) { + const sizeCol = sizeSets.map((ss) => ss.join('-')) + + return [ + new TableRow({ + children: flush([ + // new TableCell({ + // borders: tableBorderOne, + // rowSpan: 3, + // verticalAlign: AlignmentType.CENTER, + // children: [new Paragraph(String(ii + 1))], + // }), + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [ + new Paragraph({ + text: `${rowp?.info?.product_title}`, + alignment: AlignmentType.LEFT, + }), + ], + }), + infoCol(rowp), + ...sizeCol.map( + (ss) =>{ + return new TableCell({ + borders: tableBorderOne, + width: { size: 1500, type: WidthType.DXA }, + verticalAlign: AlignmentType.CENTER, + children: [ + ...((withSeason ? rowp.quote_chunk?.['SS']?.[ss] : rowp.quote_chunk[ss]) || []).map( + (quoteItem, ii) => + new Paragraph({ + alignment: AlignmentType.CENTER, + text: isEmpty(quoteItem) + ? '' + : withSize && String(quoteItem.unit_id) !== defaultUnit + ? `${quoteItem.group_size_min}-${quoteItem.group_size_max}人,${quoteItem.unit_name}, ${quoteItem.adult_cost}` + : `${quoteItem.adult_cost}`, + }), + ), + // new Paragraph({ text: '-'+ii }) + ], + }) + } + ), + ...(withSeason + ? sizeCol.map( + (ss) => + new TableCell({ + borders: tableBorderOne, + width: { size: 2000, type: WidthType.DXA }, + verticalAlign: AlignmentType.CENTER, + children: [ + ...(rowp.quote_chunk?.['PS']?.[ss] || []).map( + (quoteItem, ii) => + new Paragraph({ + alignment: AlignmentType.CENTER, + // text: `${quoteItem.adult_cost}, ${quoteItem.group_size_min}-${quoteItem.group_size_max}${quoteItem.unit_name}`, + text: + withSize && String(quoteItem.unit_id) !== defaultUnit + ? `${quoteItem.group_size_min}-${quoteItem.group_size_max}人,${quoteItem.unit_name}, ${quoteItem.adult_cost}` + : `${quoteItem.adult_cost}`, + }), + ), + ], + }), + ) + : []), + extraCol(rowp, ii), + ]), + }), + ] + } + createDetailRowSeason2(rowp, ii, { withSize = true, defaultUnit = '0', extraCol = () => {} } = {}) { + return [ + new TableRow({ + children: [ + // new TableCell({ + // borders: tableBorderOne, + // rowSpan: 3, + // verticalAlign: AlignmentType.CENTER, + // children: [new Paragraph(String(ii + 1))], + // }), + new TableCell({ + borders: tableBorderOne, + verticalAlign: AlignmentType.CENTER, + children: [ + new Paragraph({ + text: `${rowp?.info?.product_title}`, + alignment: AlignmentType.LEFT, + }), + ], + }), + new TableCell({ + borders: tableBorderOne, + width: { size: 2000, type: WidthType.DXA }, + verticalAlign: AlignmentType.CENTER, + children: [ + ...(rowp.quote_chunk['SS'] || []).map( + (quoteItem, ii) => + new Paragraph({ + alignment: AlignmentType.CENTER, + // text: `${quoteItem.adult_cost}, ${quoteItem.group_size_min}-${quoteItem.group_size_max}人,${quoteItem.unit_name}`, + text: + (withSize && String(quoteItem.unit_id) !== defaultUnit + ? `${quoteItem.group_size_min}-${quoteItem.group_size_max}人,${quoteItem.unit_name},` + : '') + `${quoteItem.adult_cost}`, + }), + ), + ], + }), + new TableCell({ + borders: tableBorderOne, + width: { size: 2000, type: WidthType.DXA }, + verticalAlign: AlignmentType.CENTER, + children: [ + ...(rowp.quote_chunk['PS'] || []).map( + (quoteItem, ii) => + new Paragraph({ + alignment: AlignmentType.CENTER, + // text: `${quoteItem.adult_cost}, ${quoteItem.group_size_min}-${quoteItem.group_size_max}人,${quoteItem.unit_name}`, + text: + (withSize && String(quoteItem.unit_id) !== defaultUnit + ? `${quoteItem.group_size_min}-${quoteItem.group_size_max}人,${quoteItem.unit_name},` + : '') + `${quoteItem.adult_cost}`, + }), + ), + ], + }), + extraCol(rowp, ii), + ], + }), + ] + } + + createTable_Memo(columnSpan, remarkContent = '') { + return new TableRow({ + children: [ + new TableCell({ + borders: tableBorderOne, + // verticalAlign: AlignmentType.CENTER, + columnSpan, + children: [new Paragraph(`备注: `), new Paragraph(remarkContent)], + }), + ], + }) + } + + createInfo_Bank() { + const Col1 = (label, opt = {}) => + new TableCell({ + borders: tableBorderOne, + width: { size: 2000, type: WidthType.DXA }, + // verticalAlign: AlignmentType.CENTER, + children: [new Paragraph(label)], + ...opt, + }) + return new Table({ + borders: tableBorderNone, + width: { + size: 100, + type: WidthType.PERCENTAGE, + }, + rows: [ + new TableRow({ + children: [Col1('帐户:'), new TableCell({ borders: tableBorderOne, children: [new Paragraph(``)] })], + }), + new TableRow({ + children: [Col1('银行:'), new TableCell({ borders: tableBorderOne, children: [new Paragraph(``)] })], + }), + new TableRow({ + children: [Col1('帐号:'), new TableCell({ borders: tableBorderOne, children: [new Paragraph(``)] })], + }), + new TableRow({ + children: [Col1('结账方式:'), new TableCell({ borders: tableBorderOne, children: [new Paragraph(``)] })], + }), + ], + }) + } + createInfo_Contact() { + const Col1 = (label, opt = {}) => + new TableCell({ + borders: tableBorderOne, + width: { size: 2000, type: WidthType.DXA }, + // verticalAlign: AlignmentType.CENTER, + children: [new Paragraph(label)], + ...opt, + }) + return new Table({ + borders: tableBorderNone, + width: { + size: 100, + type: WidthType.PERCENTAGE, + }, + rows: [ + new TableRow({ + children: [ + this.createTableHeader('联系人', 25), + this.createTableHeader('电话', 25), + this.createTableHeader('微信', 25), + this.createTableHeader('手机', 25), + ], + }), + new TableRow({ + children: [ + Col1('经理:'), + ...[null, null, null].map( + (col) => new TableCell({ borders: tableBorderOne, children: [new Paragraph(``)] }), + ), + ], + }), + new TableRow({ + children: [ + Col1('计调:'), + ...[null, null, null].map( + (col) => new TableCell({ borders: tableBorderOne, children: [new Paragraph(``)] }), + ), + ], + }), + new TableRow({ + children: [ + Col1('财务(发票):'), + ...[null, null, null].map( + (col) => new TableCell({ borders: tableBorderOne, children: [new Paragraph(``)] }), + ), + ], + }), + ], + }) + } + createInfo_Terms() { + const subHeader = (text) => + new Paragraph({ + alignment: AlignmentType.CENTER, + outlineLevel: 1, + spacing: { after: 200, before: 200 }, + style: 'TableHeader', + children: [new TextRun({ text: `${text}`, bold: true, size: 28 })], + }) + const numberingContent = ([abstract, ...content], instance) => [ + new Paragraph({ text: abstract, indent: { firstLine: `2pc` } }), + ...content.map( + ([itemTitle, ...itemList]) => + new Paragraph({ + text: itemTitle, + numbering: { reference: 'terms', level: 0, instance }, + children: itemList.map((li) => new TextRun({ text: li, break: 1 })), + }), + ), + ] + + return [ + subHeader('中华游专利产品接待权标准'), + ...numberingContent( + [ + '中华游是注重特色新产品开发的品牌,并希望通过开发新产品丰富中华游的网前线路,吸引更多的客户与中华游订购中国之旅。为了更好的鼓励各个地接社积极开发新产品、新线路,特制定中华游专利产品接待权标准,详情如下:', + [ + '新产品,新线路定义及专利期', + '新产品,新线路非市场上现成的新产品,新体验,而是包含了独家设计或独家资源的产品和体验', + ], + [ + '中华游和地接社联合设计研发的新产品', + '由中华游发起立意和产品框架', + '地接社协助填充具体内容并落地', + '专利期:6个月', + ], + [ + '地接社自主设计研发并落地(中华游或做细微调整)', + '如:北京“民生探索”', + '专利期:永久 (除非地接社已无法掌握产品或资源的独家性)', + ], + ], + 1, + ), + subHeader('关于价格变更'), + ...numberingContent( + [ + '中华游与各地接社签订年度地接协议后,将不认同中途涨价的要求。但考虑在实际合作中存在的不可控因素,为维护双方共同利益,两种价格变更情况处理方式如下', + [ + '常规价格变更:', + '一般景区和酒店会提前3个月通知价格变更事宜,要求地接社及时用聊天工具或邮件形式通知中华游产品采购,并以收到相同形式确认回执为准,否则中华游将有权拒付额外增加的一切费用。额外增加费用的承担规则为已确认团队计划保持原价,新价格仅适用于确认回复日期起的新发送团队计划。', + ], + [ + '临时价格变更:', + '一般属于政府行为或景区强制要求,如不立即交款则无法参观等紧急情况。因此种情况产生的额外费用要求第一时间通知中华游产品采购,并秉承维护双方共同利益与长期合作,共同协商友好解决方案。', + ], + ], + 2, + ), + new Paragraph({ + text: '签名盖章', + spacing: { before: 600, after: 200 }, + children: [ + new TextRun({ break: 2, children: ['甲方:', new Tab(), '乙方:桂林海纳国际旅行社有限公司'] }), + new TextRun({ break: 1, children: ['负责人签字盖章', new Tab(), '负责人签字盖章'] }), + new TextRun({ break: 3, children: ['联系人: ', new Tab(), '联系人:游佳佳'] }), + new TextRun({ break: 1, children: ['电话: ', new Tab(), '电话:13507831547'] }), + new TextRun({ break: 1, children: ['电子邮箱: ', new Tab(), '电子邮箱:xeniayou@hainatravel.com'] }), + new TextRun({ break: 1, children: [' 年 月 日', new Tab(), ' 年 月 日'] }), + ], + tabStops: [{ type: TabStopType.LEFT, position: 4800 }], + }), + // new Paragraph({ + // tabStops: [{ type: TabStopType.RIGHT, position: 1000 }], + // children: [ + // new TextRun({ + // children: [ + // '\t年', + // '\t\t月', + // '\t\t日', + // new PositionalTab({ + // alignment: PositionalTabAlignment.CENTER, + // relativeTo: PositionalTabRelativeTo.MARGIN, + // leader: PositionalTabLeader.NONE, + // }), + // // new Tab(), + // '\t年', + // '\t\t月', + // '\t\t日', + // ], + // }), + // ], + // }), + ] + } + createTableHeader(text, w = null, opt = {}) { + const textP = typeof text === 'string' ? [text] : text + return new TableCell({ + borders: tableBorderOne, + width: { size: w || `8mm`, type: w ? WidthType.PERCENTAGE : WidthType.AUTO }, + verticalAlign: AlignmentType.CENTER, + children: textP.map((t) => new Paragraph({ text: t, alignment: AlignmentType.CENTER, style: 'TableHeader' })), + ...opt, + }) + } + createHeading(text) { + return new Paragraph({ + text: text, + heading: HeadingLevel.HEADING_1, + // thematicBreak: true, + alignment: AlignmentType.CENTER, + }) + } + + createTitle(text) { + return new Paragraph({ + text: text, + heading: HeadingLevel.TITLE, + // thematicBreak: true, + alignment: AlignmentType.CENTER, + }) + } + + createSubHeading(text, style = {}) { + return new Paragraph({ + text: text, + heading: HeadingLevel.HEADING_1, + ...style, + }) + } + createPageHeaderText(text, style = {}) { + return new Paragraph({ + alignment: AlignmentType.RIGHT, + children: [ + new TextRun({ + text: text, + italics: true, + size: 20, + ...style, + }), + ], + }) + } +}