Merge remote-tracking branch 'remotes/origin/feature/price_manager'

# Conflicts:
#	src/assets/global.css
#	src/main.jsx
#	src/views/App.jsx
perf/export-docx
Jimmy Liow 1 year ago
commit 3bbd694dc6

@ -10,7 +10,9 @@
"Confirm": "Confirm",
"Close": "Close",
"Save": "Save",
"New": "New",
"Edit": "Edit",
"Audit": "Audit",
"Delete": "Delete",
"Add": "Add",
"View": "View",
@ -19,6 +21,23 @@
"Upload": "Upload",
"preview": "Preview",
"Total": "Total",
"Action": "Action",
"Import": "Import",
"Export": "Export",
"Copy": "Copy",
"sureCancel": "Are you sure to cancel?",
"sureDelete":"Are you sure to delete?",
"Yes": "Yes",
"Success": "Success",
"Failed": "Failed",
"All": "All",
"Table": {
"Total": "Total {{total}} items"
},
"Login": "Login",
"Username": "Username",
@ -46,13 +65,32 @@
"lastThreeMonth": "Last Three Month",
"thisYear": "This Year"
},
"weekdays": {
"1": "Monday",
"2": "Tuesday",
"3": "Wednesday",
"4": "Thursday",
"5": "Friday",
"6": "Saturday",
"7": "Sunday"
},
"weekdaysShort": {
"1": "Mon",
"2": "Tue",
"3": "Wed",
"4": "Thu",
"5": "Fri",
"6": "Sat",
"7": "Sun"
},
"menu": {
"Reservation": "Reservation",
"Invoice": "Invoice",
"Feedback": "Feedback",
"Notice": "Notice",
"Report": "Report",
"Airticket": "AirTicket"
"Airticket": "AirTicket",
"Products": "Products"
},
"Validation": {
"Title": "Notification",

@ -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"
},
"#": "#"
}

@ -10,7 +10,9 @@
"Confirm": "确认",
"Close": "关闭",
"Save": "保存",
"New": "新增",
"Edit": "编辑",
"Audit": "审核",
"Delete": "删除",
"Add": "添加",
"View": "查看",
@ -19,6 +21,23 @@
"Upload": "上传",
"preview": "预览",
"Total": "总数",
"Action": "操作",
"Import": "导入",
"Export": "导出",
"Copy": "复制",
"sureCancel": "确定取消?",
"sureDelete":"确定删除?",
"Yes": "是",
"Success": "成功",
"Failed": "失败",
"All": "所有",
"Table": {
"Total": "共 {{total}} 条"
},
"Login": "登录",
"Username": "账号",
@ -46,13 +65,32 @@
"lastThreeMonth": "前三个月",
"thisYear": "今年"
},
"weekdays": {
"1": "周一",
"2": "周二",
"3": "周三",
"4": "周四",
"5": "周五",
"6": "周六",
"7": "周日"
},
"weekdaysShort": {
"1": "一",
"2": "二",
"3": "三",
"4": "四",
"5": "五",
"6": "六",
"7": "日"
},
"menu": {
"Reservation": "团预订",
"Invoice": "账单",
"Feedback": "反馈表",
"Notice": "通知",
"Report": "质量评分",
"Airticket": "机票订票"
"Airticket": "机票订票",
"Products": "产品管理"
},
"Validation": {
"Title": "温馨提示",

@ -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;

@ -3,14 +3,38 @@ import { Form, Input, Row, Col, Select, DatePicker, Space, Button } from 'antd';
import { objectMapper, at } from '@/utils/commons';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from '@/config';
import useFormStore from '@/stores/Form';
import usePresets from '@/hooks/usePresets';
import { useDatePresets } from '@/hooks/useDatePresets';
import { useProductsTypes } from '@/hooks/useProductsSets';
import { useTranslation } from 'react-i18next';
import { fetchJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
import SearchInput from './SearchInput';
import AuditStateSelector from './AuditStateSelector';
import DeptSelector from './DeptSelector';
//
export const fetchVendorList = async (q) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/VendorList`, { q })
return errcode !== 0 ? [] : result
}
const { RangePicker } = DatePicker;
const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
export const SelectProductsTypes = ({...props}) => {
const productsTypes = useProductsTypes();
const { t } = useTranslation();
return (
<>
<Select labelInValue allowClear placeholder={t('products:ProductType')} options={productsTypes} {...props}/>
</>
);
};
const SearchForm = ({ initialValue, onSubmit, onReset, confirmText, formName, formLayout, loading, ...props }) => {
const { t } = useTranslation();
const presets = usePresets();
const presets = useDatePresets();
const [formValues, setFormValues] = useFormStore((state) => [state.formValues, state.setFormValues]);
const [formValuesToSub, setFormValuesToSub] = useFormStore((state) => [state.formValuesToSub, state.setFormValuesToSub]);
const [form] = Form.useForm();
@ -23,11 +47,11 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
shows: [],
...props.fieldsConfig,
};
const { confirmText } = props;
const readValues = { ...initialValue, ...formValues };
const formValuesMapper = (values) => {
const destinationObject = {
'keyword': { key: 'keyword', transform: (value) => value || '' },
'referenceNo': { key: 'referenceNo', transform: (value) => value || '' },
'dates': [
{ key: 'startdate', transform: (arrVal) => (arrVal ? arrVal[0].format(DATE_FORMAT) : '') },
@ -36,6 +60,29 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
{ key: 'endtime', transform: (arrVal) => (arrVal ? arrVal[1].format(SMALL_DATETIME_FORMAT) : '') },
],
'invoiceStatus': { key: 'invoiceStatus', transform: (value) => value?.value || value?.key || '', default: '' },
'audit_state': { key: 'audit_state', transform: (value) => value?.value || value?.key || '', default: '' },
'agency': {
key: 'agency',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
'year': [
{ key: 'year', transform: (arrVal) => (arrVal ? arrVal.format('YYYY') : '') },
],
'products_types': {
key: 'products_types',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
'dept': {
key: 'dept',
transform: (value) => {
console.log(value);
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
};
let dest = {};
const { dates, ...omittedValue } = values;
@ -57,9 +104,9 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
}, []);
const onFinish = (values) => {
console.log('Received values of form, origin form value: ', values);
console.log('Received values of form, origin form value: \n', values);
const dest = formValuesMapper(values);
console.log('form value send to onSubmit:', dest);
console.log('form value send to onSubmit:\n', dest);
const str = new URLSearchParams(dest).toString();
setFormValues(values);
setFormValuesToSub(dest);
@ -83,16 +130,21 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
setFormValuesToSub(dest);
// console.log('form onValuesChange', Object.keys(changedValues), args);
};
const onFinishFailed = ({ values, errorFields }) => {
console.log('form validate failed', '\nform values:', values, '\nerrorFields', errorFields);
};
return (
<>
<Form form={form} name='advanced_search' className='orders-search-form' onFinish={onFinish} onValuesChange={onValuesChange}>
<Form form={form} layout={'horizontal'} name={formName || 'advanced_search'} className='orders-search-form' onFinish={onFinish} onValuesChange={onValuesChange} onFinishFailed={onFinishFailed} >
{/* <EditableContext.Provider value={form}> */}
<Row gutter={16}>
{getFields({ sort, initialValue: readValues, hides, shows, fieldProps, fieldComProps, form, presets, t })}
{/* 'textAlign': 'right' */}
<Col flex='1 0 90px' className='flex justify-normal items-start' >
<Space align='center'>
<Button size={'middle'} type='primary' htmlType='submit'>
<Button size={'middle'} type='primary' htmlType='submit' loading={loading}>
{confirmText || t('Search')}
</Button>
{/* <Button size="small" onClick={onReset}>
@ -132,19 +184,27 @@ function getFields(props) {
};
let baseChildren = [];
baseChildren = [
item(
'keyword',
99,
<Form.Item name='keyword' {...fieldProps.keyword}>
<Input allowClear {...fieldComProps.keyword} />
</Form.Item>,
fieldProps?.keyword?.col || 6
),
item(
'referenceNo',
1,
99,
<Form.Item name='referenceNo' label={t('group:RefNo')} {...fieldProps.referenceNo}>
<Input placeholder={t('group:RefNo')} allowClear />
<Input placeholder={t('group:RefNo')} allowClear />
</Form.Item>,
fieldProps?.referenceNo?.col || 4
fieldProps?.referenceNo?.col || 6
),
item(
'PNR',
2,
<Form.Item name='PNR' label="PNR">
<Input placeholder={t('group:PNR')} allowClear />
99,
<Form.Item name='PNR' label='PNR'>
<Input placeholder={t('group:PNR')} allowClear />
</Form.Item>,
fieldProps?.PNR?.col || 4
),
@ -178,21 +238,70 @@ function getFields(props) {
),
item(
'username',
3,
99,
<Form.Item name='username' label={t('account:username')} {...fieldProps.username}>
<Input placeholder={t('account:username')} allowClear />
<Input placeholder={t('account:username')} allowClear />
</Form.Item>,
fieldProps?.username?.col || 4
),
item(
'realname',
4,
99,
<Form.Item name='realname' label={t('account:realname')} {...fieldProps.realname}>
<Input placeholder={t('account:realname')} allowClear />
<Input placeholder={t('account:realname')} allowClear />
</Form.Item>,
fieldProps?.realname?.col || 4
),
/**
*
*/
item(
'year',
99,
<Form.Item name={'year'} label={t('products:UseYear')} {...fieldProps.year} initialValue={at(props, 'initialValue.year')[0]}>
<DatePicker picker='year' allowClear {...fieldComProps.year} />
</Form.Item>,
fieldProps?.year?.col || 3
),
item(
'agency',
99,
<Form.Item name='agency' label={t('products:Vendor')} {...fieldProps.agency} initialValue={at(props, 'initialValue.agency')[0]}>
<SearchInput
placeholder={t('products:Vendor')}
mode={'multiple'}
maxTagCount={0}
{...fieldComProps.agency}
fetchOptions={fetchVendorList}
map={{ travel_agency_name: 'label', travel_agency_id: 'value' }}
/>
</Form.Item>,
fieldProps?.agency?.col || 6
),
item(
'audit_state',
99,
<Form.Item name={`audit_state`} initialValue={at(props, 'initialValue.audit_state')[0] || { value: '', label: 'Status' }}>
<AuditStateSelector {...fieldComProps.audit_state} />
</Form.Item>,
fieldProps?.audit_state?.col || 3
),
item(
'products_types',
99,
<Form.Item name={`products_types`} label={t('products:ProductType')} {...fieldProps.products_types} initialValue={at(props, 'initialValue.products_types')[0] || undefined}>
<SelectProductsTypes maxTagCount={1} {...fieldComProps.products_types} />
</Form.Item>,
fieldProps?.products_types?.col || 6
),
item(
'dept',
99,
<Form.Item name={`dept`} label={t('products:Dept')} {...fieldProps.dept} initialValue={at(props, 'initialValue.dept')[0] || undefined}>
<DeptSelector {...fieldComProps.dept} />
</Form.Item>,
fieldProps?.dept?.col || 6
),
];
baseChildren = baseChildren
.map((x) => {

@ -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;

@ -31,3 +31,10 @@ export const PERM_DOMESTIC = '/domestic/all'
// 机票供应商
// category: air-ticket
export const PERM_AIR_TICKET = '/air-ticket/all'
// 价格管理
export const PERM_PRODUCTS_MANAGEMENT = '/products/*'; // 管理
export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核
export const PERM_PRODUCTS_INFO_PUT = '/products/info/put'; // 信息.录入
export const PERM_PRODUCTS_OFFER_AUDIT = '/products/offer/audit'; // 价格.审核
export const PERM_PRODUCTS_OFFER_PUT = '/products/offer/put'; // 价格.录入

@ -1,8 +1,9 @@
import { useEffect, useState } from 'react';
import dayjs from "dayjs";
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
const usePresets = () => {
export const useDatePresets = () => {
const [presets, setPresets] = useState([]);
const { t, i18n } = useTranslation();
@ -39,4 +40,21 @@ const usePresets = () => {
return presets;
}
export default usePresets;
export const useWeekdays = () => {
const [data, setData] = useState([]);
const { t, i18n } = useTranslation();
useEffect(() => {
const newData = [
{ value: '1', label: t('weekdays.1') },
{ value: '2', label: t('weekdays.2') },
{ value: '3', label: t('weekdays.3') },
{ value: '4', label: t('weekdays.4') },
{ value: '5', label: t('weekdays.5') },
{ value: '6', label: t('weekdays.6') },
{ value: '7', label: t('weekdays.7') },
];
setData(newData);
return () => {};
}, [i18n.language]);
return data;
};

@ -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);
}

@ -1,13 +1,30 @@
import React, { useState } from 'react';
import { Dropdown, Menu } from 'antd';
import { useState, useEffect } from 'react';
import { Dropdown } from 'antd';
import { useTranslation } from 'react-i18next';
import { appendRequestParams } from '@/utils/request';
const i18n_to_htcode = {
'zh': 2,
'en': 1,
};
export const useDefaultLgc = () => {
const { i18n } = useTranslation();
return { language: i18n_to_htcode[i18n.language], };
};
/**
* 语言选择组件
*/
const Language = () => {
const { t, i18n } = useTranslation();
const { t, i18n } = useTranslation();
const [selectedKeys, setSelectedKeys] = useState([i18n.language]);
useEffect(() => {
appendRequestParams('lgc', i18n_to_htcode[i18n.language]);
return () => {};
}, [i18n.language]);
//
const handleChangeLanguage = ({ key }) => {
setSelectedKeys([key]);

@ -17,7 +17,7 @@ i18n
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
ns: ['common', 'group', 'vendor', 'account'],
ns: ['common', 'group', 'vendor', 'account', 'products'],
defaultNS: 'common',
detection: {
// convertDetectedLanguage: 'Iso15897',

@ -34,7 +34,10 @@ import { usingStorage } from '@/hooks/usingStorage'
import useAuthStore from './stores/Auth'
import { isNotEmpty } from '@/utils/commons'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET } from '@/config'
import ProductsManage from '@/views/products/Manage';
import ProductsDetail from '@/views/products/Detail';
import ProductsAudit from '@/views/products/Audit';
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config'
import './i18n'

@ -1,4 +1,3 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { fetchJSON, postForm } from '@/utils/request';
import { groupBy } from '@/utils/commons';
import * as config from '@/config';

@ -1,10 +1,5 @@
import { makeAutoObservable, runInAction } from "mobx";
import { fetchJSON, postForm } from "@/utils/request";
import { prepareUrl, isNotEmpty, objectMapper } from "@/utils/commons";
import { HT_HOST } from "@/config";
import { json } from "react-router-dom";
import * as config from "@/config";
import dayjs from "dayjs";
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
@ -138,366 +133,3 @@ const useInvoiceStore = create(
);
export default useInvoiceStore;
export class Invoice {
constructor(root) {
makeAutoObservable(this, { rootStore: false });
this.root = root;
}
invoiceList = []; //账单列表
invoicekImages = []; //图片列表
invoiceGroupInfo = {}; //账单详细
invoiceProductList = []; //账单细项
invoiceZDDetail = []; //报账信息
invoiceCurrencyList = []; //币种
invoicePicList = []; //多账单图片列表数组
invoiceFormData = { info_money: 0, info_Currency: "", info_date: "" }; //存储form数据
invoicePaid = [] ; //支付账单列表
invoicePaidDetail = []; //每期账单详细
loading = false;
search_date_start = dayjs().subtract(2, "M").startOf("M");
search_date_end = dayjs().endOf("M");
onDateRangeChange = dates => {
console.log(dates);
this.search_date_start = dates==null? null: dates[0];
this.search_date_end = dates==null? null: dates[1];
};
fetchInvoiceList(VEI_SN, GroupNo, DateStart, DateEnd,OrderType) {
this.loading = true;
const fetchUrl = prepareUrl(HT_HOST + "/service-cusservice/PTSearchGMBPageList")
.append("VEI_SN", VEI_SN)
.append("OrderType", 0)
.append("GroupNo", GroupNo.trim())
.append("DateStart", DateStart)
.append("DateEnd", DateEnd)
.append("Orderbytype", 1)
.append("TimeType", 0)
.append("limitmarket", "")
.append("mddgroup", "")
.append("SecuryGroup", "")
.append("TotalNum", 0)
.append("PageSize", 2000)
.append("PageIndex", 1)
.append("PayState",OrderType)
.append("token",this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl).then(json => {
runInAction(() => {
this.loading = false;
if (json.errcode == 0) {
if (isNotEmpty(json.Result)) {
this.invoiceList = json.Result.map((data, index) => {
return {
key: data.GMDSN,
gmd_gri_sn: data.GMD_GRI_SN,
gmd_vei_sn: data.GMD_VEI_SN,
GetGDate: data.GetGDate,
GMD_FillWorkers_SN: data.GMD_FillWorkers_SN,
GMD_FWks_LastEditTime: data.GMD_FWks_LastEditTime,
GMD_VerifyUser_SN: data.GMD_VerifyUser_SN,
GMD_Dealed: data.GMD_Dealed,
GMD_VRequestVerify: data.GMD_VRequestVerify,
LeftGDate: data.LeftGDate,
GMD_FillWorkers_Name: data.GMD_FillWorkers_Name,
GroupName: data.GroupName,
AllMoney: data.AllMoney,
PersonNum: data.PersonNum,
GMD_Currency: data.GMD_Currency,
VName: data.VName,
FKState: data.FKState,
};
});
} else {
this.invoiceList = [];
}
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
});
}
fetchInvoiceDetail(GMDSN, GSN) {
const fetchUrl = prepareUrl(HT_HOST + "/service-cusservice/PTGetZDDetail")
.append("VEI_SN", this.root.authStore.login.travelAgencyId)
.append("GRI_SN", GSN)
.append("GMD_SN", GMDSN)
.append("LGC", 1)
.append("Bill", 1)
.append("token",this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl).then(json => {
runInAction(() => {
if (json.errcode == 0) {
this.invoiceGroupInfo = json.GroupInfo[0];
this.invoiceProductList = json.ProductList;
this.invoiceCurrencyList = json.CurrencyList;
this.invoiceZDDetail = json.ZDDetail;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
return json;
});
}
//获取供应商提交的图片
getInvoicekImages(VEI_SN, GRI_SN) {
let url = `/service-fileServer/ListFile`;
url += `?GRI_SN=${GRI_SN}&VEI_SN=${VEI_SN}&FilePathName=invoice`;
url += `&token=${this.root.authStore.login.token}`;
fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
console.log(json);
runInAction(() => {
this.invoicekImages = json.result.map((data, index) => {
return {
uid: -index, //用负数,防止添加删除的时候错误
name: data.file_name,
status: "done",
url: data.file_url,
};
});
});
})
.catch(error => {
console.log("fetch data failed", error);
});
}
//从数据库获取图片列表
getInvoicekImages_fromData(jsonData) {
let arrLen = jsonData.length;
let arrPicList = jsonData.map((data, index) => {
const GMD_Pic = data.GMD_Pic;
let picList = [];
if (isNotEmpty(GMD_Pic)) {
let js_Pic = JSON.parse(GMD_Pic);
picList = js_Pic.map((picData, pic_Index) => {
return {
uid: -pic_Index, //用负数,防止添加删除的时候错误
name: "",
status: "done",
url: picData.url,
};
});
}
if (data.GMD_Dealed == false && arrLen == index + 1) {
this.invoicekImages = picList;
}
return picList;
});
runInAction(() => {
this.invoicePicList = arrPicList;
});
}
//获取数据库的表单默认数据回填。
getFormData(jsonData) {
let arrLen = jsonData.length;
return jsonData.map((data, index) => {
if (data.GMD_Dealed == false && arrLen == index + 1) {
//只有最后一条账单未审核通过才显示
runInAction(() => {
this.invoiceFormData = { info_money: data.GMD_Cost, info_Currency: data.GMD_Currency, info_date: isNotEmpty(data.GMD_PayDate) ? dayjs(data.GMD_PayDate) : "" };
});
}
});
}
removeFeedbackImages(fileurl) {
let url = `/service-fileServer/FileDelete`;
url += `?fileurl=${fileurl}`;
url += `&token=${this.root.authStore.login.token}`;
return fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
console.log(json);
return json.Result;
})
.catch(error => {
console.log("fetch data failed", error);
});
}
postEditInvoiceDetail(GMD_SN, Currency, Cost, PayDate, Pic, Memo) {
let postUrl = HT_HOST + "/service-cusservice/EditSupplierFK";
let formData = new FormData();
formData.append("LMI_SN", this.root.authStore.login.userId);
formData.append("GMD_SN", GMD_SN);
formData.append("Currency", Currency);
formData.append("Cost", Cost);
formData.append("PayDate", isNotEmpty(PayDate) ? PayDate : "");
formData.append("Pic", Pic);
formData.append("Memo", Memo);
formData.append("token",this.root.authStore.login.token);
return postForm(postUrl, formData).then(json => {
console.info(json);
return json;
});
}
postAddInvoice(GRI_SN, Currency, Cost, PayDate, Pic, Memo) {
let postUrl = HT_HOST + "/service-cusservice/AddSupplierFK";
let formData = new FormData();
formData.append("LMI_SN", this.root.authStore.login.userId);
formData.append("VEI_SN", this.root.authStore.login.travelAgencyId);
formData.append("GRI_SN", GRI_SN);
formData.append("Currency", Currency);
formData.append("Cost", Cost);
formData.append("PayDate", isNotEmpty(PayDate) ? PayDate : "");
formData.append("Pic", Pic);
formData.append("Memo", Memo);
formData.append("token",this.root.authStore.login.token);
return postForm(postUrl, formData).then(json => {
console.info(json);
return json;
});
}
//账单状态
invoiceStatus(FKState) {
switch (FKState - 1) {
case 1:
return "Submitted";
break;
case 2:
return "Travel Advisor";
break;
case 3:
return "Finance Dept";
break;
case 4:
return "Paid";
break;
default:
return "";
break;
}
}
fetchInvoicePaid(VEI_SN, GroupNo, DateStart, DateEnd) {
this.loading = true;
const fetchUrl = prepareUrl(HT_HOST + "/service-Cooperate/Cooperate/GetInvoicePaid")
.append("VEI_SN", VEI_SN)
.append("GroupNo", GroupNo)
.append("DateStart", DateStart)
.append("DateEnd", DateEnd)
.append("token",this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl).then(json => {
runInAction(() => {
this.loading = false;
if (json.errcode == 0) {
if (isNotEmpty(json.Result)) {
this.invoicePaid = json.Result.map((data, index) => {
return {
key: data.fl_id,
fl_finaceNo: data.fl_finaceNo,
fl_vei_sn: data.fl_vei_sn,
fl_year: data.fl_year,
fl_month: data.fl_month,
fl_memo: data.fl_memo,
fl_adddate: data.fl_adddate,
fl_addUserSn: data.fl_addUserSn,
fl_updateUserSn: data.fl_updateUserSn,
fl_updatetime: data.fl_updatetime,
fl_state: data.fl_state,
fl_paid: data.fl_paid,
fl_pic: data.fl_pic,
fcount: data.fcount,
pSum: data.pSum,
};
});
} else {
this.invoicePaid = [];
}
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
});
}
fetchInvoicePaidDetail(VEI_SN,FLID){
this.loading = true;
const fetchUrl = prepareUrl(HT_HOST + "/service-Cooperate/Cooperate/GetInvoicePaidDetail")
.append("VEI_SN", VEI_SN)
.append("fl_id", FLID)
.append("token",this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl).then(json => {
runInAction(() => {
this.loading = false;
if (json.errcode == 0) {
if (isNotEmpty(json.Result)) {
this.invoicePaidDetail = json.Result.map((data, index) => {
return {
key: data.fl2_id,
fl2_fl_id: data.fl2_fl_id,
fl2_GroupName: data.fl2_GroupName,
fl2_gri_sn: data.fl2_gri_sn,
fl2_gmd_sn: data.fl2_gmd_sn,
fl2_wl: data.fl2_wl,
fl2_ArriveDate: data.fl2_ArriveDate,
fl2_price: data.fl2_price,
fl2_state: data.fl2_state,
fl2_updatetime: data.fl2_updatetime,
fl2_updateUserSn: data.fl2_updateUserSn,
fl2_memo: data.fl2_memo,
fl2_memo2: data.fl2_memo2,
fl2_paid: data.fl2_paid,
fl2_pic: data.fl2_pic,
};
});
} else {
this.invoicePaidDetail = [];
}
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
});
}
/* 测试数据 */
//账单列表范例数据
testData = [
{
GSMSN: 449865,
gmd_gri_sn: 334233,
gmd_vei_sn: 628,
GetDate: "2023-04-2 00:33:33",
GMD_FillWorkers_SN: 8617,
GMD_FWks_LastEditTime: "2023-04-26 12:33:33",
GMD_VerifyUser_SN: 8928,
GMD_Dealed: 1,
GMD_VRequestVerify: 1,
TotalCount: 22,
LeftGDate: "2023-03-30 00:00:00",
GMD_FillWorkers_Name: "",
GroupName: " 中华游230501-CA230402033",
AllMoney: 3539,
FKState: 1,
GMD_Currency: "",
PersonNum: "1大1小",
VName: "",
},
];
}
// export default Invoice;

@ -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;

@ -113,8 +113,14 @@ export function postForm(url, data) {
}
export function postJSON(url, obj) {
const initParams = getRequestInitParams();
const params4get = Object.assign({}, initParams);
const params = new URLSearchParams(params4get).toString();
const ifp = url.includes('?') ? '&' : '?';
const fUrl = params !== '' ? `${url}${ifp}${params}` : url;
const headerObj = getRequestHeader()
return fetch(url, {
return fetch(fUrl, {
method: 'POST',
body: JSON.stringify(obj),
headers: {

@ -75,6 +75,8 @@ function App() {
theme={{
token: {
colorPrimary: colorPrimary,
// "sizeStep": 3,
// "sizeUnit": 3,
},
algorithm: theme.defaultAlgorithm,
}}>
@ -117,6 +119,7 @@ function App() {
isPermitted(PERM_OVERSEA) ? { key: 'feedback', label: <Link to='/feedback'>{t('menu.Feedback')}</Link> } : null,
isPermitted(PERM_OVERSEA) ? { key: 'report', label: <Link to='/report'>{t('menu.Report')}</Link> } : null,
isPermitted(PERM_AIR_TICKET) ? { key: 'airticket', label: <Link to='/airticket'>{t('menu.Airticket')}</Link> } : null,
{ key: 'products', label: <Link to='/products'>{t('menu.Products')}</Link> },
{
key: 'notice',
label: (

@ -331,6 +331,7 @@ function Management() {
username: { label: t('account:username') },
realname: { label: t('account:realname') },
},
sort: { username: 1, realname: 2, dates: 3},
}}
onSubmit={() => {
handelAccountSearch()

@ -1,6 +1,4 @@
import { NavLink, useNavigate } from "react-router-dom";
import { useState } from "react";
import { toJS } from "mobx";
import { Row, Col, Space, Button, Table, App, Steps } from "antd";
import { formatDate, isNotEmpty } from "@/utils/commons";
import { AuditOutlined, SmileOutlined, SolutionOutlined, EditOutlined } from "@ant-design/icons";
@ -82,7 +80,7 @@ function Index() {
fieldsConfig={{
shows: ['referenceNo', 'invoiceStatus', 'dates'],
fieldProps: {
referenceNo: { col: 5 },
referenceNo: { col: 7 },
invoiceStatus: { col: 4},
dates: { col: 10 },
},
@ -103,7 +101,7 @@ function Index() {
</Row>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered pagination={{ defaultPageSize: 20, showTotal: showTotal }} columns={invoiceListColumns} dataSource={toJS(invoiceList)} />
<Table bordered pagination={{ defaultPageSize: 20, showTotal: showTotal }} columns={invoiceListColumns} dataSource={(invoiceList)} />
</Col>
</Row>
</Space>

@ -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,733 @@
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Button, Card, Col, Row, Breadcrumb, Table, Popconfirm, Form, Input, InputNumber, Tag, Modal, Select, Tree } from 'antd';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Date from '@/components/date';
import { searchAgencyAction, getAgencyProductsAction } from '@/stores/Products/Index';
import { useProductsTypes } from '@/hooks/useProductsSets';
import Extras from './Detail/Extras';
import { groupBy } from '@/utils/commons';
import { useParams } from 'react-router-dom';
import { useHTLanguageSets } from '@/hooks/useHTLanguageSets';
import { useDefaultLgc } from '@/i18n/LanguageSwitcher';
import BatchImportPrice from '@/components/BatchImportPrice';
function Detail() {
const { t } = useTranslation();
const [form] = Form.useForm();
const [editingid, setEditingid] = useState('');
const [tags, setTags] = useState([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedTag, setSelectedTag] = useState(null);
const [saveData, setSaveData] = useState(null);
const [datePickerVisible, setDatePickerVisible] = useState(false);
const [batchImportPriceVisible, setBatchImportPriceVisible] = useState(false);
const [currentid, setCurrentid] = useState(null);
const [languageStatus, setLanguageStatus] = useState(null);
const [selectedNodeid, setSelectedNodeid] = useState(null);
const [remainderLanguage, setRemainderLanguage] = useState([])
const [selectedDateData, setSelectedDateData] = useState({ dateRange: null, selectedDays: [] });
const [treeData, setTreeData] = useState([]);
const productsTypes = useProductsTypes();
const [productsData, setProductsData] = useState(null);
const [quotation, setQuotation] = useState(null);
const [lgc_details, setLgc_details] = useState(null);
const [languageLabel, setLanguageLabel] = useState(null);
const { travel_agency_id } = useParams();
const { language } = useDefaultLgc();
const HTLanguageSets = useHTLanguageSets();
const { Search } = Input;
const [expandedKeys, setExpandedKeys] = useState([]);
const [searchValue, setSearchValue] = useState('');
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [dataList, setDataList] = useState([]);
const [defaultData, setDefaultData] = useState([]);
const productProject = {
"6": [],
"B": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
{ code: "km", name: t('products:KM') },
{ code: "remarks", name: t('products:Remarks') }
],
"J": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
{ code: "recommends_rate", name: t('products:recommendationRate') },
{ code: "duration", name: t('products:Duration') },
{ code: "dept_name", name: t('products:Dept') },
{ code: "display_to_c", name: t('products:DisplayToC') },
{ code: "remarks", name: t('products:Remarks') },
],
"Q": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
{ code: "recommends_rate", name: t('products:recommendationRate') },
{ code: "duration", name: t('products:Duration') },
{ code: "dept_name", name: t('products:Dept') },
{ code: "display_to_c", name: t('products:DisplayToC') },
{ code: "remarks", name: t('products:Remarks') },
],
"D": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
{ code: "recommends_rate", name: t('products:recommendationRate') },
{ code: "duration", name: t('products:Duration') },
{ code: "dept_name", name: t('products:Dept') },
{ code: "display_to_c", name: t('products:DisplayToC') },
{ code: "remarks", name: t('products:Remarks') },
],
"7": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
{ code: "recommends_rate", name: t('products:recommendationRate') },
{ code: "duration", name: t('products:Duration') },
{ code: "open_weekdays", name: t('products:OpenWeekdays') },
{ code: "remarks", name: t('products:Remarks') },
],
"R": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
]
}
const [selectedCategory, setSelectedCategory] = useState(productProject.B);
useEffect(() => {
setLanguageStatus(language);
const matchedLanguage = HTLanguageSets.find(HTLanguage => HTLanguage.key === language.toString());
const languageLabel = matchedLanguage.label
// setTags([languageLabel])
setLanguageLabel(languageLabel)
setSelectedTag(languageLabel)
setRemainderLanguage(HTLanguageSets.filter(item => item.key !== language.toString()))
}, []);
useEffect(() => {
const fetchData = async () => {
const a = { travel_agency_id };
const res = await getAgencyProductsAction(a);
const groupedProducts = groupBy(res.products, (row) => row.info.product_type_id);
const generateTreeData = (productsTypes, productsData) => {
return productsTypes.map(type => ({
title: type.label,
key: type.value,
selectable: false,
children: (productsData[type.value] || []).map(product => ({
title: product.info.title,
key: `${type.value}-${product.info.id}`,
}))
}));
};
const treeData = generateTreeData(productsTypes, groupedProducts);
setTreeData(treeData);
setProductsData(groupedProducts);
setDefaultData(treeData);
setDataList(flattenTreeData(treeData));
};
fetchData();
}, [productsTypes]);
const flattenTreeData = (tree) => {
let flatList = [];
const flatten = (nodes) => {
nodes.forEach((node) => {
flatList.push({ title: node.title, key: node.key });
if (node.children) {
flatten(node.children);
}
});
};
flatten(tree);
return flatList;
};
const getParentKey = (key, tree) => {
let parentKey;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item) => item.key === key)) {
parentKey = node.key;
} else {
const pKey = getParentKey(key, node.children);
if (pKey) {
parentKey = pKey;
}
}
}
}
return parentKey;
};
const titleRender = (node) => {
const index = node.title.indexOf(searchValue);
const beforeStr = node.title.substr(0, index);
const afterStr = node.title.substr(index + searchValue.length);
const highlighted = (
<span style={{ color: 'red' }}>{searchValue}</span>
);
return index > -1 ? (
<span>
{beforeStr}
{highlighted}
{afterStr}
</span>
) : (
<span>{node.title}</span>
);
};
const onChange = (e) => {
const { value } = e.target;
const newExpandedKeys = dataList
.filter(item => item.title.includes(value))
.map(item => getParentKey(item.key, defaultData))
.filter((item, i, self) => item && self.indexOf(item) === i);
console.log("newExpandedKeys", newExpandedKeys)
setExpandedKeys(newExpandedKeys);
setSearchValue(value);
setAutoExpandParent(true);
};
const onExpand = (keys) => {
setExpandedKeys(keys);
setAutoExpandParent(false);
};
const isEditing = (record) => record.id === editingid;
const edit = (record) => {
form.setFieldsValue({ ...record });
setEditingid(record.id);
};
const cancel = () => {
setEditingid('');
};
const handleSave = async (id) => {
try {
const { info, ...restRow } = await form.validateFields();
const newData = [...quotation];
const index = newData.findIndex((item) => id === item.id);
if (index > -1) {
const item = newData[index];
newData.splice(index, 1, { ...item, ...restRow });
delete newData[index].quotation
delete newData[index].extras
setQuotation(newData);
setEditingid('');
} else {
newData.push(restRow);
setQuotation(newData);
setEditingid('');
}
} catch (errInfo) {
console.log('Validate Failed:', errInfo);
}
};
const handleDelete = (id) => {
const newData = [...quotation];
const index = newData.findIndex((item) => id === item.id);
newData.splice(index, 1);
setQuotation(newData);
};
const handleAdd = () => {
const newData = {
id: `${quotation.length + 1}`,
value: '',
currency: '',
unit_name: '',
weekdays: '',
use_dates_start: '',
use_dates_end: '',
group_size_min: '',
group_size_max: ''
};
setQuotation([...quotation, newData]);
};
const handleBatchImport = () => {
setBatchImportPriceVisible(true);
}
const handleDateSelect = (id) => {
setCurrentid(id);
setDatePickerVisible(true);
};
const handleDateChange = ({ dateRange, selectedDays }) => {
//
const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
let weekDayCount = selectedDays.map(day => weekdays.indexOf(day) + 1).sort().join(',');
if (!weekDayCount || weekDayCount.length === 0) {
weekDayCount = "全年";
}
const newData = [...quotation];
const index = newData.findIndex((item) => currentid === item.id);
if (index > -1) {
newData[index].weekdays = weekDayCount;
setQuotation(newData);
}
setSelectedDateData({ dateRange, selectedDays })
};
const handleDateOk = () => {
const { dateRange } = selectedDateData;
const dateRangeList = dateRange.split('-');
const use_dates_start = dateRangeList[0];
const use_dates_end = dateRangeList[1];
if (currentid !== null) {
const newData = [...quotation];
const index = newData.findIndex((item) => currentid === item.id);
if (index > -1) {
newData[index].use_dates_start = use_dates_start;
newData[index].use_dates_end = use_dates_end;
setQuotation(newData);
setCurrentid(null);
}
}
setDatePickerVisible(false);
}
const handleBatchImportOK = () => {
setBatchImportPriceVisible(false);
}
const EditableCell = ({ editing, dataIndex, title, inputType, record, children, handleDateSelect, ...restProps }) => {
let inputNode = inputType === 'number' ? <InputNumber /> : <Input />;
if (dataIndex === 'validityPeriod' && editing) {
return (
<td {...restProps}>
{children}
<Button onClick={() => handleDateSelect(record.id)}>选择日期</Button>
</td>
);
}
if (dataIndex === 'unit_name' && editing) {
inputNode = (
<Select>
<Select.Option value="每人">每人</Select.Option>
<Select.Option value="每团">每团</Select.Option>
</Select>
);
}
if (dataIndex === 'currency' && editing) {
inputNode = (
<Select>
<Select.Option value="每人">RMB</Select.Option>
<Select.Option value="每团">MY</Select.Option>
</Select>
);
}
if (dataIndex === 'group_size' && editing) {
return (
<td {...restProps} style={{ height: 115, display: 'flex', alignItems: 'center' }}>
<InputNumber
min={0}
value={record.group_size_min}
onChange={(value) => handleInputGroupSize('group_size_min', record.id, 'group_size', value)}
style={{ width: '50%', marginRight: '10px' }}
/>
<span>-</span>
<InputNumber
min={0}
value={record.group_size_max}
onChange={(value) => handleInputGroupSize('group_size_max', record.id, 'group_size', value)}
style={{ width: '50%', marginLeft: '10px' }}
/>
</td>
);
}
return (
<td {...restProps}>
{editing ? (
<Form.Item
name={dataIndex}
style={{ margin: 0 }}
rules={[{ required: true, message: `Please Input ${title}!` }]}
>
{inputNode}
</Form.Item>
) : (
children
)}
</td>
);
};
const handleInputGroupSize = (name, id, dataIndex, value) => {
const newData = [...quotation];
const index = newData.findIndex((item) => id === item.id);
if (index > -1) {
const item = newData[index];
newData[index] = { ...item, }
if (name === 'group_size_min') {
newData[index] = { ...item, group_size_min: value };
} else {
newData[index] = { ...item, group_size_max: value };
}
setQuotation(newData);
}
}
const columns = [
{ title: t('products:adultPrice'), dataIndex: 'adult_cost', width: '10%', editable: true },
{ title: t('products:childrenPrice'), dataIndex: 'child_cost', width: '10%', editable: true },
{ title: t('products:currency'), dataIndex: 'currency', width: '10%', editable: true },
{ title: t('products:Types'), dataIndex: 'unit_name', width: '10%', editable: true },
{
title: t('products:number'),
dataIndex: 'group_size',
width: '20%',
editable: true,
render: (_, record) => `${record.group_size_min}-${record.group_size_max}`
},
{
title: t('products:validityPeriod'),
dataIndex: 'validityPeriod',
width: '20%',
editable: true,
render: (_, record) => `${record.use_dates_start}-${record.use_dates_end}`
},
{ title: t('products:Weekdays'), dataIndex: 'weekdays', width: '10%' },
{
title: t('products:operation'),
dataIndex: 'operation',
render: (_, record) => {
const editable = isEditing(record);
return editable ? (
<span>
<a href="#!" onClick={() => handleSave(record.id)} style={{ marginRight: 8 }}>{t('products:save')}</a>
<Popconfirm title={t('products:sureCancel')} onConfirm={cancel}><a>{t('products:cancel')}</a></Popconfirm>
</span>
) : (
<span>
<a disabled={editingid !== ''} onClick={() => edit(record)} style={{ marginRight: 8 }}>{t('products:edit')}</a>
<Popconfirm title={t('products:sureDelete')} onConfirm={() => handleDelete(record.id)}>
<a>{t('products:delete')}</a>
</Popconfirm>
</span>
);
},
},
];
const mergedColumns = columns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record) => ({
record,
inputType: col.dataIndex === 'age' ? 'number' : 'text',
dataIndex: col.dataIndex,
title: col.title,
editing: isEditing(record),
handleDateSelect: handleDateSelect,
}),
};
});
const handleTagClick = (tag) => {
setSelectedTag(tag);
const matchedLanguage = HTLanguageSets.find(language => language.label === tag);
const key = matchedLanguage ? matchedLanguage.key : null;
form.setFieldsValue({
lgc_details: {
title: lgc_details[key] ? lgc_details[key].title : '',
descriptions: lgc_details[key] ? lgc_details[key].descriptions : ''
}
});
setLanguageStatus(key)
};
const showModal = () => setIsModalVisible(true);
const handleOk = () => {
if (!selectedTag) return;
if (!remainderLanguage.some(item => item.label === selectedTag)) return;
if (remainderLanguage.includes(selectedTag)) return;
let tempRemainderLanguage = remainderLanguage.filter((item)=>{
return item.label !== selectedTag;
})
setRemainderLanguage(tempRemainderLanguage)
setTags([...tags, selectedTag])
setSelectedTag(null);
setIsModalVisible(false);
}
const handleCancel = () => setIsModalVisible(false);
const handleTagChange = (value) => {
console.log("handleTagChange", value)
setSelectedTag(value);
console.log("setSelectedTag", selectedTag)
};
const handleChange = (field, value) => {
console.log("languageStatus", languageStatus)
console.log("...lgc_details[languageStatus]", { ...lgc_details[languageStatus] })
// lgc_details
const updatedLgcDetails = {
...lgc_details,
[languageStatus]: { ...lgc_details[languageStatus], [field]: value, lgc: languageStatus }
};
setLgc_details(updatedLgcDetails)
console.log("AAAAAAAAAAAAAA", lgc_details);
};
//
const handleNodeSelect = (_, { node }) => {
setTags([languageLabel])
//
if (selectedNodeid === node.key) return;
const fatherKey = node.key.split('-')[0];
setSelectedCategory(productProject[fatherKey])
console.log("remainderLanguage",remainderLanguage)
let initialQuotationData = null;
let infoData = null;
let lgcDetailsData = null;
productsData[fatherKey].forEach(element => {
if (element.info.title === node.title) {
initialQuotationData = element.quotation;
infoData = element.info;
lgcDetailsData = element.lgc_details;
return true;
}
});
console.log("infoData", infoData)
// lgc_details
let newLgcDetails = {};
lgcDetailsData.forEach(element => {
newLgcDetails[element.lgc] = element;
});
// lgc_details
setLgc_details(newLgcDetails);
setQuotation(initialQuotationData);
console.log("descriptions", lgc_details)
// 使 setTimeout lgc_details
form.setFieldsValue({
info: {
title: infoData.title,
code: infoData.code,
product_type_name: infoData.product_type_name,
city_name: infoData.city_name,
remarks: infoData.remarks,
open_weekdays: infoData.open_weekdays,
recommends_rate: infoData.recommends_rate,
duration: infoData.duration,
dept: infoData.dept,
km: infoData.km,
dept_name: infoData.dept_name
},
lgc_details: {
title: newLgcDetails[language]?.title || '',
descriptions: newLgcDetails[language]?.descriptions || ''
}
});
};
const onSave = (values) => {
const tempData = values;
tempData['quotation'] = quotation;
// tempData['extras'] = bindingData;
// tempData['lgc_details'] = languageStatus;
setSaveData(tempData);
console.log("保存的数据", tempData)
};
return (
<div>
<Row>
<Col span={6}>
<Card style={{ width: "20%", position: "fixed", maxHeight: "80vh", overflowY: "auto" }}>
<Search style={{ marginBottom: 8 }} placeholder="Search" onChange={onChange} />
<Tree
onSelect={handleNodeSelect}
treeData={treeData}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onExpand={onExpand}
titleRender={titleRender}
/>
</Card>
</Col>
<Col span={18}>
<Form form={form} name="control-hooks" onFinish={onSave}>
<Card
style={{ width: "80%" }}
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>供应商</Link> },
{ title: <Link to={'/products'}>综费</Link> },
{ title: '文章列表' }
]} />
}
>
<h2>{t('products:productProject')}</h2>
<Row gutter={16}>
{selectedCategory.map((item, index) => (
<Col span={8} id={index}>
<Form.Item name={['info', item.code]} label={item.name}>
{item.code === "duration" ? (
<Input suffix="H"/>
) : (
<Input />
)}
</Form.Item>
</Col>
))}
</Row>
<Card title={
<div>
{tags.map(tag => (
<Tag
id={tag}
onClick={() => handleTagClick(tag)}
color={tag === selectedTag ? 'blue' : undefined}
style={{ cursor: 'pointer' }}
>
{tag}
</Tag>
))}
<Tag onClick={showModal} style={{ cursor: 'pointer' }}>+</Tag>
</div>
}>
<Modal title="选择语言" visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
<Select
showSearch
style={{ width: "80%" }}
placeholder="选择语言"
optionFilterProp="children"
onChange={handleTagChange}
value={remainderLanguage.some((item) => item.label === selectedTag) ? selectedTag : undefined}
>
{
remainderLanguage.map((value, label) => (
<Select.Option key={value.label} value={value.label}>
{value.label}
</Select.Option>
))
}
</Select>
</Modal>
<Form.Item label={t('products:Name')} name={['lgc_details', 'title']}>
<Input
style={{ width: "30%" }}
onChange={(e) => handleChange('title', e.target.value)}
/>
</Form.Item>
<Form.Item label={t('products:Description')} name={['lgc_details', 'descriptions']}>
<Input.TextArea
rows={4}
onChange={(e) => handleChange('descriptions', e.target.value)}
/>
</Form.Item>
</Card>
</Card>
<Card style={{ width: "80%" }}>
<h2>{t('products:supplierQuotation')}</h2>
<Form.Item name="quotation">
<Table
components={{ body: { cell: EditableCell } }}
bordered
dataSource={quotation}
columns={mergedColumns}
rowClassName="editable-row"
pagination={{ onChange: cancel }}
/>
<Button onClick={handleAdd} type="primary" style={{ marginBottom: 16 }}>
{t('products:addQuotation')}
</Button>
<Button onClick={handleBatchImport} type="primary" style={{ marginBottom: 16 }}>批量添加</Button>
</Form.Item>
</Card>
<Card style={{ width: "80%" }}>
<Extras productId={2} />
</Card>
<Button type="primary" htmlType="submit" style={{ marginTop: 16, float: "right", marginRight: "20%" }}>
{t('products:save')}
</Button>
<Button type="primary" htmlType="submit" style={{ marginTop: 16, float: "right", marginRight: "5%" }}>
提交审核
</Button>
</Form>
</Col>
</Row>
{datePickerVisible && (
<Modal
title="选择日期"
visible={datePickerVisible}
onOk={handleDateOk}
onCancel={() => setDatePickerVisible(false)}
>
<Date onDateChange={handleDateChange} />
</Modal>
)}
{
batchImportPriceVisible && (
<Modal
title="批量添加价格"
visible={batchImportPriceVisible}
onOk={handleBatchImportOK}
onCancel={() => setBatchImportPriceVisible(false)}
width="80%"
>
<BatchImportPrice/>
</Modal>
)
}
</div>
);
}
export default Detail;

@ -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…
Cancel
Save