diff --git a/public/locales/en/common.json b/public/locales/en/common.json index b113807..8ed1854 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -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", diff --git a/public/locales/en/products.json b/public/locales/en/products.json index b0363b7..b170ecf 100644 --- a/public/locales/en/products.json +++ b/public/locales/en/products.json @@ -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?", + + "#": "#" } diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index e3239a1..82b43f9 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -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": "账单", diff --git a/public/locales/zh/products.json b/public/locales/zh/products.json index 17ec532..55005ce 100644 --- a/public/locales/zh/products.json +++ b/public/locales/zh/products.json @@ -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":"确定删除?", + + "#": "#" } diff --git a/src/components/SearchForm.jsx b/src/components/SearchForm.jsx index 979a6fc..f6bad2c 100644 --- a/src/components/SearchForm.jsx +++ b/src/components/SearchForm.jsx @@ -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 ( <> -
+ {/* */} {getFields({ sort, initialValue: readValues, hides, shows, fieldProps, fieldComProps, form, presets, t })} {/* 'textAlign': 'right' */} - {/* */} {/* */} + {t('Edit')} @@ -46,20 +49,21 @@ const Header = ({ title, agency, refresh, ...props}) => { - {/* todo: export */} - + {/* todo: export, 审核完成之后才能导出 */} + + {/* */} ); }; -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,35 +99,37 @@ 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 {stateMapVal[`${r.audit_state_id}`]?.label}; }, }, { title: '价格审核', key: 'action', - render: (_, r) => r.audit_state_id <= 0 ?( - - - - - ) : null, + render: (_, r) => + r.audit_state_id <= 0 ? ( + + + + + ) : null, }, ]; - return r.id} />; -} + return
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: ( 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 ( - - ) -} + setActiveKey(_activeKey); + }; + return ; +}; 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 ( <> - }> -
+ } loading={loading} > + {/* debug: 0 */} + {/* */} + diff --git a/src/views/products/Detail.jsx b/src/views/products/Detail.jsx index 8ce7648..716ffa7 100644 --- a/src/views/products/Detail.jsx +++ b/src/views/products/Detail.jsx @@ -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() { -

{t('products:bindingProducts')}

+ {/*

{t('products:bindingProducts')}

{t('products:addBinding')} - + */} + diff --git a/src/views/products/Detail/Extras.jsx b/src/views/products/Detail/Extras.jsx new file mode 100644 index 0000000..b5e0f1b --- /dev/null +++ b/src/views/products/Detail/Extras.jsx @@ -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) => ( + + ), + }, + ]; + const paginationProps = { + showTotal: (total) => t('Table.Total', { total }), + }; + return ( + <> + + + setOpen(false)} destroyOnClose> + { + onSearchProducts(formVal); + }} + /> +
+ + + ); +}; + +/** + * + */ +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) => ( + handleDelAddon(r)} > + + + ), + }, + ]; + + return ( + <> + +

{t('products:EditComponents.Extras')}

+
r.info.id} /> + + + + ); +}; +export default Extras; diff --git a/src/views/products/Index.jsx b/src/views/products/Manage.jsx similarity index 52% rename from src/views/products/Index.jsx rename to src/views/products/Manage.jsx index d1b1495..3e5d3b6 100644 --- a/src/views/products/Index.jsx +++ b/src/views/products/Manage.jsx @@ -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) => ( - {t('Edit')} - {t('Audit')} + {t('Edit')} + {t('Audit')} + ), }, @@ -49,15 +70,12 @@ function Index() { - - -
- - - +
+ + {/* 复制弹窗 */} + setCopyModalVisible(false)} destroyOnClose> +
复制源: {sourceAgency.travel_agency_name}
+ { + handleCopyAgency(formVal.agency); + }} + /> +
); }