diff --git a/package.json b/package.json
index 046ad36..039bf95 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "global-highlights-hub",
"private": true,
- "version": "2.0.0-beta.5",
+ "version": "2.0.0-rc.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/public/locales/en/products.json b/public/locales/en/products.json
index 5ec35f9..7f4357f 100644
--- a/public/locales/en/products.json
+++ b/public/locales/en/products.json
@@ -105,6 +105,7 @@
"KM": "",
"Description": "",
"Remarks": "",
+ "PriceUnit": "报价单位:每人/每团",
"UseDates": "数值越短,价格越优先。举例:英文导游1.1-12.31价300元, 8.1-8.31价400元,如走团时间为8月,自动取400元。",
"NewTitle": {
"6": "",
diff --git a/public/locales/zh/products.json b/public/locales/zh/products.json
index f4c4d2c..fbd6c94 100644
--- a/public/locales/zh/products.json
+++ b/public/locales/zh/products.json
@@ -105,6 +105,7 @@
"KM": "",
"Description": "",
"Remarks": "",
+ "PriceUnit": "报价单位:每人/每团",
"UseDates": "数值越短,价格越优先。举例:英文导游1.1-12.31价300元, 8.1-8.31价400元,如走团时间为8月,自动取400元。",
"NewTitle": {
"6": "",
diff --git a/src/stores/Products/Index.js b/src/stores/Products/Index.js
index 45307c6..7e51280 100644
--- a/src/stores/Products/Index.js
+++ b/src/stores/Products/Index.js
@@ -35,6 +35,13 @@ export const getAgencyProductsAction = async (param) => {
return errcode !== 0 ? { agency: {}, products: [] } : result;
};
+export const getAgencyAllExtrasAction = async (param) => {
+ const _param = { ...param, use_year: String(param.use_year || '').replace('all', ''), };
+ const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_products_extras`, _param);
+ const extrasMapped = result.reduce((acc, curr) => ({...acc, [curr.product_id]: curr.extras}), {});
+ return errcode !== 0 ? {} : extrasMapped;
+}
+
/**
* @param {object} body { id, travel_agency_id, extras: [{id, title, code}] }
*/
diff --git a/src/utils/commons.js b/src/utils/commons.js
index 0e93cc5..c343b05 100644
--- a/src/utils/commons.js
+++ b/src/utils/commons.js
@@ -162,6 +162,13 @@ export const sortArrayByOrder = (items, keyName, keyOrder) => {
return keyOrder.indexOf(a[keyName]) - keyOrder.indexOf(b[keyName]);
});
};
+
+export function unique(arr) {
+ const x = new Set(arr);
+ return [...x];
+}
+export const uniqWith = (arr, fn) => arr.filter((element, index) => arr.findIndex((step) => fn(element, step)) === index);
+
/**
* 合并Object, 递归地
*/
diff --git a/src/views/products/Audit.jsx b/src/views/products/Audit.jsx
index 3a146eb..3fb2941 100644
--- a/src/views/products/Audit.jsx
+++ b/src/views/products/Audit.jsx
@@ -172,7 +172,7 @@ const Audit = ({ ...props }) => {
const [loading, setLoading] = useProductsStore(state => [state.loading, state.setLoading]);
const { travelAgencyId } = usingStorage();
- const handleGetAgencyProducts = ({pick_year, pick_agency, pick_state}={}) => {
+ const handleGetAgencyProducts = async ({pick_year, pick_agency, pick_state}={}) => {
const year = pick_year || use_year || dayjs().year();
const agency = pick_agency || travel_agency_id || travelAgencyId;
const state = pick_state ?? audit_state;
diff --git a/src/views/products/Detail.jsx b/src/views/products/Detail.jsx
index e28175e..d9ea10d 100644
--- a/src/views/products/Detail.jsx
+++ b/src/views/products/Detail.jsx
@@ -20,7 +20,7 @@ function Detail() {
const [loading, setLoading] = useProductsStore((state) => [state.loading, state.setLoading]);
const { travelAgencyId } = usingStorage();
- const handleGetAgencyProducts = ({ pick_year, pick_agency, pick_state } = {}) => {
+ const handleGetAgencyProducts = async ({ pick_year, pick_agency, pick_state } = {}) => {
const year = pick_year || use_year || switchParams.use_year || dayjs().year();
const agency = pick_agency || travel_agency_id || travelAgencyId;
const state = pick_state ?? audit_state;
diff --git a/src/views/products/Detail/Header.jsx b/src/views/products/Detail/Header.jsx
index 0964917..796ec99 100644
--- a/src/views/products/Detail/Header.jsx
+++ b/src/views/products/Detail/Header.jsx
@@ -4,7 +4,7 @@ import { App, Button, Divider, Popconfirm, Select } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import { useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
import { useTranslation } from 'react-i18next';
-import useProductsStore, { postAgencyProductsAuditAction, postAgencyAuditAction } from '@/stores/Products/Index';
+import useProductsStore, { postAgencyProductsAuditAction, postAgencyAuditAction, getAgencyAllExtrasAction } from '@/stores/Products/Index';
import { isEmpty, objectMapper } from '@/utils/commons';
import useAuthStore from '@/stores/Auth';
import RequireAuth from '@/components/RequireAuth';
@@ -30,7 +30,7 @@ const Header = ({ refresh, ...props }) => {
const [activeAgency, setActiveAgency] = useProductsStore((state) => [state.activeAgency, state.setActiveAgency]);
const [switchParams, setSwitchParams] = useProductsStore((state) => [state.switchParams, state.setSwitchParams]);
// const [activeAgencyState] = useProductsStore((state) => [state.activeAgencyState]);
- // const [agencyProducts] = useProductsStore((state) => [state.agencyProducts]);
+ const [agencyProducts] = useProductsStore((state) => [state.agencyProducts]);
const stateMapVal = useProductsAuditStatesMapVal();
const { message, notification } = App.useApp();
const navigate = useNavigate();
@@ -129,12 +129,15 @@ const Header = ({ refresh, ...props }) => {
};
- const handleDownload = () => {
+ const handleDownload = async () => {
+ // await refresh();
+ const agencyExtras = await getAgencyAllExtrasAction(switchParams);
const documentCreator = new AgencyContract();
- const doc = documentCreator.create([]);
+ const doc = documentCreator.create([switchParams, activeAgency, agencyProducts, agencyExtras]);
+ const _d = dayjs().format('YYYYMMDD_HH.mm.ss.SSS'); // Date.now().toString(32)
Packer.toBlob(doc).then(blob => {
- saveAs(blob, `${pickYear}地接合同-${Date.now().toString(32)}.docx`);
+ saveAs(blob, `${activeAgency.travel_agency_name}${pickYear}年地接合同-${_d}.docx`);
});
};
diff --git a/src/views/products/Detail/ProductInfo.jsx b/src/views/products/Detail/ProductInfo.jsx
index 0c1f4c1..8cd0d83 100644
--- a/src/views/products/Detail/ProductInfo.jsx
+++ b/src/views/products/Detail/ProductInfo.jsx
@@ -98,8 +98,8 @@ const ProductInfo = ({ ...props }) => {
items={[
{ title: productsTypesMapVal[editingProduct?.info?.product_type_id]?.label || editingProduct?.info?.product_type_name },
{ title: editingProduct?.info?.title ?? t('New') },
- { title: 'htID: ' + editingProduct?.info?.htid },
- { title: 'ID: ' + editingProduct?.info?.id },
+ // { title: 'htID: ' + editingProduct?.info?.htid },
+ // { title: 'ID: ' + editingProduct?.info?.id },
]}
/>
diff --git a/src/views/products/Detail/ProductInfoQuotation.jsx b/src/views/products/Detail/ProductInfoQuotation.jsx
index b1decb4..d95358a 100644
--- a/src/views/products/Detail/ProductInfoQuotation.jsx
+++ b/src/views/products/Detail/ProductInfoQuotation.jsx
@@ -1,5 +1,5 @@
import { useState } from 'react'
-import { Table, Form, Modal, Typography, Button, Radio, Input, Flex, Card, InputNumber, Checkbox, DatePicker, Space, App, Tooltip } from 'antd'
+import { Table, Form, Modal, Button, Radio, Input, Flex, Card, InputNumber, Checkbox, DatePicker, Space, App, Tooltip } from 'antd'
import { useTranslation } from 'react-i18next'
import { CloseOutlined, StarTwoTone, PlusOutlined, ExclamationCircleFilled, QuestionCircleOutlined } from '@ant-design/icons'
import { useDatePresets } from '@/hooks/useDatePresets'
@@ -201,15 +201,15 @@ const ProductInfoQuotation = ({ editable, ...props }) => {
}
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
+ // { 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
{ title: t('products:adultPrice'), dataIndex: 'adult_cost', width: '5rem' },
{ title: t('products:childrenPrice'), dataIndex: 'child_cost', width: '5rem' },
{ title: t('products:currency'), dataIndex: 'currency', width: '4rem' },
{
- title: t('products:unit_name'),
+ title: (<>{t('products:unit_name')} >),
dataIndex: 'unit_id',
- width: '4rem',
+ width: '6rem',
render: (text) => t(`products:PriceUnit.${text}`), // (text === '0' ? '每人' : text === '1' ? '每团' : text),
},
{
diff --git a/src/views/products/Print/AgencyContract.jsx b/src/views/products/Print/AgencyContract.jsx
index 96f9ce6..1bdc2d1 100644
--- a/src/views/products/Print/AgencyContract.jsx
+++ b/src/views/products/Print/AgencyContract.jsx
@@ -4,9 +4,7 @@ import {
Header,
Footer,
HeadingLevel,
- Packer,
Paragraph,
- TabStopPosition,
TabStopType,
Table,
TableCell,
@@ -16,7 +14,118 @@ import {
NumberFormat,
PageNumber,
WidthType,
+ LevelFormat,
+ Tab,
+ PositionalTab,
+ PositionalTabAlignment,
+ PositionalTabRelativeTo,
+ PositionalTabLeader,
} from 'docx';
+import dayjs from 'dayjs';
+import { cloneDeep, groupBy, isEmpty, 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 chunkBy = (use_year, dataList = [], by = []) => {
+ const groupSize = dataList.reduce((a, c) => a.concat((c.quotation || []).reduce((aq, cq) => aq.concat([[cq.group_size_min, cq.group_size_max]]), [])), []);
+ const allSizeCut = groupBy(
+ uniqWith(groupSize, (a, b) => a[0] === b[0] && a[1] === b[1]),
+ (c) => c[0]
+ );
+ const sizeSets = Object.keys(allSizeCut).map((s1) => [Number(s1), Math.max(...allSizeCut[s1].map((c) => c[1]))]);
+ const sizeSetsByMax = uniqWith(sizeSets, (a, b) => a[1] === b[1]);
+ // const sizeSetsByMax = [[1,4], [5,6], [7, 9], [10, Infinity]]; // test: 固定车辆座次
+
+ const dataRoll = dataList.map((rowp, ii) => {
+ const quotation = rowp.quotation.map((quoteItem) => {
+ const matchRange = cloneDeep(sizeSetsByMax)
+ .reverse()
+ .find((ss) => quoteItem.group_size_min >= ss[0] && quoteItem.group_size_max <= ss[1]);
+ // const matchMax =
+ // matchRange ||
+ // cloneDeep(sizeSetsByMax)
+ // // .reverse()
+ // .find((ss) => quoteItem.group_size_max <= ss[1]);
+ // const matchMin =
+ // matchMax ||
+ // cloneDeep(sizeSetsByMax)
+ // // .reverse()
+ // .find((ss) => quoteItem.group_size_min >= ss[0]);
+ // const inSet = matchMin;
+ const inSet = matchRange;
+ return {
+ ...quoteItem,
+ quote_season: isFullYearOrLonger(use_year, quoteItem.use_dates_start, quoteItem.use_dates_end) ? 'SS' : 'PS',
+ quote_size: inSet ? inSet.join('-') : '10-Infinity',
+ };
+ });
+ return { ...rowp, quotation };
+ });
+
+ const chunk = dataRoll.map((rowp) => {
+ const quote_chunk = groupBy(rowp.quotation, (quoteItem) => by.map((key) => quoteItem[key]).join('@'));
+ return { ...rowp, quote_chunk };
+ });
+ const dataSource = chunk.map((rowp, ri) => {
+ const thisChunk = Object.keys(rowp.quote_chunk).reduce((qc, ckey) => {
+ const ckeyArr = ckey.split('@');
+ if (isEmpty(qc[ckeyArr[0]])) {
+ qc[ckeyArr[0]] = ckeyArr[1] ? { [ckeyArr[1]]: rowp.quote_chunk[ckey] } : rowp.quote_chunk[ckey];
+ } else {
+ qc[ckeyArr[0]][ckeyArr[1]] = (qc[ckeyArr[0]][ckeyArr[1]] || []).concat(rowp.quote_chunk[ckey]);
+ }
+ return qc;
+ }, {});
+
+ return { ...rowp, quote_chunk: thisChunk };
+ });
+ const allquotation = chunk.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}`));
+ // console.log(chunk, dataSource, sizeSets, sizeSetsByMax);
+ return { chunk, dataSource, sizeSets: sizeSetsByMax, SSRange, PSRange };
+};
+
const DOC_TITLE = '地接合同';
const pageMargins = {
top: `15mm`,
@@ -38,10 +147,18 @@ const tableBorderOne = {
};
export default class AgencyContract {
- create([data]) {
+ 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);
+
const document = new Document({
creator: 'China Highlights',
- title: DOC_TITLE,
+ title: h1,
+ subject: '2025桂林海纳价格合同模板',
+ // description: '',
styles: {
paragraphStyles: [
{
@@ -49,13 +166,31 @@ export default class AgencyContract {
name: 'Normal',
quickFormat: true,
run: {
- size: 20,
+ size: 22,
font: { name: '宋体' },
color: '000000',
},
paragraph: {
spacing: {
- after: 0,
+ 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,
},
},
},
@@ -72,7 +207,8 @@ export default class AgencyContract {
},
paragraph: {
spacing: {
- after: 120,
+ before: 200,
+ after: 200,
},
},
},
@@ -83,17 +219,53 @@ export default class AgencyContract {
next: 'Normal',
quickFormat: true,
run: {
- size: 24,
+ size: 28,
font: { name: '宋体' },
color: '000000',
},
paragraph: {
spacing: {
before: 120,
- // after: 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: [
@@ -109,7 +281,10 @@ export default class AgencyContract {
},
headers: {
default: new Header({
- children: [this.createPageHeaderText(`${new Date().toLocaleString()}系统生成`)],
+ children: [
+ this.createPageHeaderText(`Email: xeniayou@hainatravel.com`, { italics: false }),
+ this.createPageHeaderText(`https://www.chinahighlights.com`, { italics: false }),
+ ],
}),
},
footers: {
@@ -137,17 +312,47 @@ export default class AgencyContract {
size: 20,
}),
],
- })
+ }),
],
}),
},
children: [
- new Paragraph({
- text: DOC_TITLE,
- heading: HeadingLevel.HEADING_1,
- alignment: 'center',
- }),
- this.createSubHeading(`副标题`),
+ // new Paragraph({
+ // text: h1,
+ // heading: HeadingLevel.HEADING_1,
+ // alignment: 'center',
+ // }),
+ 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_J(use_year, agencyProducts['J'])]),
+ ...(isEmpty(agencyProducts['Q'])
+ ? []
+ : [this.createSubHeading(`导游`, { numbering: { reference: 'products-type', level: 0 } }), this.createTable_6(use_year, agencyProducts['Q'])]),
+ // 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_J(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(),
],
},
],
@@ -156,21 +361,584 @@ export default class AgencyContract {
return document;
}
+ /**
+ * 综费
+ */
+ createTable_6(use_year, dataList, style = {}) {
+ const { chunk, SSRange, PSRange } = chunkBy(use_year, dataList, ['quote_season']);
+
+ 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)).reduce((a, c) => a.concat(c), []),
+
+ this.createTable_Memo(3),
+ ],
+ });
+ }
+ /**
+ * 超公里
+ */
+ createTable_B(use_year, dataList) {
+ const { chunk: dataSource, sizeSets, SSRange, PSRange } = chunkBy(use_year, dataList, ['quote_size']);
+ const infoCol = (rowp) =>
+ new TableCell({
+ borders: tableBorderOne,
+ verticalAlign: AlignmentType.CENTER,
+ children: [
+ new Paragraph({
+ text: `${rowp?.info?.km}`,
+ alignment: AlignmentType.CENTER,
+ }),
+ ],
+ });
+
+ return new Table({
+ borders: tableBorderNone,
+ width: {
+ size: 100,
+ type: WidthType.PERCENTAGE,
+ },
+ rows: [
+ new TableRow({
+ children: [
+ this.createTableHeader('超公里项目', 20),
+ this.createTableHeader('往返公里数', 20),
+ this.createTableHeader(['全年', ...SSRange, ...PSRange], 60, { columnSpan: sizeSets.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: '' })] }),
+ ...sizeSets.map((ss) => new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, children: [new Paragraph({ text: ss.join('-') })] })),
+ ],
+ }),
+ ...dataSource.map((row, ri) => this.createDetailRowSize(sizeSets, row, ri, { infoCol })).reduce((a, c) => a.concat(c), []),
+ this.createTable_Memo(2 + sizeSets.length),
+ ],
+ });
+ }
+
+ /**
+ * 车费
+ */
+ createTable_J(use_year, dataList) {
+ const { chunk, dataSource, sizeSets, SSRange, PSRange } = chunkBy(use_year, dataList, ['quote_season', 'quote_size']);
+
+ return new Table({
+ borders: tableBorderNone,
+ width: {
+ size: 100,
+ type: WidthType.PERCENTAGE,
+ },
+ rows: [
+ new TableRow({
+ children: [
+ this.createTableHeader('项目', 20),
+ this.createTableHeader(['平季', ...SSRange], 40, { columnSpan: sizeSets.length }),
+ this.createTableHeader(['旺季', ...PSRange], 40, { columnSpan: sizeSets.length }),
+ ],
+ }),
+ new TableRow({
+ children: [
+ new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, children: [new Paragraph({ text: '' })] }),
+ ...sizeSets.map((ss) => new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, children: [new Paragraph({ text: ss.join('-') })] })),
+ ...sizeSets.map((ss) => new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, children: [new Paragraph({ text: ss.join('-') })] })),
+ ],
+ }),
+ ...dataSource.map((row, ri) => this.createDetailRowSize(sizeSets, row, ri, { withSeason: true })).reduce((a, c) => a.concat(c), []),
+
+ this.createTable_Memo(1 + sizeSets.length * 2),
+ ],
+ });
+ }
+ /**
+ * 导游
+ * @ignore
+ * @deprecated
+ */
+ createTable_Q(use_year, dataList) {}
+ /**
+ * 景点
+ */
+ 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 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}`;
+ return new Paragraph({
+ children: [
+ // new TextRun({ text: `${extra.info.product_title} ${firstQ.unit_name}`, bold: true }),
+ new TextRun({
+ text:
+ `${firstQ.adult_cost}, ${firstQ.group_size_min}-${firstQ.group_size_max}人, ${firstQ.unit_name}` +
+ // + (`, ${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.map((row, ri) => this.createDetailRowSeason2(row, ri, { extraCol })).reduce((a, c) => a.concat(c), []),
+
+ this.createTable_Memo(1 + sizeSets.length * 2 + 1),
+ ],
+ });
+ }
+ /**
+ * 餐费
+ */
+ 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 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: '' })
+ ],
+ });
+
+ return new Table({
+ borders: tableBorderNone,
+ width: {
+ size: 100,
+ type: WidthType.PERCENTAGE,
+ },
+ rows: [
+ new TableRow({
+ children: [
+ this.createTableHeader('社会餐厅午餐/晚餐', 25),
+ this.createTableHeader(['价格', ...SSRange, ...PSRange], 50, { columnSpan: sizeSets.length }),
+ this.createTableHeader('司陪餐补', 25),
+ ],
+ }),
+ new TableRow({
+ children: [
+ new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, children: [new Paragraph({ text: '' })] }),
+ ...sizeSets.map((ss) => new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, children: [new Paragraph({ text: ss.join('-') })] })),
+ // new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, rowSpan: dataList.length+1, children: [new Paragraph({ text: '' })] }),
+ new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, children: [new Paragraph({ text: '' })] }),
+ ],
+ }),
+ ...chunk.map((row, ri) => this.createDetailRowSize(sizeSets, row, ri, { withSize: false, extraCol })).reduce((a, c) => a.concat(c), []),
+ // ...dataSource.map((row, ri) => this.createDetailRowSize(sizeSets, row, ri, true)).reduce((a, c) => a.concat(c), []),
+
+ this.createTable_Memo(1 + sizeSets.length + 1),
+ ],
+ });
+ }
+ /**
+ * 包价线路
+ * @deprecated
+ */
+ createTable_D(use_year, dataList) {
+ const { chunk, dataSource, sizeSets, SSRange, PSRange } = chunkBy(use_year, dataList, ['quote_season', 'quote_size']);
+
+ return new Table({
+ borders: tableBorderNone,
+ width: {
+ size: 100,
+ type: WidthType.PERCENTAGE,
+ },
+ rows: [
+ new TableRow({
+ children: [
+ this.createTableHeader('项目', 30),
+ this.createTableHeader(['平季', ...SSRange], 35, { columnSpan: sizeSets.length }),
+ this.createTableHeader(['旺季', ...PSRange], 35, { columnSpan: sizeSets.length }),
+ ],
+ }),
+ new TableRow({
+ children: [
+ new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, children: [new Paragraph({ text: '' })] }),
+ ...sizeSets.map((ss) => new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, children: [new Paragraph({ text: ss.join('-') })] })),
+ ...sizeSets.map((ss) => new TableCell({ borders: tableBorderOne, verticalAlign: AlignmentType.CENTER, children: [new Paragraph({ text: ss.join('-') })] })),
+ ],
+ }),
+ ...dataSource.map((row, ri) => this.createDetailRowSize(sizeSets, row, ri, { withSeason: true })).reduce((a, c) => a.concat(c), []),
+
+ this.createTable_Memo(1 + sizeSets.length * 2),
+ ],
+ });
+ }
+
+ createDetailRowSize(sizeSets, rowp, ii, { withSeason = false, withSize = true, infoCol = () => {}, extraCol = () => {} } = {}) {
+ const sizeCol = sizeSets.map((ss) => ss.join('-'));
+
+ 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,
+ }),
+ ],
+ }),
+ infoCol(rowp),
+ ...sizeCol.map(
+ (ss) =>
+ new TableCell({
+ borders: tableBorderOne,
+ width: { size: 2000, type: WidthType.DXA },
+ verticalAlign: AlignmentType.CENTER,
+ children: [
+ ...((withSeason ? rowp.quote_chunk?.['SS']?.[ss] : rowp.quote_chunk[ss]) || []).map(
+ (quoteItem, ii) =>
+ new Paragraph({
+ alignment: AlignmentType.LEFT,
+ text: isEmpty(quoteItem)
+ ? ''
+ : withSize
+ ? `${quoteItem.adult_cost}, ${quoteItem.group_size_min}-${quoteItem.group_size_max}人,${quoteItem.unit_name}`
+ : `${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.LEFT,
+ // text: `${quoteItem.adult_cost}, ${quoteItem.group_size_min}-${quoteItem.group_size_max}${quoteItem.unit_name}`,
+ text: withSize
+ ? `${quoteItem.adult_cost}, ${quoteItem.group_size_min}-${quoteItem.group_size_max}人,${quoteItem.unit_name}`
+ : `${quoteItem.adult_cost}`,
+ })
+ ),
+ ],
+ })
+ )
+ : []),
+ extraCol(rowp, ii),
+ ],
+ }),
+ ];
+ }
+ createDetailRowSeason2(rowp, ii, { 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.LEFT,
+ text: `${quoteItem.adult_cost}, ${quoteItem.group_size_min}-${quoteItem.group_size_max}人,${quoteItem.unit_name}`,
+ })
+ ),
+ ],
+ }),
+ 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.LEFT,
+ text: `${quoteItem.adult_cost}, ${quoteItem.group_size_min}-${quoteItem.group_size_max}人,${quoteItem.unit_name}`,
+ })
+ ),
+ ],
+ }),
+ extraCol(rowp, ii),
+ ],
+ }),
+ ];
+ }
+
+ createTable_Memo(columnSpan) {
+ return new TableRow({
+ children: [
+ new TableCell({
+ borders: tableBorderOne,
+ // verticalAlign: AlignmentType.CENTER,
+ columnSpan,
+ children: [new Paragraph(`备注: `), new Paragraph(``)],
+ }),
+ ],
+ });
+ }
+
+ 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,
+ // thematicBreak: true,
+ alignment: AlignmentType.CENTER,
+ });
+ }
+
+ createTitle(text) {
+ return new Paragraph({
+ text: text,
+ heading: HeadingLevel.TITLE,
+ // thematicBreak: true,
+ alignment: AlignmentType.CENTER,
});
}
- createSubHeading(text) {
+ createSubHeading(text, style = {}) {
return new Paragraph({
text: text,
- heading: HeadingLevel.HEADING_2,
+ heading: HeadingLevel.HEADING_1,
+ ...style,
});
}
- createPageHeaderText(text) {
+ createPageHeaderText(text, style = {}) {
return new Paragraph({
alignment: AlignmentType.RIGHT,
children: [
@@ -178,6 +946,7 @@ export default class AgencyContract {
text: text,
italics: true,
size: 20,
+ ...style,
}),
],
});