1.完善表格显示内容

2.修改供应商报价表格的对应字段
feature/price_manager
黄文强@HWQ-PC 1 year ago
commit c843fab969

@ -10,6 +10,7 @@
"Confirm": "Confirm",
"Close": "Close",
"Save": "Save",
"New": "New",
"Edit": "Edit",
"Audit": "Audit",
"Delete": "Delete",
@ -25,6 +26,12 @@
"Export": "Export",
"Copy": "Copy",
"sureCancel": "Sure you want to cancel?",
"sureDelete":"Sure you want to delete?",
"Success": "Success",
"Failed": "Failed",
"Table": {
"Total": "Total {{total}} items"
},
@ -54,6 +61,24 @@
"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",

@ -3,13 +3,18 @@
"Experience": "Experience",
"Car": "Transport Services",
"Guide": "Guide Services",
"Package": "Package",
"Package": "Package Tour",
"Attractions": "Attractions",
"Meals": "Meals",
"Extras": "Extras",
"Overtravel": "超公里",
"Special": "Special"
},
"EditComponents": {
"info": "Product Information",
"Quotation": "Quotation",
"Extras": "Add-on"
},
"auditState": {
"New": "New",
"Pending": "Pending",
@ -58,10 +63,31 @@
"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?"
"sureDelete":"Sure you want to delete?",
"#": "#"
}

@ -10,6 +10,7 @@
"Confirm": "确认",
"Close": "关闭",
"Save": "保存",
"New": "新增",
"Edit": "编辑",
"Audit": "审核",
"Delete": "删除",
@ -25,6 +26,12 @@
"Export": "导出",
"Copy": "复制",
"sureCancel": "确定取消?",
"sureDelete":"确定删除?",
"Success": "成功",
"Failed": "失败",
"Table": {
"Total": "共 {{total}} 条"
},
@ -54,6 +61,24 @@
"lastThreeMonth": "前三个月",
"thisYear": "今年"
},
"weekdays": {
"1": "一",
"2": "二",
"3": "三",
"4": "四",
"5": "五",
"6": "六",
"7": "日"
},
"weekdaysShort": {
"1": "一",
"2": "二",
"3": "三",
"4": "四",
"5": "五",
"6": "六",
"7": "日"
},
"menu": {
"Reservation": "团预订",
"Invoice": "账单",

@ -10,6 +10,11 @@
"Overtravel": "超公里",
"Special": "特殊项目"
},
"EditComponents": {
"info": "产品信息",
"Quotation": "报价",
"Extras": "附加项目"
},
"auditState": {
"New": "新增",
"Pending": "待审核",
@ -37,23 +42,6 @@
"AuditedBy": "审核人员",
"AuditDate": "审核时间",
"Quotation": "报价",
"Offer": "报价",
"Unit": "单位",
"GroupSize": "人等",
"UseDates": "使用日期",
"Weekdays": "有效日/周X",
"UseYear": "年份",
"AgeType": {
"Type": "人群",
"Adult": "成人",
"Child": "儿童"
},
"#": "#",
"productProject": "产品项目",
"Code": "代码",
"City": "城市",
@ -76,10 +64,31 @@
"validityPeriod":"有效期",
"operation": "操作",
"price": "价格",
"Quotation": "报价",
"Offer": "报价",
"Unit": "单位",
"GroupSize": "人等",
"UseDates": "使用日期",
"Weekdays": "有效日/周X",
"OnWeekdays": "周: ",
"Unlimited": "不限",
"UseYear": "年份",
"AgeType": {
"Type": "人群",
"Adult": "成人",
"Child": "儿童"
},
"save":"保存",
"edit":"编辑",
"cancel":"取消",
"delete":"删除",
"cancel":"取消",
"sureCancel": "确定取消?",
"sureDelete":"确定删除?"
"sureDelete":"确定删除?",
"#": "#"
}

@ -21,7 +21,7 @@ export const fetchVendorList = async (q) => {
const { RangePicker } = DatePicker;
const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
const SearchForm = ({ initialValue, onSubmit, onReset, confirmText, formName, loading, ...props }) => {
const { t } = useTranslation();
const presets = useDatePresets();
const [formValues, setFormValues] = useFormStore((state) => [state.formValues, state.setFormValues]);
@ -36,7 +36,6 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
shows: [],
...props.fieldsConfig,
};
const { confirmText } = props;
const readValues = { ...initialValue, ...formValues };
const formValuesMapper = (values) => {
@ -109,14 +108,14 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
};
return (
<>
<Form form={form} name='advanced_search' className='orders-search-form' onFinish={onFinish} onValuesChange={onValuesChange}>
<Form form={form} name={formName || 'advanced_search'} className='orders-search-form' onFinish={onFinish} onValuesChange={onValuesChange}>
{/* <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}>

@ -31,7 +31,7 @@ function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
showSearch
allowClear
maxTagCount={1}
dropdownStyle={{width: '16rem'}}
dropdownStyle={{width: '20rem'}}
{...props}
onSearch={debounceFetcher}
notFoundContent={fetching ? <Spin size='small' /> : null}

@ -1,27 +1,30 @@
import { Outlet, useNavigate } from 'react-router-dom';
import { Layout, Flex, theme } from 'antd';
import { Layout, Flex, theme, Spin, Divider } from 'antd';
import BackBtn from './BackBtn';
const { Content, Header } = Layout;
const HeaderWrapper = ({ children, header, ...props }) => {
const HeaderWrapper = ({ children, header, loading, ...props }) => {
const navigate = useNavigate();
const {
token: { colorBgContainer },
} = theme.useToken();
return (
<>
<Layout className=''>
<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>
<div className='grow h-full'>{header}</div>
<BackBtn />
</Flex>
</Header>
<Divider className='my-2' />
<Content className='' style={{ backgroundColor: colorBgContainer }}>
{children || <Outlet />}
</Content>
</Layout>
</Spin>
</>
);
};

@ -49,7 +49,7 @@ export const useProductsTypes = () => {
{ label: t('products:type.Overtravel'), 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.Package'), value: 'D', key: 'D' },
{ label: t('products:type.Package'), value: 'D', key: 'D' }, // 包价线路
{ label: t('products:type.Attractions'), value: '7', key: '7' },
{ label: t('products:type.Meals'), value: 'C', key: 'C' },
{ label: t('products:type.Extras'), value: '8', key: '8' },
@ -67,11 +67,11 @@ export const useProductsAuditStates = () => {
useEffect(() => {
const newData = [
{ key: '-1', value: '-1', label: t('products:auditState.New') },
{ key: '0', value: '0', label: t('products:auditState.Pending') },
{ key: '2', value: '2', label: t('products:auditState.Approved') },
{ key: '3', value: '3', label: t('products:auditState.Rejected') },
{ key: '1', value: '1', label: t('products:auditState.Published') },
{ 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);

@ -32,10 +32,10 @@ import Airticket from "@/views/airticket/Index";
import AirticketPlan from "@/views/airticket/Plan";
import { ThemeContext } from '@/stores/ThemeContext'
import ProductsIndex from '@/views/products/Index';
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 } from '@/config'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config'
import './i18n';
@ -64,8 +64,8 @@ const router = createBrowserRouter([
{ 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 />},
{ path: "products",element:<ProductsIndex />},
{ path: "products/:travel_agency_id/:use_year/:audit_state/audit",element:<ProductsAudit />},
{ 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:<ProductsDetail />},
]
},

@ -1,7 +1,7 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { fetchJSON, postForm } from '@/utils/request';
import { fetchJSON, postForm, postJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
import { groupBy } from '@/utils/commons';
@ -9,13 +9,50 @@ 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 (from, to) => {
const postbody = { source_agency: from, target_agency: to };
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 postProductsQuoteAudit = async (auditState, quoteRow) => {
/**
* todo:
*/
export const addProductExtraAction = async (body) => {
const { errcode, result } = await postJSON(`${HT_HOST}/products/extras`, body);
return errcode === 0 ? true : false;
};
/**
* todo:
*/
export const delProductExtrasAction = async (body) => {
const { errcode, result } = await postJSON(`${HT_HOST}/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,
@ -30,7 +67,7 @@ export const postProductsQuoteAudit = async (auditState, quoteRow) => {
// return errcode !== 0 ? {} : result;
};
export const postProductsAudit = async (auditState, infoRow) => {
export const postProductsAuditAction = async (auditState, infoRow) => {
const postbody = {
audit_state: auditState,
id: infoRow.id,
@ -59,11 +96,11 @@ export const useProductsStore = create(
...initialState,
// state actions
setLoading: loading => set({ loading }),
setSearchValues: searchValues => set({ searchValues }),
setLoading: (loading) => set({ loading }),
setSearchValues: (searchValues) => set({ searchValues }),
setAgencyList: (agencyList) => set({ agencyList }),
setActiveAgency: activeAgency => set({ activeAgency }),
setAgencyProducts: agencyProducts => set({ agencyProducts }),
setActiveAgency: (activeAgency) => set({ activeAgency }),
setAgencyProducts: (agencyProducts) => set({ agencyProducts }),
reset: () => set(initialState),
@ -80,12 +117,16 @@ export const useProductsStore = create(
const { setLoading, setActiveAgency, setAgencyProducts } = get();
setLoading(true);
const res = await getAgencyProductsAction(param);
const productsData = groupBy(res.products, row => row.info.product_type_id);
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: {

@ -1,18 +1,20 @@
import { useEffect, useState } from 'react';
import { useParams, } from 'react-router-dom';
import { App, Button, Collapse, Table, Space, } from 'antd';
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, { postProductsQuoteAudit } from '@/stores/Products/Index';
import useProductsStore, { postProductsQuoteAuditAction, } from '@/stores/Products/Index';
import { isEmpty } from '@/utils/commons';
// import PrintContractPDF from './PrintContractPDF';
const Header = ({ title, agency, refresh, ...props}) => {
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 [activeAgency] = useProductsStore((state) => [state.activeAgency]);
const { message, notification } = App.useApp();
const handleAuditItem = (state, row) => {
postProductsQuoteAudit(state, {id: row.id, travel_agency_id: activeAgency.travel_agency_id})
postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id })
.then((json) => {
if (json.errcode === 0) {
message.success(json.errmsg);
@ -33,10 +35,11 @@ const Header = ({ title, agency, refresh, ...props}) => {
return (
<div className='flex justify-end items-center gap-4 h-full'>
<div className='grow'>
<h2 className='m-0 leading-tight'>{title}</h2>
<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>
@ -46,20 +49,21 @@ const Header = ({ title, agency, refresh, ...props}) => {
<Button size='small' type={'primary'} danger ghost onClick={() => handleAuditItem('3', agency)}>
{t('products:auditStateAction.Rejected')}
</Button>
{/* todo: export */}
<Button size='small'>{t('Export')} PDF</Button>
{/* todo: export, 审核完成之后才能导出 */}
<Button size='small'>{t('Print')} PDF</Button>
{/* <PrintContractPDF /> */}
</div>
);
};
const PriceTable = ({dataSource,refresh}) => {
const PriceTable = ({ dataSource, refresh }) => {
const { t } = useTranslation('products');
const [loading, activeAgency, ] = useProductsStore((state) => [state.loading, state.activeAgency, ]);
const [loading, activeAgency] = useProductsStore((state) => [state.loading, state.activeAgency]);
const { message, notification } = App.useApp();
const stateMapVal = useProductsAuditStatesMapVal();
const handleAuditPriceItem = (state, row) => {
postProductsQuoteAudit(state, {id: row.id, travel_agency_id: activeAgency.travel_agency_id})
postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id })
.then((json) => {
if (json.errcode === 0) {
message.success(json.errmsg);
@ -79,7 +83,7 @@ const PriceTable = ({dataSource,refresh}) => {
};
const columns = [
{ key: 'title', dataIndex: ['info', 'title'], title: t('Title'), onCell: (r, index) => ({ rowSpan: r.rowSpan }) },
{ key: 'title', dataIndex: ['info', 'title'], width: '16rem', title: t('Title'), onCell: (r, index) => ({ rowSpan: r.rowSpan }) },
{ key: 'adult', title: t('AgeType.Adult'), render: (_, { value, currency, unit_name }) => `${value} ${currency} / ${unit_name}` },
{ key: 'child', title: t('AgeType.Child'), render: (_, { value, currency, unit_name }) => `${value} ${currency} / ${unit_name}` },
// {key: 'price', title: t('Currency'), },
@ -95,20 +99,21 @@ const PriceTable = ({dataSource,refresh}) => {
key: 'useDates',
dataIndex: ['use_dates_start'],
title: t('UseDates'),
render: (_, { use_dates_start, use_dates_end }) => `${use_dates_start} ~ ${use_dates_end}`,
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') },
{ key: 'weekdays', dataIndex: ['weekdays'], title: t('Weekdays'), render: (text, r) => text || t('Unlimited') },
{
key: 'state',
title: t('State'),
render: (_, r) => {
return stateMapVal[`${r.audit_state_id}`]?.label;
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 ?(
render: (_, r) =>
r.audit_state_id <= 0 ? (
<Space>
<Button onClick={() => handleAuditPriceItem('2', r)}></Button>
<Button onClick={() => handleAuditPriceItem('3', r)}></Button>
@ -116,14 +121,15 @@ const PriceTable = ({dataSource,refresh}) => {
) : null,
},
];
return <Table pagination={false} {...{ loading, columns, dataSource }} rowKey={(r) => r.id} />;
}
return <Table size={'small'} pagination={false} {...{ columns, dataSource }} rowKey={(r) => r.id} />;
};
/**
*
*/
const TypesPanels = (props) => {
const [loading, agencyProducts, ] = useProductsStore((state) => [state.loading, state.agencyProducts]);
const { t } = useTranslation();
const [loading, agencyProducts] = useProductsStore((state) => [state.loading, state.agencyProducts]);
// console.log(agencyProducts);
const productsTypes = useProductsTypes();
const [activeKey, setActiveKey] = useState([]);
@ -138,7 +144,7 @@ const TypesPanels = (props) => {
children: (
<PriceTable
// loading={loading}
dataSource={agencyProducts[ele.value].reduce((r, c) => r.concat(c.quotation.map((q, i) => ({ ...q, info: c.info, rowSpan: i === 0 ? c.quotation.length : 0 }))), [])}
dataSource={agencyProducts[ele.value].reduce((r, c) => r.concat(c.quotation.map((q, i) => ({ ...q, weekdays: q.weekdays.split(',').filter(Boolean).map(w => t(`weekdaysShort.${w}`)).join(', '), info: c.info, rowSpan: i === 0 ? c.quotation.length : 0 }))), [])}
refresh={props.refresh}
/>
),
@ -151,31 +157,31 @@ const TypesPanels = (props) => {
}, [productsTypes, agencyProducts]);
const onCollapseChange = (_activeKey) => {
setActiveKey(_activeKey)
}
return (
<Collapse items={showTypes} activeKey={activeKey} onChange={onCollapseChange} />
)
}
setActiveKey(_activeKey);
};
return <Collapse items={showTypes} activeKey={activeKey} onChange={onCollapseChange} />;
};
const Audit = ({ ...props }) => {
const { travel_agency_id, use_year, audit_state } = useParams();
const [activeAgency, getAgencyProducts] = useProductsStore((state) => [state.activeAgency, state.getAgencyProducts]);
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])
}, [travel_agency_id]);
return (
<>
<SecondHeaderWrapper header={<Header title={activeAgency.travel_agency_name} agency={activeAgency} refresh={handleGetAgencyProducts} />}>
<hr />
<SecondHeaderWrapper header={<Header title={activeAgency.travel_agency_name} agency={activeAgency} refresh={handleGetAgencyProducts} />} loading={loading} >
{/* debug: 0 */}
{/* <PrintContractPDF /> */}
<TypesPanels refresh={handleGetAgencyProducts} />
</SecondHeaderWrapper>
</>

@ -7,6 +7,9 @@ import { fetchJSON } from "@/utils/request";
import { type } from 'windicss/utils';
import { searchAgencyAction, getAgencyProductsAction } from '@/stores/Products/Index';
import Extras from './Detail/Extras';
function Index() {
const { t } = useTranslation();
const [form] = Form.useForm();
@ -679,7 +682,7 @@ function Index() {
</Card>
<Card style={{ width: "80%" }}>
<h2>{t('products:bindingProducts')}</h2>
{/* <h2>{t('products:bindingProducts')}</h2>
<Form.Item name="extras">
<Table
components={componentsBinding}
@ -692,7 +695,8 @@ function Index() {
<Button onClick={handleAddBinding} type="primary" style={{ marginBottom: 16 }}>
{t('products:addBinding')}
</Button>
</Form.Item>
</Form.Item> */}
<Extras productId={2} />
</Card>

@ -0,0 +1,173 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { App, Table, Button, Modal, Popconfirm } from 'antd';
import useProductsStore, { getAgencyProductExtrasAction, getAgencyProductsAction, addProductExtraAction, delProductExtrasAction } from '@/stores/Products/Index';
import { isEmpty, cloneDeep } from '@/utils/commons';
import SearchForm from '@/components/SearchForm';
import RequireAuth from '@/components/RequireAuth';
import { PERM_PRODUCTS_MANAGEMENT } from '@/config';
const NewAddonModal = ({ onPick, ...props }) => {
const { travel_agency_id, use_year } = useParams();
const { t } = useTranslation();
const { notification, message } = App.useApp();
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([]);
const result = await getAgencyProductsAction({ ...param, audit_state: '1', travel_agency_id, use_year });
setSearchResult(result?.products || []);
setSearchLoading(false);
};
const handleAddExtras = async (item) => {
// const success = await fetchBindOrder({ coli_sn, conversationid: currentConversationID });
// success ? message.success('') : message.error('');
// setOpen(false);
if (typeof onPick === 'function') {
onPick(item);
}
};
// todo:
const searchResultColumns = [
{ key: 'title', dataIndex: ['info', 'title'], width: '16rem', title: t('products:Title') },
{
title: t('products:price'),
dataIndex: ['quotation', '0', 'value'],
width: '10rem',
render: (_, { quotation }) => `${quotation[0].value} ${quotation[0].currency} / ${quotation[0].unit_name}`, // todo:
},
{
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')}
</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 newSuccess = await addProductExtraAction({ travel_agency_id, id: productId, extras: [2] });
newSuccess ? message.success(t('Success')) : message.error(t('Failed'));
await handleGetAgencyProductExtras();
}
const handleDelAddon = async (item) => {
// todo:
const delSuccess = await delProductExtrasAction({ travel_agency_id, id: productId, extras: [2] });
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].value} ${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('products:sureDelete')} onConfirm={(e) => handleDelAddon(r)} >
<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;

@ -1,14 +1,16 @@
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Row, Col, Space, Table } from 'antd';
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 from '@/stores/Products/Index';
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]);
@ -16,11 +18,29 @@ function Index() {
const handleSearchAgency = (formVal = undefined) => {
const { starttime, endtime, ...param } = formVal || formValuesToSub;
const searchParam = objectMapper(param, { agency: 'travel_agency_ids', startdate: 'edit_date1', enddate: 'edit_date2' });
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 (toID) => {
setCopyLoading(true);
const success = await copyAgencyDataAction(sourceAgency.travel_agency_id, toID);
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();
}, []);
@ -39,8 +59,9 @@ function Index() {
key: 'action',
render: (_, r) => (
<Space size={'large'}>
<Link to={`/products/${r.travel_agency_id}/${searchValues.year || 'all'}/${searchValues.audit_state || 'all'}/edit`}>{t('Edit')}</Link>
<Link to={`/products/${r.travel_agency_id}/${searchValues.year || 'all'}/${searchValues.audit_state || 'all'}/audit`}>{t('Audit')}</Link>
<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>
),
},
@ -49,15 +70,12 @@ function Index() {
<Space direction='vertical' style={{ width: '100%' }}>
<SearchForm
fieldsConfig={{
shows: ['agency', 'audit_state', 'dates', 'year', 'keyword'], // todo: agency
shows: ['agency', 'audit_state', 'dates', 'year', 'keyword'],
fieldProps: {
agency: { col: 4 },
dates: { label: t('products:CreateDate') },
keyword: { label: t('products:Title'), col: 4 },
},
fieldComProps: {
agency: { mode: null }, // todo:
},
sort: { agency: 1, audit_state: 2, keyword: 100 },
}}
initialValue={{
@ -69,19 +87,27 @@ function Index() {
handleSearchAgency(formVal);
}}
/>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table
bordered={true}
columns={columns}
dataSource={agencyList}
pagination={{ defaultPageSize: 20, showTotal: showTotal }}
loading={loading}
rowKey={'travel_agency_id'}
<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'],
fieldProps: {
agency: { label: `目标${t('products:Vendor')}`, col: 12 },
},
fieldComProps: {
agency: { mode: null }, //
},
}}
onSubmit={(err, formVal, filedsVal) => {
handleCopyAgency(formVal.agency);
}}
/>
</Col>
<Col md={24} lg={24} xxl={24}></Col>
</Row>
</Modal>
</Space>
);
}
Loading…
Cancel
Save