Merge branch 'main' of github.com:hainatravel/GHHub
commit
d97638b843
Binary file not shown.
@ -0,0 +1,118 @@
|
||||
{
|
||||
"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": "Components"
|
||||
},
|
||||
"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": "Display To C",
|
||||
"Dept": "Dept",
|
||||
|
||||
"productProject": "Product project",
|
||||
"Code": "Code",
|
||||
"City": "City",
|
||||
"Remarks": "Remarks",
|
||||
"tourTime": "Tour time",
|
||||
"recommendationRate": "Recommends rate",
|
||||
"Name": "Name",
|
||||
"Price":"Price",
|
||||
"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"
|
||||
},
|
||||
|
||||
"CopyFormMsg": {
|
||||
"Source": "Source ",
|
||||
"target": "Target ",
|
||||
"requiredVendor": "Please pick a target vendor",
|
||||
"requiredTypes": "Please select product types",
|
||||
"requiredDept": "Please pick a owner department"
|
||||
},
|
||||
|
||||
"quotationTable": {
|
||||
"Adult": "Adult",
|
||||
"Child": "Child",
|
||||
"Currency": "Currency",
|
||||
"Unit": "Unit",
|
||||
"GroupSize": "Group Size",
|
||||
"UseDates": "Use Dates",
|
||||
"Operation": "Operation",
|
||||
"Weekdays": "Weekdays"
|
||||
},
|
||||
|
||||
"#": "Products"
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
{
|
||||
"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": "儿童"
|
||||
},
|
||||
|
||||
"CopyFormMsg": {
|
||||
"Source": "源",
|
||||
"target": "目标",
|
||||
"requiredVendor": "请选择目标供应商",
|
||||
"requiredTypes": "请选择产品类型",
|
||||
"requiredDept": "请选择所属小组"
|
||||
},
|
||||
|
||||
"quotationTable": {
|
||||
"Adult": "成人",
|
||||
"Child": "儿童",
|
||||
"Currency": "币种",
|
||||
"Unit": "类型",
|
||||
"GroupSize": "人等",
|
||||
"UseDates": "使用日期",
|
||||
"Weekdays": "周末",
|
||||
"Operation": "Operation"
|
||||
},
|
||||
|
||||
"#": "产品"
|
||||
}
|
||||
@ -1,23 +1,6 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
.logo {
|
||||
float: left;
|
||||
height: 36px;
|
||||
margin: 16px 24px 16px 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.reservation-highlight {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background-color: rgba(255, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
#error-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
.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,29 @@
|
||||
import { createContext, useEffect, useState } from 'react';
|
||||
import {} from 'antd';
|
||||
import SearchInput from './SearchInput';
|
||||
import { fetchJSON } from '@/utils/request';
|
||||
import { HT_HOST } from '@/config';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
//供应商列表
|
||||
export const fetchCityList = async (q) => {
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/search_cities`, { q });
|
||||
return errcode !== 0 ? [] : result;
|
||||
};
|
||||
|
||||
const CitySelector = ({ ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<SearchInput
|
||||
placeholder={t('products:City')}
|
||||
mode={null}
|
||||
maxTagCount={0}
|
||||
{...props}
|
||||
fetchOptions={fetchCityList}
|
||||
map={{ city_name: 'label', city_id: 'value' }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CitySelector;
|
||||
@ -0,0 +1,60 @@
|
||||
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}
|
||||
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,23 @@
|
||||
import { Select } from 'antd';
|
||||
import { useProductsTypes } from '@/hooks/useProductsSets';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { fetchJSON } from '@/utils/request';
|
||||
import { HT_HOST } from '@/config';
|
||||
|
||||
//供应商列表
|
||||
export const fetchVendorList = async (q) => {
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/VendorList`, { q })
|
||||
return errcode !== 0 ? [] : result
|
||||
}
|
||||
|
||||
const ProductsTypesSelector = ({...props}) => {
|
||||
const productsTypes = useProductsTypes();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Select labelInValue allowClear placeholder={t('products:ProductType')} options={productsTypes} {...props}/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ProductsTypesSelector;
|
||||
@ -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,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: 'muted' },
|
||||
{ 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: 'danger' },
|
||||
{ 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,95 +1,110 @@
|
||||
import React from "react";
|
||||
import { configure } from "mobx";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
} from "react-router-dom";
|
||||
import "@/assets/global.css";
|
||||
import App from "@/views/App";
|
||||
import Standlone from "@/views/Standlone";
|
||||
import Login from "@/views/Login";
|
||||
import Logout from "@/views/Logout";
|
||||
import Index from "@/views/index";
|
||||
import ErrorPage from "@/components/ErrorPage";
|
||||
} from 'react-router-dom'
|
||||
import '@/assets/global.css'
|
||||
import App from '@/views/App'
|
||||
import Standlone from '@/views/Standlone'
|
||||
import Login from '@/views/Login'
|
||||
import Logout from '@/views/Logout'
|
||||
import ErrorPage from '@/components/ErrorPage'
|
||||
import RequireAuth from '@/components/RequireAuth'
|
||||
import ReservationNewest from "@/views/reservation/Newest";
|
||||
import ReservationDetail from "@/views/reservation/Detail";
|
||||
import ChangePassword from "@/views/account/ChangePassword";
|
||||
import AccountProfile from "@/views/account/Profile";
|
||||
import AccountManagement from "@/views/account/Management";
|
||||
import RoleList from "@/views/account/RoleList";
|
||||
import FeedbackIndex from "@/views/feedback/Index";
|
||||
import FeedbackDetail from "@/views/feedback/Detail";
|
||||
import FeedbackCustomerDetail from "@/views/feedback/CustomerDetail";
|
||||
import ReportIndex from "@/views/report/Index";
|
||||
import NoticeIndex from "@/views/notice/Index";
|
||||
import NoticeDetail from "@/views/notice/Detail";
|
||||
import InvoiceIndex from "@/views/invoice/Index";
|
||||
import InvoiceDetail from "@/views/invoice/Detail";
|
||||
import InvoicePaid from "@/views/invoice/Paid";
|
||||
import InvoicePaidDetail from "@/views/invoice/PaidDetail";
|
||||
import Airticket from "@/views/airticket/Index";
|
||||
import AirticketPlan from "@/views/airticket/Plan";
|
||||
import ReservationNewest from '@/views/reservation/Newest'
|
||||
import ReservationDetail from '@/views/reservation/Detail'
|
||||
import ChangePassword from '@/views/account/ChangePassword'
|
||||
import AccountProfile from '@/views/account/Profile'
|
||||
import AccountManagement from '@/views/account/Management'
|
||||
import RoleList from '@/views/account/RoleList'
|
||||
import FeedbackIndex from '@/views/feedback/Index'
|
||||
import FeedbackDetail from '@/views/feedback/Detail'
|
||||
import FeedbackCustomerDetail from '@/views/feedback/CustomerDetail'
|
||||
import ReportIndex from '@/views/report/Index'
|
||||
import NoticeIndex from '@/views/notice/Index'
|
||||
import NoticeDetail from '@/views/notice/Detail'
|
||||
import InvoiceIndex from '@/views/invoice/Index'
|
||||
import InvoiceDetail from '@/views/invoice/Detail'
|
||||
import InvoicePaid from '@/views/invoice/Paid'
|
||||
import InvoicePaidDetail from '@/views/invoice/PaidDetail'
|
||||
import Airticket from '@/views/airticket/Index'
|
||||
import AirticketPlan from '@/views/airticket/Plan'
|
||||
import { ThemeContext } from '@/stores/ThemeContext'
|
||||
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, PERM_PRODUCTS_OFFER_PUT } from '@/config'
|
||||
|
||||
import './i18n';
|
||||
import './i18n'
|
||||
|
||||
configure({
|
||||
useProxies: "ifavailable",
|
||||
enforceActions: "observed",
|
||||
computedRequiresReaction: true,
|
||||
observableRequiresReaction: false,
|
||||
reactionRequiresObservable: true,
|
||||
disableErrorBoundaries: process.env.NODE_ENV == "production"
|
||||
});
|
||||
const { createRoot } = ReactDOM
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <App />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{ index: true, element: <Index /> },
|
||||
{ path: "account/change-password", element: <ChangePassword />},
|
||||
{ path: "account/profile", element: <AccountProfile />},
|
||||
{ path: "account/management", element: <RequireAuth subject={PERM_ACCOUNT_MANAGEMENT} result={true}><AccountManagement /></RequireAuth>},
|
||||
{ path: "account/role-list", element: <RequireAuth subject={PERM_ROLE_NEW} result={true}><RoleList /></RequireAuth>},
|
||||
{ path: "reservation/newest", element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationNewest /></RequireAuth>},
|
||||
{ path: "reservation/:reservationId", element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationDetail /></RequireAuth>},
|
||||
{ path: "feedback", element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackIndex /></RequireAuth>},
|
||||
{ path: "feedback/:GRI_SN/:CII_SN/:RefNo", element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackCustomerDetail /></RequireAuth>},
|
||||
{ path: "feedback/:GRI_SN/:RefNo", element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackDetail /></RequireAuth>},
|
||||
{ path: "report", element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReportIndex /></RequireAuth>},
|
||||
{ path: "notice", element: <RequireAuth subject={PERM_OVERSEA} result={true}><NoticeIndex /></RequireAuth>},
|
||||
{ path: "notice/:CCP_BLID", element: <RequireAuth subject={PERM_OVERSEA} result={true}><NoticeDetail /></RequireAuth>},
|
||||
{ path: "invoice",element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceIndex /></RequireAuth>},
|
||||
{ path: "invoice/detail/:GMDSN/:GSN",element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceDetail /></RequireAuth>},
|
||||
{ path: "invoice/paid",element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaid /></RequireAuth>},
|
||||
{ path: "invoice/paid/detail/:flid", element: <RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaidDetail /></RequireAuth>},
|
||||
{ path: "airticket",element: <RequireAuth subject={PERM_AIR_TICKET} result={true}><Airticket /></RequireAuth>},
|
||||
{ path: "airticket/plan/:coli_sn",element:<AirticketPlan />},
|
||||
]
|
||||
},
|
||||
{
|
||||
element: <Standlone />,
|
||||
children: [
|
||||
{ path: "/login", element: <Login /> },
|
||||
{ path: "/logout", element: <Logout /> },
|
||||
]
|
||||
const initRouter = async () => {
|
||||
return createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <App />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{ index: true, element: <NoticeIndex /> },
|
||||
{ path: 'account/change-password', element: <ChangePassword />},
|
||||
{ path: 'account/profile', element: <AccountProfile />},
|
||||
{ path: 'account/management', element: <RequireAuth subject={PERM_ACCOUNT_MANAGEMENT} result={true}><AccountManagement /></RequireAuth>},
|
||||
{ path: 'account/role-list', element: <RequireAuth subject={PERM_ROLE_NEW} result={true}><RoleList /></RequireAuth>},
|
||||
{ path: 'reservation/newest', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationNewest /></RequireAuth>},
|
||||
{ path: 'reservation/:reservationId', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationDetail /></RequireAuth>},
|
||||
{ path: 'feedback', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackIndex /></RequireAuth>},
|
||||
{ path: 'feedback/:GRI_SN/:CII_SN/:RefNo', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackCustomerDetail /></RequireAuth>},
|
||||
{ path: 'feedback/:GRI_SN/:RefNo', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackDetail /></RequireAuth>},
|
||||
{ path: 'report', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReportIndex /></RequireAuth>},
|
||||
{ path: 'notice', element: <NoticeIndex />},
|
||||
{ path: 'notice/:CCP_BLID', element: <NoticeDetail />},
|
||||
{ path: 'invoice',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceIndex /></RequireAuth>},
|
||||
{ path: 'invoice/detail/:GMDSN/:GSN',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceDetail /></RequireAuth>},
|
||||
{ path: 'invoice/paid',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaid /></RequireAuth>},
|
||||
{ path: 'invoice/paid/detail/:flid', element: <RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaidDetail /></RequireAuth>},
|
||||
{ path: 'airticket',element: <RequireAuth subject={PERM_AIR_TICKET} result={true}><Airticket /></RequireAuth>},
|
||||
{ path: 'airticket/plan/:coli_sn',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketPlan /></RequireAuth>},
|
||||
{ path: "products",element: <RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsManage /></RequireAuth>},
|
||||
{ path: "products/:travel_agency_id/:use_year/:audit_state/audit",element:<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsAudit /></RequireAuth>},
|
||||
{ path: "products/:travel_agency_id/:use_year/:audit_state/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
|
||||
{ path: "products/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
|
||||
]
|
||||
},
|
||||
{
|
||||
element: <Standlone />,
|
||||
children: [
|
||||
{ path: '/login', element: <Login /> },
|
||||
{ path: '/logout', element: <Logout /> },
|
||||
]
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
const initAppliction = async () => {
|
||||
|
||||
const { loginToken, userId } = usingStorage()
|
||||
|
||||
if (isNotEmpty(userId) && isNotEmpty(loginToken)) {
|
||||
await useAuthStore.getState().initAuth()
|
||||
}
|
||||
]);
|
||||
|
||||
const router = await initRouter()
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
//<React.StrictMode>
|
||||
<ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}>
|
||||
<RouterProvider
|
||||
router={router}
|
||||
fallbackElement={() => <div>Loading...</div>}
|
||||
/>
|
||||
</ThemeContext.Provider>
|
||||
//</React.StrictMode>
|
||||
)
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
//<React.StrictMode>
|
||||
<ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}>
|
||||
<RouterProvider
|
||||
router={router}
|
||||
fallbackElement={() => <div>Loading...</div>}
|
||||
/>
|
||||
</ThemeContext.Provider>
|
||||
//</React.StrictMode>
|
||||
);
|
||||
initAppliction()
|
||||
|
||||
@ -0,0 +1,145 @@
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索所有产品, 返回产品列表
|
||||
* ! 只有审核通过, 已发布的
|
||||
* @param {object} params { keyword, use_year, product_types, travel_agency_id, city }
|
||||
*/
|
||||
export const searchPublishedProductsAction = async (param) => {
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/web_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}/Service_BaseInfoWeb/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) => {
|
||||
return true; // test: 先不更新到HT
|
||||
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_add`, body);
|
||||
return errcode === 0 ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const delProductExtrasAction = async (body) => {
|
||||
return true; // test: 先不更新到HT
|
||||
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: {},
|
||||
editingProduct: {},
|
||||
};
|
||||
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 }),
|
||||
setEditingProduct: (editingProduct) => set({ editingProduct }),
|
||||
|
||||
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,45 @@
|
||||
const initListener = []
|
||||
const authListener = []
|
||||
|
||||
export const addInitLinstener = (fn) => {
|
||||
initListener.push(fn)
|
||||
}
|
||||
|
||||
export const addAuthLinstener = (fn) => {
|
||||
authListener.push(fn)
|
||||
}
|
||||
|
||||
export const notifyInit = async () => {
|
||||
for (const listener of initListener) {
|
||||
await listener()
|
||||
}
|
||||
}
|
||||
|
||||
export const notifyAuth = async (obj) => {
|
||||
for (const listener of authListener) {
|
||||
await listener(obj)
|
||||
}
|
||||
}
|
||||
|
||||
// Zustand 中间件,用于订阅前端应用的生命周期,实验阶段。
|
||||
// 失败,无法同步调用异步方法!
|
||||
export const lifecycleware = (fn) => (set, get, store) => {
|
||||
|
||||
addInitLinstener(() => {
|
||||
if (store.getState().hasOwnProperty('onInit')) {
|
||||
store.getState().onInit()
|
||||
} else {
|
||||
console.info('store has no function: onInit.')
|
||||
}
|
||||
})
|
||||
|
||||
addAuthLinstener(() => {
|
||||
if (store.getState().hasOwnProperty('onAuth')) {
|
||||
store.getState().onAuth()
|
||||
} else {
|
||||
console.info('store has no function: onAuth.')
|
||||
}
|
||||
})
|
||||
|
||||
return fn(set, get, store)
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
export default function Index() {
|
||||
return (
|
||||
<p id="zero-state">
|
||||
Global Highlights Hub
|
||||
<br />
|
||||
Check out{" "}
|
||||
<a href="https://www.chinahighlights.com">
|
||||
the docs at chinahighlights.com
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,247 @@
|
||||
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 { cloneDeep, isEmpty } from '@/utils/commons';
|
||||
import useAuthStore from '@/stores/Auth';
|
||||
import RequireAuth from '@/components/RequireAuth';
|
||||
// import PrintContractPDF from './PrintContractPDF';
|
||||
import { PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from '@/config';
|
||||
|
||||
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> */}
|
||||
<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT}>
|
||||
<Link className='px-2' to={`/products/${travel_agency_id}/${use_year}/${audit_state}/edit`}>
|
||||
{t('Edit')}
|
||||
</Link>
|
||||
</RequireAuth>
|
||||
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
|
||||
<Button size='small' type={'primary'} onClick={() => handleAuditItem('2', agency)}>
|
||||
{t('products:auditStateAction.Published')}
|
||||
</Button>
|
||||
</RequireAuth>
|
||||
{/* <Button size='small' type={'primary'} ghost onClick={() => handleAuditItem('2', agency)}>
|
||||
{t('products:auditStateAction.Approved')}
|
||||
</Button> */}
|
||||
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
|
||||
<Button size='small' type={'primary'} danger ghost onClick={() => handleAuditItem('3', agency)}>
|
||||
{t('products:auditStateAction.Rejected')}
|
||||
</Button>
|
||||
</RequireAuth>
|
||||
{/* todo: export, 审核完成之后才能导出 */}
|
||||
<Button size='small'>{t('Print')} PDF</Button>
|
||||
{/* <PrintContractPDF /> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PriceTable = ({ productType, dataSource, refresh }) => {
|
||||
const { t } = useTranslation('products');
|
||||
const { travel_agency_id, use_year, audit_state } = useParams();
|
||||
const isPermitted = useAuthStore(state => state.isPermitted);
|
||||
const [loading, activeAgency] = useProductsStore((state) => [state.loading, state.activeAgency]);
|
||||
const [setEditingProduct] = useProductsStore((state) => [state.setEditingProduct]);
|
||||
const { message, notification } = App.useApp();
|
||||
const stateMapVal = useProductsAuditStatesMapVal();
|
||||
|
||||
const [renderData, setRenderData] = useState(dataSource);
|
||||
|
||||
// console.log(dataSource);
|
||||
|
||||
const handleAuditPriceItem = (state, row, rowIndex) => {
|
||||
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(); // debug: 不要刷新, 等太久
|
||||
// const newData = structuredClone(renderData);
|
||||
const newData = cloneDeep(renderData);
|
||||
newData.splice(rowIndex, 1, {...row, audit_state_id: state, });
|
||||
setRenderData(newData);
|
||||
}
|
||||
}
|
||||
})
|
||||
.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) => {
|
||||
const title = text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || '';
|
||||
return isPermitted(PERM_PRODUCTS_OFFER_PUT) ? <Link to={`/products/${travel_agency_id}/${use_year}/${audit_state}/edit`} onClick={() => setEditingProduct(r.info)}>{title}</Link> : 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) => {
|
||||
const stateCls = ` text-${stateMapVal[`${r.audit_state_id}`]?.color} `;
|
||||
return <span className={stateCls}>{stateMapVal[`${r.audit_state_id}`]?.label}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'action',
|
||||
render: (_, r, ri) =>
|
||||
(Number(r.audit_state_id)) === 0 ? (
|
||||
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
|
||||
<Space>
|
||||
<Button onClick={() => handleAuditPriceItem('2', r, ri)}>✔</Button>
|
||||
<Button onClick={() => handleAuditPriceItem('3', r, ri)}>✖</Button>
|
||||
</Space>
|
||||
</RequireAuth>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
return <Table size={'small'} className='border-collapse' rowHoverable={false} rowClassName={rowStyle} pagination={false} {...{ columns, }} dataSource={renderData} 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;
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,160 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { App, Form, Modal, DatePicker, Divider } from 'antd';
|
||||
import { isEmpty, objectMapper } from '@/utils/commons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SearchInput from '@/components/SearchInput';
|
||||
import DeptSelector from '@/components/DeptSelector';
|
||||
import ProductsTypesSelector, { fetchVendorList } from '@/components/ProductsTypesSelector';
|
||||
import dayjs from 'dayjs';
|
||||
import arraySupport from 'dayjs/plugin/arraySupport';
|
||||
import { copyAgencyDataAction } from '@/stores/Products/Index';
|
||||
|
||||
import useAuthStore from '@/stores/Auth';
|
||||
import RequireAuth from '@/components/RequireAuth';
|
||||
import { PERM_PRODUCTS_MANAGEMENT } from '@/config';
|
||||
|
||||
dayjs.extend(arraySupport);
|
||||
|
||||
export const CopyProductsForm = ({ action, initialValues, onFormInstanceReady, source, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const isPermitted = useAuthStore((state) => state.isPermitted);
|
||||
|
||||
useEffect(() => {
|
||||
onFormInstanceReady(form);
|
||||
}, []);
|
||||
|
||||
const onValuesChange = (changeValues, allValues) => {};
|
||||
return (
|
||||
<Form layout='horizontal' form={form} name='form_in_modal' initialValues={initialValues} onValuesChange={onValuesChange}>
|
||||
{action === '#' && <Form.Item name='agency' label={`${t('products:CopyFormMsg.target')}${t('products:Vendor')}`} rules={[{ required: true, message: t('products:CopyFormMsg.requiredVendor') }]}>
|
||||
<SearchInput
|
||||
placeholder={t('products:Vendor')}
|
||||
mode={null}
|
||||
maxTagCount={0}
|
||||
fetchOptions={fetchVendorList}
|
||||
map={{ travel_agency_name: 'label', travel_agency_id: 'value' }}
|
||||
/>
|
||||
</Form.Item>}
|
||||
<Form.Item name={`products_types`} label={t('products:ProductType')}>
|
||||
<ProductsTypesSelector maxTagCount={1} mode={'multiple'} placeholder={t('All')} />
|
||||
</Form.Item>
|
||||
{action === '#' && <RequireAuth subject={PERM_PRODUCTS_MANAGEMENT}>
|
||||
<Form.Item name={`dept`} label={t('products:Dept')} rules={[{ required: false, message: t('products:CopyFormMsg.requiredDept') }]}>
|
||||
<DeptSelector isLeaf={true} />
|
||||
</Form.Item>
|
||||
</RequireAuth>}
|
||||
<Form.Item name={'source_use_year'} label={`${t('products:CopyFormMsg.Source')}${t('products:UseYear')}`} initialValue={dayjs([source.sourceYear, 1, 1])}>
|
||||
<DatePicker picker='year' allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item name={'target_use_year'} label={`${t('products:CopyFormMsg.target')}${t('products:UseYear')}`}>
|
||||
<DatePicker picker='year' allowClear disabledDate={(current) => current <= dayjs([source.sourceYear, 12, 31])} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
const formValuesMapper = (values) => {
|
||||
const destinationObject = {
|
||||
'agency': {
|
||||
key: 'target_agency',
|
||||
transform: (value) => {
|
||||
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
|
||||
},
|
||||
},
|
||||
'source_use_year': [{ key: 'source_use_year', transform: (arrVal) => (arrVal ? arrVal.format('YYYY') : '') }],
|
||||
'target_use_year': [{ key: 'target_use_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) => {
|
||||
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
let dest = {};
|
||||
const { agency, year, ...omittedValue } = values;
|
||||
dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
|
||||
for (const key in dest) {
|
||||
if (Object.prototype.hasOwnProperty.call(dest, key)) {
|
||||
dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : dest[key];
|
||||
}
|
||||
}
|
||||
// omit empty
|
||||
// Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]);
|
||||
return dest;
|
||||
};
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const CopyProductsFormModal = ({ source, action = '#' | 'o', open, onSubmit, onCancel, initialValues, loading, copyModalVisible, setCopyModalVisible }) => {
|
||||
const { t } = useTranslation();
|
||||
const { notification, message } = App.useApp();
|
||||
const [formInstance, setFormInstance] = useState();
|
||||
|
||||
const [copyLoading, setCopyLoading] = useState(false);
|
||||
const handleCopyAgency = async (param) => {
|
||||
param.target_agency = isEmpty(param.target_agency) ? source.sourceAgency.travel_agency_id : param.target_agency;
|
||||
setCopyLoading(true);
|
||||
console.log(param);
|
||||
const toID = param.target_agency;
|
||||
const success = await copyAgencyDataAction({...param, source_agency: source.sourceAgency.travel_agency_id});
|
||||
setCopyLoading(false);
|
||||
success ? message.success(t('Success')) : message.error(t('Failed'));
|
||||
|
||||
if (typeof onSubmit === 'function') {
|
||||
onSubmit(param);
|
||||
}
|
||||
// setCopyModalVisible(false);
|
||||
// navigate(`/products/${toID}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`);
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
width={600}
|
||||
open={open}
|
||||
title={`${t('Copy')}${t('products:#')}${t('products:Offer')}`}
|
||||
okText='确认'
|
||||
// cancelText='Cancel'
|
||||
okButtonProps={{
|
||||
autoFocus: true,
|
||||
}}
|
||||
confirmLoading={copyLoading}
|
||||
onCancel={() => {
|
||||
onCancel();
|
||||
formInstance?.resetFields();
|
||||
}}
|
||||
destroyOnClose
|
||||
onOk={async () => {
|
||||
try {
|
||||
const values = await formInstance?.validateFields();
|
||||
// formInstance?.resetFields();
|
||||
const dest = formValuesMapper(values);
|
||||
handleCopyAgency(dest);
|
||||
} catch (error) {
|
||||
console.log('Failed:', error);
|
||||
}
|
||||
}}>
|
||||
<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT}>
|
||||
<div className='py-2'>
|
||||
{t('products:CopyFormMsg.Source')}: {source.sourceAgency.travel_agency_name}
|
||||
<Divider type={'vertical'} />
|
||||
{source.sourceYear}
|
||||
</div>
|
||||
</RequireAuth>
|
||||
<CopyProductsForm action={action}
|
||||
source={source}
|
||||
initialValues={initialValues}
|
||||
onFormInstanceReady={(instance) => {
|
||||
setFormInstance(instance);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default CopyProductsFormModal;
|
||||
@ -0,0 +1,176 @@
|
||||
import { useEffect, useState, useSyncExternalStore } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { App, Table, Button, Modal, Popconfirm } from 'antd';
|
||||
import { getAgencyProductExtrasAction, searchPublishedProductsAction, 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, year, ...param } = copyObject;
|
||||
setSearchLoading(true);
|
||||
setSearchResult([]);
|
||||
const search_year = year || use_year;
|
||||
const result = await searchPublishedProductsAction({ ...param, use_year: search_year, });
|
||||
setSearchResult(result);
|
||||
setSearchLoading(false);
|
||||
};
|
||||
const handleAddExtras = async (item) => {
|
||||
if (typeof onPick === 'function') {
|
||||
onPick(item);
|
||||
}
|
||||
};
|
||||
|
||||
const searchResultColumns = [
|
||||
{ key: 'ptype', dataIndex: 'type', width: '6rem', title: t('products:ProductType'), render: (text, r) => productsTypesMapVal[text]?.label || text },
|
||||
{ key: 'code', dataIndex: 'code', width: '6rem', title: t('products:Code') },
|
||||
{ key: 'title', dataIndex: '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: [ 'year', 'keyword', 'products_types', 'city'], // 'dates',
|
||||
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'}
|
||||
rowKey={'id'}
|
||||
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, ['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.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}`,
|
||||
// },
|
||||
// { 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,55 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { DatePicker, Button } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { useDatePresets } from '@/hooks/useDatePresets';
|
||||
const addValidityWithWeekend = ({ onDateChange }) => {
|
||||
const dateFormat = 'YYYY/MM/DD';
|
||||
const { RangePicker } = DatePicker;
|
||||
const [dateRange, setDateRange] = useState(null);
|
||||
const [selectedDays, setSelectedDays] = useState([]);
|
||||
const presets = useDatePresets();
|
||||
const days = [
|
||||
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
|
||||
];
|
||||
|
||||
|
||||
const handleChange = (date, dateString) => {
|
||||
console.log("dateString",dateString)
|
||||
onDateChange({ dateRange: dateString, 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 allowClear={true} inputReadOnly={true} presets={presets} placeholder={['From', 'Thru']} 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 addValidityWithWeekend;
|
||||
@ -0,0 +1,118 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { App, Space, Table, Button, Modal, Divider } from 'antd';
|
||||
import SearchForm from '@/components/SearchForm';
|
||||
import dayjs from 'dayjs';
|
||||
import arraySupport from 'dayjs/plugin/arraySupport';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useProductsStore, { copyAgencyDataAction } from '@/stores/Products/Index';
|
||||
import useFormStore from '@/stores/Form';
|
||||
import { objectMapper } from '@/utils/commons';
|
||||
import CopyProductsFormModal from './Detail/CopyProducts';
|
||||
|
||||
dayjs.extend(arraySupport);
|
||||
|
||||
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 useYear = formValuesToSub.year;
|
||||
|
||||
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 [copyAction, setCopyAction] = useState();
|
||||
const [copyLoading, setCopyLoading] = useState(false);
|
||||
const handleCopyAgency = async (param) => {
|
||||
// setCopyLoading(true);
|
||||
const toID = param.target_agency;
|
||||
// const success = await copyAgencyDataAction({...param, 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, action) => {
|
||||
setSourceAgency(from);
|
||||
setCopyAction(action);
|
||||
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: 'poster_by', dataIndex: 'poster_name' },
|
||||
{ title: t('products:CreateDate'), key: 'poster_date', dataIndex: 'poster_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' size={'small'} onClick={() => openCopyModal(r, '#')}>{t('Copy')+'-'+t('products:#')}</Button>
|
||||
<Button type='link' size={'small'} onClick={() => openCopyModal(r, 'o')}>{t('Copy')+'-'+t('products:Offer')}</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 },
|
||||
year: { col: 4, rules: [{ required: true }] },
|
||||
},
|
||||
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'} />
|
||||
|
||||
{/* 复制弹窗 */}
|
||||
<CopyProductsFormModal
|
||||
open={copyModalVisible}
|
||||
action={copyAction}
|
||||
source={{ sourceAgency, sourceYear: useYear }}
|
||||
onCancel={() => setCopyModalVisible(false)}
|
||||
onSubmit={(formVal) => {
|
||||
handleCopyAgency(formVal);
|
||||
}}
|
||||
{...{copyModalVisible, setCopyModalVisible}}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index;
|
||||
Loading…
Reference in New Issue