Merge remote-tracking branch 'remotes/origin/feature/price_manager'
# Conflicts: # src/assets/global.css # src/main.jsx # src/views/App.jsxperf/export-docx
commit
3bbd694dc6
@ -0,0 +1,111 @@
|
||||
{
|
||||
"ProductType": "Product Type",
|
||||
"type": {
|
||||
"Experience": "Experience",
|
||||
"Car": "Transport Services",
|
||||
"Guide": "Guide Services",
|
||||
"Package": "Package Tour",
|
||||
"Attractions": "Attractions",
|
||||
"Meals": "Meals",
|
||||
"Extras": "Extras",
|
||||
"UltraService": "Ultra Service"
|
||||
},
|
||||
"EditComponents": {
|
||||
"info": "Product Information",
|
||||
"Quotation": "Quotation",
|
||||
"Extras": "Add-on"
|
||||
},
|
||||
"auditState": {
|
||||
"New": "New",
|
||||
"Pending": "Pending",
|
||||
"Approved": "Approved",
|
||||
"Rejected": "Rejected",
|
||||
"Published": "Published"
|
||||
},
|
||||
"auditStateAction": {
|
||||
"New": "New",
|
||||
"Pending": "Pending",
|
||||
"Approved": "Approve",
|
||||
"Rejected": "Reject",
|
||||
"Published": "Publish"
|
||||
},
|
||||
"PriceUnit": {
|
||||
"0": "Person",
|
||||
"1": "Group",
|
||||
"title": "Price Unit"
|
||||
},
|
||||
"Status": "Status",
|
||||
"State": "State",
|
||||
|
||||
"Title": "Title",
|
||||
"Vendor": "Vendor",
|
||||
"AuState": "Audit State",
|
||||
"CreatedBy": "Created By",
|
||||
"CreateDate": "Create Date",
|
||||
"AuditedBy": "Audited By",
|
||||
"AuditDate": "Audit Date",
|
||||
"OpenHours": "Open Hours",
|
||||
"Duration": "Duration",
|
||||
"KM": "KM",
|
||||
"RecommendsRate": "RecommendsRate",
|
||||
"OpenWeekdays": "Open Weekdays",
|
||||
"DisplayToC": "DisplayToC",
|
||||
"Dept": "Dept",
|
||||
|
||||
"productProject": "Product project",
|
||||
"Code": "Code",
|
||||
"City": "City",
|
||||
"Remarks": "Remarks",
|
||||
"tourTime": "Tour time",
|
||||
"recommendationRate": "Recommends rate",
|
||||
"Name": "Name",
|
||||
"Description":"Description",
|
||||
"supplierQuotation": "Supplier quotation",
|
||||
"addQuotation": "Add quotation",
|
||||
"bindingProducts": "Binding products",
|
||||
"addBinding": "Add binding",
|
||||
|
||||
"adultPrice": "Adult price",
|
||||
"childrenPrice": "Child price",
|
||||
"currency": "Currency",
|
||||
"Types": "Type",
|
||||
"number": "Number",
|
||||
"validityPeriod":"Validity period",
|
||||
"operation": "Operation",
|
||||
"price": "Price",
|
||||
"weekends":"Weekends",
|
||||
|
||||
"Quotation": "Quotation",
|
||||
"Offer": "Offer",
|
||||
|
||||
"Unit": "Unit",
|
||||
"GroupSize": "Group Size",
|
||||
"UseDates": "Use Dates",
|
||||
|
||||
"Weekdays": "Weekdays",
|
||||
"OnWeekdays": "On Weekdays: ",
|
||||
"Unlimited": "Unlimited",
|
||||
|
||||
"UseYear": "Use Year",
|
||||
|
||||
"AgeType": {
|
||||
"Type": "Age Type",
|
||||
"Adult": "Adult",
|
||||
"Child": "Child"
|
||||
},
|
||||
|
||||
"save":"save",
|
||||
"edit":"edit",
|
||||
"delete":"delete",
|
||||
"cancel":"cancel",
|
||||
"sureCancel":"Sure you want to cancel?",
|
||||
"sureDelete":"Sure you want to delete?",
|
||||
|
||||
"CopyFormMsg": {
|
||||
"requiredVendor": "Please pick a target vendor",
|
||||
"requiredTypes": "Please select product types",
|
||||
"requiredDept": "Please pick a owner department"
|
||||
},
|
||||
|
||||
"#": "#"
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
{
|
||||
"ProductType": "项目类型",
|
||||
"type": {
|
||||
"Experience": "综费",
|
||||
"Car": "车费",
|
||||
"Guide": "导游",
|
||||
"Package": "包价线路",
|
||||
"Attractions": "景点",
|
||||
"Meals": "餐费",
|
||||
"Extras": "附加项目",
|
||||
"UltraService": "超公里"
|
||||
},
|
||||
"EditComponents": {
|
||||
"info": "产品信息",
|
||||
"Quotation": "报价",
|
||||
"Extras": "附加项目"
|
||||
},
|
||||
"auditState": {
|
||||
"New": "新增",
|
||||
"Pending": "待审核",
|
||||
"Approved": "已通过",
|
||||
"Rejected": "已拒绝",
|
||||
"Published": "已发布"
|
||||
},
|
||||
"auditStateAction": {
|
||||
"New": "新增",
|
||||
"Pending": "待审核",
|
||||
"Approved": "审核通过",
|
||||
"Rejected": "审核拒绝",
|
||||
"Published": "审核发布"
|
||||
},
|
||||
"PriceUnit": {
|
||||
"0": "每人",
|
||||
"1": "每团",
|
||||
"title": "报价单位"
|
||||
},
|
||||
"Status": "状态",
|
||||
"State": "状态",
|
||||
|
||||
"Title": "名称",
|
||||
"Vendor": "供应商",
|
||||
"AuState": "审核状态",
|
||||
"CreatedBy": "提交人员",
|
||||
"CreateDate": "提交时间",
|
||||
"AuditedBy": "审核人员",
|
||||
"AuditDate": "审核时间",
|
||||
|
||||
"OpenHours": "游览时间",
|
||||
"Duration": "游览时长",
|
||||
"KM": "公里数",
|
||||
"RecommendsRate": "推荐指数",
|
||||
"OpenWeekdays": "周开放日",
|
||||
"DisplayToC": "报价信显示",
|
||||
"Dept": "小组",
|
||||
|
||||
|
||||
"productProject": "产品项目",
|
||||
"Code": "简码",
|
||||
"City": "城市",
|
||||
"Remarks": "备注",
|
||||
"tourTime": "游览时间",
|
||||
"recommendationRate": "推荐指数",
|
||||
"Name":"名称",
|
||||
"Price":"价格",
|
||||
"Description":"描述",
|
||||
"supplierQuotation": "供应商报价",
|
||||
"addQuotation": "添加报价",
|
||||
"bindingProducts": "绑定产品",
|
||||
"addBinding": "添加绑定",
|
||||
|
||||
"adultPrice": "成人价",
|
||||
"childrenPrice": "儿童价",
|
||||
"currency": "币种",
|
||||
"Types": "类型",
|
||||
"number": "人等",
|
||||
"validityPeriod":"有效期",
|
||||
"operation": "操作",
|
||||
"price": "价格",
|
||||
|
||||
"Quotation": "报价",
|
||||
"Offer": "报价",
|
||||
"Unit": "单位",
|
||||
"GroupSize": "人等",
|
||||
"UseDates": "使用日期",
|
||||
|
||||
"Weekdays": "周末",
|
||||
"OnWeekdays": "周: ",
|
||||
"Unlimited": "不限",
|
||||
|
||||
"UseYear": "年份",
|
||||
|
||||
"AgeType": {
|
||||
"Type": "人群",
|
||||
"Adult": "成人",
|
||||
"Child": "儿童"
|
||||
},
|
||||
|
||||
"save":"保存",
|
||||
"edit":"编辑",
|
||||
"delete":"删除",
|
||||
"cancel":"取消",
|
||||
"sureCancel": "确定取消?",
|
||||
"sureDelete":"确定删除?",
|
||||
|
||||
"CopyFormMsg": {
|
||||
"requiredVendor": "请选择目标供应商",
|
||||
"requiredTypes": "请选择产品类型",
|
||||
"requiredDept": "请选择所属小组"
|
||||
},
|
||||
|
||||
"#": "#"
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
.ant-table-wrapper.border-collapse table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { Select } from 'antd';
|
||||
import { useProductsAuditStates } from '@/hooks/useProductsSets';
|
||||
|
||||
const AuditStateSelector = ({ ...props }) => {
|
||||
const states = useProductsAuditStates();
|
||||
return (
|
||||
<>
|
||||
<Select labelInValue allowClear options={states} {...props}/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default AuditStateSelector;
|
@ -0,0 +1,243 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Table, Input, Button, DatePicker, Row, Col, Tag, Select } from 'antd';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Option } = Select;
|
||||
const BatchImportPrice = () => {
|
||||
const [startDate, setStartDate] = useState(null);
|
||||
const [endDate, setEndDate] = useState(null);
|
||||
const [startPeople, setStartPeople] = useState(1);
|
||||
const [endPeople, setEndPeople] = useState(5);
|
||||
const [dateRanges, setDateRanges] = useState([]);
|
||||
const [peopleRanges, setPeopleRanges] = useState([]);
|
||||
const [tableData, setTableData] = useState([]);
|
||||
const [currency, setCurrency] = useState('RMB'); // 默认值为 RMB
|
||||
const [type, setType] = useState('每人'); // 默认值为 每人
|
||||
|
||||
const handleGenerateTable = () => {
|
||||
if (dateRanges.length === 0 || peopleRanges.length === 0) return;
|
||||
|
||||
const newData = dateRanges.flatMap(dateRange => {
|
||||
const { startDate, endDate } = dateRange;
|
||||
const dates = generateDateRange(startDate, endDate);
|
||||
const row = { dateRange: `${startDate.format('YYYY-MM-DD')} ~ ${endDate.format('YYYY-MM-DD')}` };
|
||||
|
||||
peopleRanges.forEach(peopleRangeString => {
|
||||
const [start, end] = peopleRangeString.split('-').map(Number);
|
||||
generatePeopleRange(start, end).forEach(person => {
|
||||
row[`${person}_adultPrice`] = '';
|
||||
row[`${person}_childPrice`] = '';
|
||||
});
|
||||
});
|
||||
|
||||
dates.forEach(date => {
|
||||
row[date] = '';
|
||||
});
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
setTableData(newData);
|
||||
};
|
||||
|
||||
const generateDateRange = (start, end) => {
|
||||
const dates = [];
|
||||
let currentDate = start.clone();
|
||||
|
||||
while (currentDate <= end) {
|
||||
dates.push(currentDate.format('YYYY-MM-DD'));
|
||||
currentDate = currentDate.add(1, 'day');
|
||||
}
|
||||
|
||||
return dates;
|
||||
};
|
||||
|
||||
const generatePeopleRange = (start, end) => {
|
||||
const range = [];
|
||||
for (let i = start; i <= end; i++) {
|
||||
range.push(`人等${i}`);
|
||||
}
|
||||
return range;
|
||||
};
|
||||
|
||||
const handleCellChange = (value, dateRange, peopleRange, type) => {
|
||||
const newData = [...tableData];
|
||||
const rowIndex = newData.findIndex(row => row.dateRange === dateRange);
|
||||
newData[rowIndex][`${peopleRange}_${type}`] = value;
|
||||
setTableData(newData);
|
||||
};
|
||||
|
||||
const handleAddDateRange = () => {
|
||||
if (startDate && endDate) {
|
||||
const newDateRange = { startDate, endDate };
|
||||
// 检查是否已经存在相同的日期范围
|
||||
const isDateRangeExist = dateRanges.some(range => (
|
||||
range.startDate.isSame(startDate, 'day') && range.endDate.isSame(endDate, 'day')
|
||||
));
|
||||
if (!isDateRangeExist) {
|
||||
setDateRanges([...dateRanges, newDateRange]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddPeopleRange = () => {
|
||||
if (startPeople <= endPeople) {
|
||||
const newPeopleRange = `${startPeople}-${endPeople}`;
|
||||
// 检查是否已经存在相同的人员范围
|
||||
const isPeopleRangeExist = peopleRanges.includes(newPeopleRange);
|
||||
if (!isPeopleRangeExist) {
|
||||
setPeopleRanges([...peopleRanges, newPeopleRange]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = (index, type) => {
|
||||
if (type === 'date') {
|
||||
setDateRanges(dateRanges.filter((_, i) => i !== index));
|
||||
} else {
|
||||
const removedPeopleRange = peopleRanges[index];
|
||||
setPeopleRanges(peopleRanges.filter(range => range !== removedPeopleRange));
|
||||
}
|
||||
setTableData([]);
|
||||
};
|
||||
|
||||
|
||||
const [adultPrice, setAdultPrice] = useState('');
|
||||
const [childPrice, setChildPrice] = useState('');
|
||||
|
||||
const handleAdultPriceChange = (value, dateRange) => {
|
||||
const newData = [...tableData];
|
||||
const rowIndex = newData.findIndex(row => row.dateRange === dateRange);
|
||||
newData[rowIndex]['成人价'] = value;
|
||||
setTableData(newData);
|
||||
};
|
||||
|
||||
const handleChildPriceChange = (value, dateRange) => {
|
||||
const newData = [...tableData];
|
||||
const rowIndex = newData.findIndex(row => row.dateRange === dateRange);
|
||||
newData[rowIndex]['儿童价'] = value;
|
||||
setTableData(newData);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '日期\\人等',
|
||||
dataIndex: 'dateRange',
|
||||
key: 'dateRange',
|
||||
},
|
||||
...peopleRanges.flatMap(peopleRange => ([
|
||||
{
|
||||
title: peopleRange,
|
||||
dataIndex: `${peopleRange}_price`,
|
||||
key: `${peopleRange}_price`,
|
||||
render: (text, record) => (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Input
|
||||
value={record[`${peopleRange}_adultPrice`]}
|
||||
onChange={(e) => handleCellChange(e.target.value, record.dateRange, peopleRange, 'adultPrice')}
|
||||
placeholder="成人价"
|
||||
style={{ width: '45%' }}
|
||||
suffix={`${currency}/${type}`}
|
||||
/>
|
||||
<Input
|
||||
value={record[`${peopleRange}_childPrice`]}
|
||||
onChange={(e) => handleCellChange(e.target.value, record.dateRange, peopleRange, 'childPrice')}
|
||||
placeholder="儿童价"
|
||||
style={{ width: '45%' }}
|
||||
suffix={`${currency}/${type}`}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
])),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Row>
|
||||
<Col span={4}>
|
||||
<Select value={currency} onChange={value => setCurrency(value)} style={{ width: '100%', marginTop: 10 }}>
|
||||
<Option value="RMB">RMB</Option>
|
||||
<Option value="MY">MY</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Select value={type} onChange={value => setType(value)} style={{ width: '100%', marginTop: 10 }}>
|
||||
<Option value="每人">每人</Option>
|
||||
<Option value="美团">美团</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<RangePicker
|
||||
onChange={(dates) => {
|
||||
if (dates && dates.length === 2) {
|
||||
setStartDate(dates[0]);
|
||||
setEndDate(dates[1]);
|
||||
} else {
|
||||
setStartDate(null);
|
||||
setEndDate(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Button onClick={handleAddDateRange} type="primary">记录有效期</Button>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Input.Group compact style={{ marginTop: 10 }}>
|
||||
<Input
|
||||
style={{ width: 100, textAlign: 'center' }}
|
||||
placeholder="Start"
|
||||
value={startPeople}
|
||||
onChange={(e) => setStartPeople(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
<Input
|
||||
style={{ width: 30, borderLeft: 0, pointerEvents: 'none', backgroundColor: '#fff' }}
|
||||
placeholder="~"
|
||||
disabled
|
||||
/>
|
||||
<Input
|
||||
style={{ width: 100, textAlign: 'center', borderLeft: 0 }}
|
||||
placeholder="End"
|
||||
value={endPeople}
|
||||
onChange={(e) => setEndPeople(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
</Input.Group>
|
||||
</Col>
|
||||
<Button onClick={handleAddPeopleRange} type="primary" style={{ marginTop: 10 }}>记录人等</Button>
|
||||
</Row>
|
||||
<Button onClick={handleGenerateTable} type="primary" style={{ marginTop: 10 }}>生成表格</Button>
|
||||
|
||||
|
||||
<Row gutter={16} style={{ marginTop: '16px' }}>
|
||||
<Col span={24}>
|
||||
{dateRanges.map((dateRange, index) => (
|
||||
<Tag key={index} closable onClose={() => handleRemoveTag(index, 'date')}>
|
||||
{`${dateRange.startDate.format('YYYY-MM-DD')} ~ ${dateRange.endDate.format('YYYY-MM-DD')}`}
|
||||
</Tag>
|
||||
))}
|
||||
{peopleRanges.map((peopleRange, index) => (
|
||||
<Tag key={index} closable onClose={() => handleRemoveTag(index, 'people')}>
|
||||
{peopleRange}
|
||||
</Tag>
|
||||
))}
|
||||
</Col>
|
||||
</Row>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
bordered
|
||||
pagination={false}
|
||||
style={{ marginTop: '16px' }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BatchImportPrice;
|
||||
|
||||
|
||||
|
@ -0,0 +1,61 @@
|
||||
import { Component } from 'react';
|
||||
import { Select } from 'antd';
|
||||
// import { groups, leafGroup } from '../../libs/ht';
|
||||
|
||||
/**
|
||||
* 小组
|
||||
*/
|
||||
export const groups = [
|
||||
{ value: '1,2,28,7,33', key: '1,2,28,7,33', label: 'GH事业部', code: 'GH', children: [1, 2, 28, 7, 33] },
|
||||
{ value: '8,9,11,12,20,21', key: '8,9,11,12,20,21', label: '国际事业部', code: 'INT', children: [8, 9, 11, 12, 20, 21] },
|
||||
{ value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: '', children: [10, 18, 16, 30] },
|
||||
{ value: '1', key: '1', label: 'CH直销', code: '', children: [] },
|
||||
{ value: '2', key: '2', label: 'CH大客户', code: '', children: [] },
|
||||
{ value: '28', key: '28', label: 'AH亚洲项目组', code: 'AH', children: [] },
|
||||
{ value: '33', key: '33', label: 'GH项目组', code: '', children: [] },
|
||||
{ value: '7', key: '7', label: '市场推广', code: '', children: [] },
|
||||
{ value: '8', key: '8', label: '德语', code: '', children: [] },
|
||||
{ value: '9', key: '9', label: '日语', code: '', children: [] },
|
||||
{ value: '11', key: '11', label: '法语', code: '', children: [] },
|
||||
{ value: '12', key: '12', label: '西语', code: '', children: [] },
|
||||
{ value: '20', key: '20', label: '俄语', code: '', children: [] },
|
||||
{ value: '21', key: '21', label: '意语', code: '', children: [] },
|
||||
{ value: '10', key: '10', label: '商旅', code: '', children: [] },
|
||||
{ value: '18', key: '18', label: 'CT', code: 'CT', children: [] },
|
||||
{ value: '16', key: '16', label: 'APP', code: 'APP', children: [] },
|
||||
{ value: '30', key: '30', label: 'Trippest', code: 'TP', children: [] },
|
||||
{ value: '31', key: '31', label: '花梨鹰', code: '', children: [] },
|
||||
];
|
||||
export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.code || c.key)]: c }), {});
|
||||
export const groupsMappedByKey = groups.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {});
|
||||
export const leafGroup = groups.slice(3);
|
||||
export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest
|
||||
|
||||
export const DeptSelector = ({show_all, isLeaf,...props}) => {
|
||||
const _show_all = ['tags', 'multiple'].includes(props.mode) ? false : show_all;
|
||||
const options = isLeaf===true ? leafGroup : groups;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
mode={props.mode}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="选择小组"
|
||||
labelInValue
|
||||
maxTagCount={1}
|
||||
allowClear={props.mode != null}
|
||||
{...props}
|
||||
options={options}
|
||||
/>
|
||||
{/* {_show_all ? (
|
||||
<Select.Option key="ALL" value="ALL">
|
||||
所有小组
|
||||
</Select.Option>
|
||||
) : (
|
||||
''
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
export default DeptSelector;
|
@ -0,0 +1,49 @@
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { Select, Spin } from 'antd';
|
||||
import { debounce, objectMapper } from '@/utils/commons';
|
||||
|
||||
function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const [options, setOptions] = useState([]);
|
||||
const fetchRef = useRef(0);
|
||||
const debounceFetcher = useMemo(() => {
|
||||
const loadOptions = (value) => {
|
||||
fetchRef.current += 1;
|
||||
const fetchId = fetchRef.current;
|
||||
setOptions([]);
|
||||
setFetching(true);
|
||||
fetchOptions(value).then((newOptions) => {
|
||||
const mapperOptions = newOptions.map(ele => objectMapper(ele, props.map));
|
||||
if (fetchId !== fetchRef.current) {
|
||||
// for fetch callback order
|
||||
return;
|
||||
}
|
||||
setOptions(mapperOptions);
|
||||
setFetching(false);
|
||||
});
|
||||
};
|
||||
return debounce(loadOptions, debounceTimeout);
|
||||
}, [fetchOptions, debounceTimeout]);
|
||||
return (
|
||||
<Select
|
||||
labelInValue
|
||||
filterOption={false}
|
||||
showSearch
|
||||
allowClear
|
||||
maxTagCount={1}
|
||||
dropdownStyle={{width: '20rem'}}
|
||||
{...props}
|
||||
onSearch={debounceFetcher}
|
||||
notFoundContent={fetching ? <Spin size='small' /> : null}
|
||||
optionFilterProp='label'
|
||||
>
|
||||
{options.map((d) => (
|
||||
<Select.Option key={d.value} title={d.label}>
|
||||
{d.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export default DebounceSelect;
|
@ -0,0 +1,31 @@
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import { Layout, Flex, theme, Spin, Divider } from 'antd';
|
||||
import BackBtn from './BackBtn';
|
||||
|
||||
const { Content, Header } = Layout;
|
||||
const HeaderWrapper = ({ children, header, loading, ...props }) => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
token: { colorBgContainer },
|
||||
} = theme.useToken();
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={loading || false}>
|
||||
<Layout className=' bg-white'>
|
||||
<Header className='header px-6 h-10 ' style={{ background: 'white' }}>
|
||||
<Flex justify={'space-between'} align={'center'} className='h-full'>
|
||||
{/* {header} */}
|
||||
<div className='grow h-full'>{header}</div>
|
||||
<BackBtn />
|
||||
</Flex>
|
||||
</Header>
|
||||
<Divider className='my-2' />
|
||||
<Content className='' style={{ backgroundColor: colorBgContainer }}>
|
||||
{children || <Outlet />}
|
||||
</Content>
|
||||
</Layout>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default HeaderWrapper;
|
@ -0,0 +1,51 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DatePicker, Button } from 'antd';
|
||||
|
||||
const Date = ({ onDateChange }) => {
|
||||
const dateFormat = 'YYYY/MM/DD';
|
||||
const { RangePicker } = DatePicker;
|
||||
const [dateRange, setDateRange] = useState(null);
|
||||
const [selectedDays, setSelectedDays] = useState([]);
|
||||
|
||||
const days = [
|
||||
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
|
||||
];
|
||||
|
||||
const handleChange = (date, dateString) => {
|
||||
const range = dateString[0] + "-" + dateString[1];
|
||||
setDateRange(range);
|
||||
onDateChange({ dateRange: range, selectedDays });
|
||||
};
|
||||
|
||||
const handleDayClick = (day) => {
|
||||
setSelectedDays((prevSelectedDays) => {
|
||||
const updatedDays = prevSelectedDays.includes(day)
|
||||
? prevSelectedDays.filter((d) => d !== day)
|
||||
: [...prevSelectedDays, day];
|
||||
onDateChange({ dateRange, selectedDays: updatedDays });
|
||||
return updatedDays;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>Data</h4>
|
||||
<RangePicker format={dateFormat} onChange={handleChange} />
|
||||
<h4>Weekdays</h4>
|
||||
<div>
|
||||
{days.map((day, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
type={selectedDays.includes(day) ? 'primary' : 'default'}
|
||||
onClick={() => handleDayClick(day)}
|
||||
style={{ margin: '5px' }}
|
||||
>
|
||||
{day}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Date;
|
@ -0,0 +1,14 @@
|
||||
export const useHTLanguageSets = () => {
|
||||
const newData = [
|
||||
{ key: '1', value: '1', label: 'English' },
|
||||
{ key: '2', value: '2', label: 'Chinese (中文)' },
|
||||
{ key: '3', value: '3', label: 'Japanese (日本語)' },
|
||||
{ key: '4', value: '4', label: 'German (Deutsch)' },
|
||||
{ key: '5', value: '5', label: 'French (Français)' },
|
||||
{ key: '6', value: '6', label: 'Spanish (Español)' },
|
||||
{ key: '7', value: '7', label: 'Russian (Русский)' },
|
||||
{ key: '8', value: '8', label: 'Italian (Italiano)' },
|
||||
];
|
||||
|
||||
return newData;
|
||||
};
|
@ -0,0 +1,122 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useAuthStore from '@/stores/Auth';
|
||||
import { PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config';
|
||||
|
||||
/**
|
||||
* 产品管理 相关的预设数据
|
||||
* 项目类型
|
||||
* * 酒店预定 1
|
||||
* * 火车 2
|
||||
* * 飞机票务 3
|
||||
* * 游船 4
|
||||
* * 快巴 5
|
||||
* * 旅行社(综费) 6
|
||||
* * 景点 7
|
||||
* * 特殊项目 8
|
||||
* * 其他 9
|
||||
* * 酒店 A
|
||||
* * 超公里 B
|
||||
* * 餐费 C
|
||||
* * 小包价 D // 包价线路
|
||||
* * 站 X
|
||||
* * 购物 S
|
||||
* * 餐 R (餐厅)
|
||||
* * 娱乐 E
|
||||
* * 精华线路 T
|
||||
* * 客人testimonial F
|
||||
* * 线路订单 O
|
||||
* * 省 P
|
||||
* * 信息 I
|
||||
* * 国家 G
|
||||
* * 城市 K
|
||||
* * 图片 H
|
||||
* * 地图 M
|
||||
* * 包价线路 L (已废弃)
|
||||
* * 节日节庆 V
|
||||
* * 火车站 N
|
||||
* * 手机租赁 Z
|
||||
* * ---- webht 类型, 20240624 新增HT类型 ----
|
||||
* * 导游 Q
|
||||
* * 车费 J
|
||||
*/
|
||||
|
||||
export const useProductsTypes = (showAll = false) => {
|
||||
const [types, setTypes] = useState([]);
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const allItem = [{ label: t('All'), value: '', key: '' }];
|
||||
const newData = [
|
||||
{ label: t('products:type.Experience'), value: '6', key: '6' },
|
||||
{ label: t('products:type.UltraService'), value: 'B', key: 'B' },
|
||||
{ label: t('products:type.Car'), value: 'J', key: 'J' },
|
||||
{ label: t('products:type.Guide'), value: 'Q', key: 'Q' },
|
||||
{ label: t('products:type.Attractions'), value: '7', key: '7' },
|
||||
{ label: t('products:type.Meals'), value: 'R', key: 'R' },
|
||||
{ label: t('products:type.Extras'), value: '8', key: '8' },
|
||||
{ label: t('products:type.Package'), value: 'D', key: 'D' },
|
||||
];
|
||||
const res = showAll ? [...allItem, ...newData] : newData;
|
||||
setTypes(res);
|
||||
}, [i18n.language]);
|
||||
|
||||
return types;
|
||||
};
|
||||
export const useProductsTypesMapVal = (value) => {
|
||||
const stateSets = useProductsTypes();
|
||||
const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {});
|
||||
return stateMapVal;
|
||||
};
|
||||
|
||||
export const useProductsAuditStates = () => {
|
||||
const [types, setTypes] = useState([]);
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const newData = [
|
||||
{ key: '-1', value: '-1', label: t('products:auditState.New'), color: 'gray-500' },
|
||||
{ key: '0', value: '0', label: t('products:auditState.Pending'), color: '' },
|
||||
{ key: '2', value: '2', label: t('products:auditState.Approved'), color: 'primary' },
|
||||
{ key: '3', value: '3', label: t('products:auditState.Rejected'), color: 'red-500' },
|
||||
{ key: '1', value: '1', label: t('products:auditState.Published'), color: 'primary' },
|
||||
// ELSE 未知
|
||||
];
|
||||
setTypes(newData);
|
||||
}, [i18n.language]);
|
||||
|
||||
return types;
|
||||
};
|
||||
|
||||
export const useProductsAuditStatesMapVal = (value) => {
|
||||
const stateSets = useProductsAuditStates();
|
||||
const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {});
|
||||
return stateMapVal;
|
||||
};
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export const useProductsTypesFieldsets = (type) => {
|
||||
const [isPermitted] = useAuthStore((state) => [state.isPermitted]);
|
||||
const infoDefault = [['code'], ['title']];
|
||||
const infoAdmin = ['remarks', 'dept', 'display_to_c'];
|
||||
const infoTypesMap = {
|
||||
'6': [[],[]],
|
||||
'B': [['city_id', 'km'], []],
|
||||
'J': [['city_id', 'recommends_rate', 'duration', 'display_to_c'], ['description',]],
|
||||
'Q': [['city_id', 'duration', ], ['description',]],
|
||||
'D': [['city_id', 'recommends_rate','duration',], ['description',]],
|
||||
'7': [['city_id', 'recommends_rate', 'duration', 'display_to_c', 'open_weekdays'], ['description',]], // todo: 怎么是2个图
|
||||
'R': [['city_id',], ['description',]],
|
||||
'8': [[],[]], // todo: ?
|
||||
};
|
||||
const thisTypeFieldset = (_type) => {
|
||||
const adminSet = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? infoAdmin : [];
|
||||
return [
|
||||
[...infoDefault[0], ...infoTypesMap[_type][0], ...adminSet],
|
||||
[...infoDefault[1], ...infoTypesMap[_type][1]]
|
||||
];
|
||||
};
|
||||
return thisTypeFieldset(type);
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
import { fetchJSON, postForm, postJSON } from '@/utils/request';
|
||||
import { HT_HOST } from '@/config';
|
||||
import { groupBy } from '@/utils/commons';
|
||||
|
||||
export const searchAgencyAction = async (param) => {
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_search`, param);
|
||||
return errcode !== 0 ? [] : result;
|
||||
};
|
||||
|
||||
export const copyAgencyDataAction = async (postbody) => {
|
||||
const formData = new FormData();
|
||||
Object.keys(postbody).forEach((key) => {
|
||||
formData.append(key, postbody[key]);
|
||||
});
|
||||
const { errcode, result } = await postForm(`${HT_HOST}/agency/products/copy`, formData);
|
||||
return errcode === 0 ? true : false;
|
||||
};
|
||||
|
||||
export const getAgencyProductsAction = async (param) => {
|
||||
const _param = { ...param, use_year: (param.use_year || '').replace('all', ''), audit_state: (param.audit_state || '').replace('all', '') };
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/travel_agency_products`, _param);
|
||||
return errcode !== 0 ? { agency: {}, products: [] } : result;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const addProductExtraAction = async (body) => {
|
||||
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_add`, body);
|
||||
return errcode === 0 ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const delProductExtrasAction = async (body) => {
|
||||
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_del`, body);
|
||||
return errcode === 0 ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定产品的附加项目
|
||||
* @param {object} param { id, travel_agency_id, use_year }
|
||||
*/
|
||||
export const getAgencyProductExtrasAction = async (param) => {
|
||||
const _param = { ...param, use_year: (param.use_year || '').replace('all', '') };
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras`, _param);
|
||||
return errcode !== 0 ? [] : result;
|
||||
};
|
||||
|
||||
export const postProductsQuoteAuditAction = async (auditState, quoteRow) => {
|
||||
const postbody = {
|
||||
audit_state: auditState,
|
||||
id: quoteRow.id,
|
||||
travel_agency_id: quoteRow.travel_agency_id,
|
||||
};
|
||||
const formData = new FormData();
|
||||
Object.keys(postbody).forEach((key) => {
|
||||
formData.append(key, postbody[key]);
|
||||
});
|
||||
const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/quotation_audit`, formData);
|
||||
return json;
|
||||
// return errcode !== 0 ? {} : result;
|
||||
};
|
||||
|
||||
export const postProductsAuditAction = async (auditState, infoRow) => {
|
||||
const postbody = {
|
||||
audit_state: auditState,
|
||||
id: infoRow.id,
|
||||
travel_agency_id: infoRow.travel_agency_id,
|
||||
};
|
||||
const formData = new FormData();
|
||||
Object.keys(postbody).forEach((key) => {
|
||||
formData.append(key, postbody[key]);
|
||||
});
|
||||
const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/travel-agency-products-audit`, formData);
|
||||
return json;
|
||||
// const { errcode, result } = json;
|
||||
// return errcode !== 0 ? {} : result;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
loading: false,
|
||||
searchValues: {},
|
||||
agencyList: [],
|
||||
activeAgency: {},
|
||||
agencyProducts: {},
|
||||
};
|
||||
export const useProductsStore = create(
|
||||
devtools((set, get) => ({
|
||||
// 初始化状态
|
||||
...initialState,
|
||||
|
||||
// state actions
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setSearchValues: (searchValues) => set({ searchValues }),
|
||||
setAgencyList: (agencyList) => set({ agencyList }),
|
||||
setActiveAgency: (activeAgency) => set({ activeAgency }),
|
||||
setAgencyProducts: (agencyProducts) => set({ agencyProducts }),
|
||||
|
||||
reset: () => set(initialState),
|
||||
|
||||
// side effects
|
||||
searchAgency: async (param) => {
|
||||
const { setLoading, setAgencyList } = get();
|
||||
setLoading(true);
|
||||
const res = await searchAgencyAction(param);
|
||||
setAgencyList(res);
|
||||
setLoading(false);
|
||||
},
|
||||
|
||||
getAgencyProducts: async (param) => {
|
||||
const { setLoading, setActiveAgency, setAgencyProducts } = get();
|
||||
setLoading(true);
|
||||
const res = await getAgencyProductsAction(param);
|
||||
const productsData = groupBy(res.products, (row) => row.info.product_type_id);
|
||||
setAgencyProducts(productsData);
|
||||
setActiveAgency(res.agency);
|
||||
setLoading(false);
|
||||
},
|
||||
|
||||
getAgencyProductExtras: async (param) => {
|
||||
const res = await getAgencyProductExtrasAction(param);
|
||||
// todo:
|
||||
},
|
||||
}))
|
||||
);
|
||||
export default useProductsStore;
|
@ -0,0 +1,216 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { App, Button, Collapse, Table, Space, Divider } from 'antd';
|
||||
import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
|
||||
import SecondHeaderWrapper from '@/components/SecondHeaderWrapper';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useProductsStore, { postProductsQuoteAuditAction, } from '@/stores/Products/Index';
|
||||
import { isEmpty } from '@/utils/commons';
|
||||
// import PrintContractPDF from './PrintContractPDF';
|
||||
|
||||
const Header = ({ title, agency, refresh, ...props }) => {
|
||||
const { travel_agency_id, use_year, audit_state } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const [activeAgency] = useProductsStore((state) => [state.activeAgency]);
|
||||
const { message, notification } = App.useApp();
|
||||
const handleAuditItem = (state, row) => {
|
||||
postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id })
|
||||
.then((json) => {
|
||||
if (json.errcode === 0) {
|
||||
message.success(json.errmsg);
|
||||
if (typeof refresh === 'function') {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((ex) => {
|
||||
notification.error({
|
||||
message: 'Notification',
|
||||
description: ex.message,
|
||||
placement: 'top',
|
||||
duration: 4,
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className='flex justify-end items-center gap-4 h-full'>
|
||||
<div className='grow'>
|
||||
<h2 className='m-0 leading-tight'>{title}<Divider type={'vertical'} />{(use_year || '').replace('all', '')}</h2>
|
||||
</div>
|
||||
{/* <Button size='small'>{t('Copy')}</Button> */}
|
||||
{/* <Button size='small'>{t('Import')}</Button> */}
|
||||
<Link className='px-2' to={`/products/${travel_agency_id}/${use_year}/${audit_state}/edit`}>{t('Edit')}</Link>
|
||||
<Button size='small' type={'primary'} onClick={() => handleAuditItem('2', agency)}>
|
||||
{t('products:auditStateAction.Published')}
|
||||
</Button>
|
||||
{/* <Button size='small' type={'primary'} ghost onClick={() => handleAuditItem('2', agency)}>
|
||||
{t('products:auditStateAction.Approved')}
|
||||
</Button> */}
|
||||
<Button size='small' type={'primary'} danger ghost onClick={() => handleAuditItem('3', agency)}>
|
||||
{t('products:auditStateAction.Rejected')}
|
||||
</Button>
|
||||
{/* todo: export, 审核完成之后才能导出 */}
|
||||
<Button size='small'>{t('Print')} PDF</Button>
|
||||
{/* <PrintContractPDF /> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PriceTable = ({ productType, dataSource, refresh }) => {
|
||||
const { t } = useTranslation('products');
|
||||
const [loading, activeAgency] = useProductsStore((state) => [state.loading, state.activeAgency]);
|
||||
const { message, notification } = App.useApp();
|
||||
const stateMapVal = useProductsAuditStatesMapVal();
|
||||
|
||||
// console.log(dataSource);
|
||||
|
||||
const handleAuditPriceItem = (state, row) => {
|
||||
postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id })
|
||||
.then((json) => {
|
||||
if (json.errcode === 0) {
|
||||
message.success(json.errmsg);
|
||||
if (typeof refresh === 'function') {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((ex) => {
|
||||
notification.error({
|
||||
message: 'Notification',
|
||||
description: ex.message,
|
||||
placement: 'top',
|
||||
duration: 4,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const rowStyle = (r, tri) => {
|
||||
const trCls = tri%2 !== 0 ? ' bg-stone-50' : '';
|
||||
const [infoI, quoteI] = r.rowSpanI;
|
||||
const bigTrCls = quoteI === 0 && tri !== 0 ? 'border-collapse border-double border-0 border-t-4 border-stone-300' : '';
|
||||
return [trCls, bigTrCls].join(' ');
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ key: 'title', dataIndex: ['info', 'title'], width: '16rem', title: t('Title'), onCell: (r, index) => ({ rowSpan: r.rowSpan, }), className: 'bg-white', render: (text, r) => text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || '' },
|
||||
...(productType === 'B' ? [{ key: 'km', dataIndex: ['info', 'km'], title: t('KM')}] : []),
|
||||
{ key: 'adult', title: t('AgeType.Adult'), render: (_, { adult_cost, currency, unit_id, unit_name }) => `${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
|
||||
{ key: 'child', title: t('AgeType.Child'), render: (_, { child_cost, currency, unit_id, unit_name }) => `${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
|
||||
// {key: 'unit', title: t('Unit'), },
|
||||
{
|
||||
key: 'groupSize',
|
||||
dataIndex: ['group_size_min'],
|
||||
title: t('GroupSize'),
|
||||
render: (_, { group_size_min, group_size_max }) => `${group_size_min} - ${group_size_max}`,
|
||||
},
|
||||
{
|
||||
key: 'useDates',
|
||||
dataIndex: ['use_dates_start'],
|
||||
title: t('UseDates'),
|
||||
render: (_, { use_dates_start, use_dates_end, weekdays }) => `${use_dates_start} ~ ${use_dates_end}`, // + (weekdays ? `, ${t('OnWeekdays')}${weekdays}` : ''),
|
||||
},
|
||||
{ key: 'weekdays', dataIndex: ['weekdays'], title: t('Weekdays'), render: (text, r) => text || t('Unlimited') },
|
||||
{
|
||||
key: 'state',
|
||||
title: t('State'),
|
||||
render: (_, r) => {
|
||||
return <span className={`text-${stateMapVal[`${r.audit_state_id}`]?.color}`}>{stateMapVal[`${r.audit_state_id}`]?.label}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '价格审核',
|
||||
key: 'action',
|
||||
render: (_, r) =>
|
||||
r.audit_state_id <= 0 ? (
|
||||
<Space>
|
||||
<Button onClick={() => handleAuditPriceItem('2', r)}>✔</Button>
|
||||
<Button onClick={() => handleAuditPriceItem('3', r)}>✖</Button>
|
||||
</Space>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
return <Table size={'small'} className='border-collapse' rowHoverable={false} rowClassName={rowStyle} pagination={false} {...{ columns, dataSource }} rowKey={(r) => r.id} />;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const TypesPanels = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [loading, agencyProducts] = useProductsStore((state) => [state.loading, state.agencyProducts]);
|
||||
// console.log(agencyProducts);
|
||||
const productsTypes = useProductsTypes();
|
||||
const [activeKey, setActiveKey] = useState([]);
|
||||
const [showTypes, setShowTypes] = useState([]);
|
||||
useEffect(() => {
|
||||
// 只显示有产品的类型; 展开产品的价格表, 合并名称列; 转化为价格主表, 携带产品属性信息
|
||||
const hasDataTypes = Object.keys(agencyProducts);
|
||||
const _show = productsTypes
|
||||
.filter((kk) => hasDataTypes.includes(kk.value))
|
||||
.map((ele) => ({
|
||||
...ele,
|
||||
extra: t('Table.Total', { total: agencyProducts[ele.value].length }),
|
||||
children: (
|
||||
<PriceTable
|
||||
// loading={loading}
|
||||
productType={ele.value}
|
||||
dataSource={agencyProducts[ele.value].reduce(
|
||||
(r, c, ri) =>
|
||||
r.concat(
|
||||
c.quotation.map((q, i) => ({
|
||||
...q,
|
||||
weekdays: q.weekdays
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
.map((w) => t(`weekdaysShort.${w}`))
|
||||
.join(', '),
|
||||
info: c.info,
|
||||
lgc_details: c.lgc_details.reduce((rlgc, clgc) => ({...r, [clgc.lgc]: clgc}), {}),
|
||||
rowSpan: i === 0 ? c.quotation.length : 0,
|
||||
rowSpanI: [ri, i],
|
||||
}))
|
||||
),
|
||||
[]
|
||||
)}
|
||||
refresh={props.refresh}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
setShowTypes(_show);
|
||||
|
||||
setActiveKey(isEmpty(_show) ? [] : [_show[0].key]);
|
||||
return () => {};
|
||||
}, [productsTypes, agencyProducts]);
|
||||
|
||||
const onCollapseChange = (_activeKey) => {
|
||||
setActiveKey(_activeKey);
|
||||
};
|
||||
return <Collapse items={showTypes} activeKey={activeKey} onChange={onCollapseChange} />;
|
||||
};
|
||||
|
||||
const Audit = ({ ...props }) => {
|
||||
const { travel_agency_id, use_year, audit_state } = useParams();
|
||||
const [loading, activeAgency, getAgencyProducts] = useProductsStore((state) => [state.loading, state.activeAgency, state.getAgencyProducts]);
|
||||
|
||||
const handleGetAgencyProducts = () => {
|
||||
getAgencyProducts({ travel_agency_id, use_year, audit_state });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleGetAgencyProducts();
|
||||
|
||||
return () => {};
|
||||
}, [travel_agency_id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecondHeaderWrapper header={<Header title={activeAgency.travel_agency_name} agency={activeAgency} refresh={handleGetAgencyProducts} />} loading={loading} >
|
||||
{/* debug: 0 */}
|
||||
{/* <PrintContractPDF /> */}
|
||||
|
||||
<TypesPanels refresh={handleGetAgencyProducts} />
|
||||
</SecondHeaderWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Audit;
|
@ -0,0 +1,175 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { App, Table, Button, Modal, Popconfirm } from 'antd';
|
||||
import { getAgencyProductExtrasAction, getAgencyProductsAction, addProductExtraAction, delProductExtrasAction } from '@/stores/Products/Index';
|
||||
import { cloneDeep, pick } from '@/utils/commons';
|
||||
import SearchForm from '@/components/SearchForm';
|
||||
|
||||
import RequireAuth from '@/components/RequireAuth';
|
||||
import { PERM_PRODUCTS_MANAGEMENT } from '@/config';
|
||||
import { useProductsTypesMapVal } from '@/hooks/useProductsSets';
|
||||
|
||||
const NewAddonModal = ({ onPick, ...props }) => {
|
||||
const { travel_agency_id, use_year } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const { notification, message } = App.useApp();
|
||||
|
||||
const productsTypesMapVal = useProductsTypesMapVal();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false); // bind loading
|
||||
const [searchLoading, setSearchLoading] = useState(false);
|
||||
const [searchResult, setSearchResult] = useState([]);
|
||||
|
||||
const onSearchProducts = async (values) => {
|
||||
const copyObject = cloneDeep(values);
|
||||
const { starttime, endtime, ...param } = copyObject;
|
||||
setSearchLoading(true);
|
||||
setSearchResult([]);
|
||||
// debug: audit_state: '1',
|
||||
const result = await getAgencyProductsAction({ ...param, audit_state: '0', travel_agency_id, use_year });
|
||||
setSearchResult(result?.products || []);
|
||||
setSearchLoading(false);
|
||||
};
|
||||
const handleAddExtras = async (item) => {
|
||||
if (typeof onPick === 'function') {
|
||||
onPick(item);
|
||||
}
|
||||
};
|
||||
|
||||
// todo: 如何显示价格表
|
||||
const searchResultColumns = [
|
||||
{ key: 'ptype', dataIndex: ['info', 'product_type_id'], width: '6rem', title: t('products:ProductType'), render: (text, r) => productsTypesMapVal[text].label },
|
||||
{ key: 'title', dataIndex: ['info', 'title'], width: '16rem', title: t('products:Title') },
|
||||
{
|
||||
title: t('products:price'),
|
||||
dataIndex: ['quotation', '0', 'adult_cost'],
|
||||
width: '10rem',
|
||||
render: (_, { quotation }) => `${quotation[0].adult_cost} ${quotation[0].currency} / ${quotation[0].unit_name}`,
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<Button className='text-primary' onClick={() => handleAddExtras(record)}>
|
||||
附加此项目
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
const paginationProps = {
|
||||
showTotal: (total) => t('Table.Total', { total }),
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Button type='primary' onClick={() => setOpen(true)} className='mt-2'>
|
||||
{t('New')} {t('products:EditComponents.Extras')}
|
||||
</Button>
|
||||
|
||||
<Modal width={'95%'} style={{ top: 20 }} open={open} title={'添加附加'} footer={false} onCancel={() => setOpen(false)} destroyOnClose>
|
||||
<SearchForm
|
||||
fieldsConfig={{
|
||||
shows: ['dates', 'year', 'keyword'],
|
||||
fieldProps: {
|
||||
dates: { label: t('products:CreateDate') },
|
||||
keyword: { label: t('products:Title'), col: 4 },
|
||||
},
|
||||
// sort: { keyword: 100 },
|
||||
}}
|
||||
initialValue={
|
||||
{
|
||||
// dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
|
||||
// year: dayjs().add(1, 'year'),
|
||||
}
|
||||
}
|
||||
onSubmit={(err, formVal, filedsVal) => {
|
||||
onSearchProducts(formVal);
|
||||
}}
|
||||
/>
|
||||
<Table
|
||||
size={'small'}
|
||||
key={'searchProductsTable'}
|
||||
loading={searchLoading}
|
||||
dataSource={searchResult}
|
||||
columns={searchResultColumns}
|
||||
pagination={searchResult.length <= 10 ? false : paginationProps}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const Extras = ({ productId, onChange, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
const { notification, message } = App.useApp();
|
||||
|
||||
const { travel_agency_id, use_year } = useParams();
|
||||
|
||||
const [extrasData, setExtrasData] = useState([]);
|
||||
|
||||
const handleGetAgencyProductExtras = async () => {
|
||||
const data = await getAgencyProductExtrasAction({ id: productId, travel_agency_id, use_year });
|
||||
setExtrasData(data);
|
||||
};
|
||||
|
||||
const handleNewAddOn = async (item) => {
|
||||
setExtrasData(prev => [].concat(prev, [item]));
|
||||
// todo: 提交后端; 重复绑定同一个
|
||||
const _item = pick(item.info, ['id', 'title', 'code']);
|
||||
const newSuccess = await addProductExtraAction({ travel_agency_id, id: productId, extras: [_item] });
|
||||
newSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`);
|
||||
await handleGetAgencyProductExtras();
|
||||
}
|
||||
|
||||
const handleDelAddon = async (item) => {
|
||||
const delSuccess = await delProductExtrasAction({ travel_agency_id, id: productId, extras: [item.info.id] });
|
||||
delSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`);
|
||||
await handleGetAgencyProductExtras();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleGetAgencyProductExtras();
|
||||
|
||||
return () => {};
|
||||
}, []);
|
||||
|
||||
const columns = [
|
||||
{ title: t('products:Title'), dataIndex: ['info', 'title'], width: '16rem', },
|
||||
{
|
||||
title: t('products:Offer'),
|
||||
dataIndex: ['quotation', '0', 'value'],
|
||||
width: '10rem',
|
||||
|
||||
render: (_, { quotation }) => `${quotation[0].adult_cost} ${quotation[0].currency} / ${quotation[0].unit_name}`, // todo: 成人 儿童
|
||||
},
|
||||
// { title: t('products:Types'), dataIndex: 'age_type', width: '40%', },
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operation',
|
||||
width: '4rem',
|
||||
render: (_, r) => (
|
||||
<Popconfirm title={t('sureDelete')} onConfirm={(e) => handleDelAddon(r)} okText={t('Yes')} >
|
||||
<Button size='small' type='link' danger>
|
||||
{t('Delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT}>
|
||||
<h2>{t('products:EditComponents.Extras')}</h2>
|
||||
<Table dataSource={extrasData} columns={columns} bordered pagination={false} rowKey={(r) => r.info.id} />
|
||||
<NewAddonModal onPick={handleNewAddOn} />
|
||||
</RequireAuth>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Extras;
|
@ -0,0 +1,121 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { App, Space, Table, Button, Modal } from 'antd';
|
||||
import SearchForm from '@/components/SearchForm';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useProductsStore, { copyAgencyDataAction } from '@/stores/Products/Index';
|
||||
import useFormStore from '@/stores/Form';
|
||||
import { objectMapper } from '@/utils/commons';
|
||||
|
||||
function Index() {
|
||||
const { notification, message } = App.useApp();
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation();
|
||||
const [loading, agencyList, searchAgency] = useProductsStore((state) => [state.loading, state.agencyList, state.searchAgency]);
|
||||
const [searchValues, setSearchValues] = useProductsStore((state) => [state.searchValues, state.setSearchValues]);
|
||||
const formValuesToSub = useFormStore(state => state.formValuesToSub);
|
||||
|
||||
const handleSearchAgency = (formVal = undefined) => {
|
||||
const { starttime, endtime, ...param } = formVal || formValuesToSub;
|
||||
const searchParam = objectMapper(param, { agency: 'travel_agency_ids', startdate: 'edit_date1', enddate: 'edit_date2', year: 'use_year' });
|
||||
setSearchValues(searchParam);
|
||||
searchAgency(searchParam);
|
||||
}
|
||||
|
||||
const [copyModalVisible, setCopyModalVisible] = useState(false);
|
||||
const [sourceAgency, setSourceAgency] = useState({});
|
||||
const [copyLoading, setCopyLoading] = useState(false);
|
||||
const handleCopyAgency = async (param) => {
|
||||
setCopyLoading(true);
|
||||
const postbody = objectMapper(param, { agency: 'target_agency', });
|
||||
const toID = postbody.target_agency;
|
||||
const success = await copyAgencyDataAction({...postbody, source_agency: sourceAgency.travel_agency_id});
|
||||
setCopyLoading(false);
|
||||
success ? message.success('复制成功') : message.error('复制失败');
|
||||
|
||||
setCopyModalVisible(false);
|
||||
navigate(`/products/${toID}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`);
|
||||
};
|
||||
|
||||
const openCopyModal = (from) => {
|
||||
setSourceAgency(from);
|
||||
setCopyModalVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// handleSearchAgency();
|
||||
}, []);
|
||||
|
||||
const showTotal = (total) => t('Table.Total', { total });
|
||||
|
||||
const columns = [
|
||||
{ title: t('products:Vendor'), key: 'vendor', dataIndex: 'travel_agency_name' },
|
||||
{ title: t('products:CreatedBy'), key: 'created_by', dataIndex: 'created_by_name' },
|
||||
{ title: t('products:CreateDate'), key: 'create_date', dataIndex: 'create_date' },
|
||||
{ title: t('products:AuState'), key: 'audit_state', dataIndex: 'audit_state' },
|
||||
{ title: t('products:AuditedBy'), key: 'audited_by', dataIndex: 'audited_by_name' },
|
||||
{ title: t('products:AuditDate'), key: 'audit_date', dataIndex: 'audit_date' },
|
||||
{
|
||||
title: '',
|
||||
key: 'action',
|
||||
render: (_, r) => (
|
||||
<Space size={'large'}>
|
||||
<Link to={`/products/${r.travel_agency_id}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`}>{t('Edit')}</Link>
|
||||
<Link to={`/products/${r.travel_agency_id}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/audit`}>{t('Audit')}</Link>
|
||||
<Button type='link' onClick={() => openCopyModal(r)}>{t('Copy')}</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Space direction='vertical' style={{ width: '100%' }}>
|
||||
<SearchForm
|
||||
fieldsConfig={{
|
||||
shows: ['agency', 'audit_state', 'dates', 'year', ],
|
||||
fieldProps: {
|
||||
agency: { col: 4 },
|
||||
dates: { label: t('products:CreateDate') },
|
||||
keyword: { label: t('products:Title'), col: 4 },
|
||||
},
|
||||
sort: { agency: 1, audit_state: 2, keyword: 100 },
|
||||
}}
|
||||
initialValue={{
|
||||
dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
|
||||
audit_state: { value: '', label: t('products:State') },
|
||||
year: dayjs().add(1, 'year'),
|
||||
}}
|
||||
onSubmit={(err, formVal, filedsVal) => {
|
||||
handleSearchAgency(formVal);
|
||||
}}
|
||||
/>
|
||||
<Table bordered={true} columns={columns} dataSource={agencyList} pagination={{ defaultPageSize: 20, showTotal: showTotal }} loading={loading} rowKey={'travel_agency_id'} />
|
||||
|
||||
{/* 复制弹窗 */}
|
||||
<Modal width={600} open={copyModalVisible} title={`复制供应商产品`} footer={false} onCancel={() => setCopyModalVisible(false)} destroyOnClose>
|
||||
<div className='py-2'>复制源: {sourceAgency.travel_agency_name}</div>
|
||||
<SearchForm formName='copyform' loading={copyLoading}
|
||||
confirmText={t('Confirm')}
|
||||
fieldsConfig={{
|
||||
shows: ['agency', 'products_types', 'dept'],
|
||||
fieldProps: {
|
||||
agency: { label: `目标${t('products:Vendor')}`, col: 24, rules: [{ required: true, message: t('products:CopyFormMsg.requiredVendor') }] },
|
||||
products_types: { col: 24 },
|
||||
dept: { col: 24, rules: [{ required: true, message: t('products:CopyFormMsg.requiredDept') }] },
|
||||
},
|
||||
fieldComProps: {
|
||||
agency: { mode: null }, // 单选
|
||||
products_types: { mode: 'multiple' },
|
||||
dept: { isLeaf: true },
|
||||
},
|
||||
}}
|
||||
onSubmit={(err, formVal, filedsVal) => {
|
||||
handleCopyAgency(formVal);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index;
|
Loading…
Reference in New Issue