diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 85155ad..b228ae8 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -10,7 +10,9 @@ "Confirm": "Confirm", "Close": "Close", "Save": "Save", + "New": "New", "Edit": "Edit", + "Audit": "Audit", "Delete": "Delete", "Add": "Add", "View": "View", @@ -19,6 +21,23 @@ "Upload": "Upload", "preview": "Preview", "Total": "Total", + "Action": "Action", + "Import": "Import", + "Export": "Export", + "Copy": "Copy", + + "sureCancel": "Are you sure to cancel?", + "sureDelete":"Are you sure to delete?", + "Yes": "Yes", + + "Success": "Success", + "Failed": "Failed", + + "All": "All", + + "Table": { + "Total": "Total {{total}} items" + }, "Login": "Login", "Username": "Username", @@ -46,13 +65,32 @@ "lastThreeMonth": "Last Three Month", "thisYear": "This Year" }, + "weekdays": { + "1": "Monday", + "2": "Tuesday", + "3": "Wednesday", + "4": "Thursday", + "5": "Friday", + "6": "Saturday", + "7": "Sunday" + }, + "weekdaysShort": { + "1": "Mon", + "2": "Tue", + "3": "Wed", + "4": "Thu", + "5": "Fri", + "6": "Sat", + "7": "Sun" + }, "menu": { "Reservation": "Reservation", "Invoice": "Invoice", "Feedback": "Feedback", "Notice": "Notice", "Report": "Report", - "Airticket": "AirTicket" + "Airticket": "AirTicket", + "Products": "Products" }, "Validation": { "Title": "Notification", diff --git a/public/locales/en/products.json b/public/locales/en/products.json new file mode 100644 index 0000000..667c143 --- /dev/null +++ b/public/locales/en/products.json @@ -0,0 +1,111 @@ +{ + "ProductType": "Product Type", + "type": { + "Experience": "Experience", + "Car": "Transport Services", + "Guide": "Guide Services", + "Package": "Package Tour", + "Attractions": "Attractions", + "Meals": "Meals", + "Extras": "Extras", + "UltraService": "Ultra Service" + }, + "EditComponents": { + "info": "Product Information", + "Quotation": "Quotation", + "Extras": "Add-on" + }, + "auditState": { + "New": "New", + "Pending": "Pending", + "Approved": "Approved", + "Rejected": "Rejected", + "Published": "Published" + }, + "auditStateAction": { + "New": "New", + "Pending": "Pending", + "Approved": "Approve", + "Rejected": "Reject", + "Published": "Publish" + }, + "PriceUnit": { + "0": "Person", + "1": "Group", + "title": "Price Unit" + }, + "Status": "Status", + "State": "State", + + "Title": "Title", + "Vendor": "Vendor", + "AuState": "Audit State", + "CreatedBy": "Created By", + "CreateDate": "Create Date", + "AuditedBy": "Audited By", + "AuditDate": "Audit Date", + "OpenHours": "Open Hours", + "Duration": "Duration", + "KM": "KM", + "RecommendsRate": "RecommendsRate", + "OpenWeekdays": "Open Weekdays", + "DisplayToC": "DisplayToC", + "Dept": "Dept", + + "productProject": "Product project", + "Code": "Code", + "City": "City", + "Remarks": "Remarks", + "tourTime": "Tour time", + "recommendationRate": "Recommends rate", + "Name": "Name", + "Description":"Description", + "supplierQuotation": "Supplier quotation", + "addQuotation": "Add quotation", + "bindingProducts": "Binding products", + "addBinding": "Add binding", + + "adultPrice": "Adult price", + "childrenPrice": "Child price", + "currency": "Currency", + "Types": "Type", + "number": "Number", + "validityPeriod":"Validity period", + "operation": "Operation", + "price": "Price", + "weekends":"Weekends", + + "Quotation": "Quotation", + "Offer": "Offer", + + "Unit": "Unit", + "GroupSize": "Group Size", + "UseDates": "Use Dates", + + "Weekdays": "Weekdays", + "OnWeekdays": "On Weekdays: ", + "Unlimited": "Unlimited", + + "UseYear": "Use Year", + + "AgeType": { + "Type": "Age Type", + "Adult": "Adult", + "Child": "Child" + }, + + "save":"save", + "edit":"edit", + "delete":"delete", + "cancel":"cancel", + "sureCancel":"Sure you want to cancel?", + "sureDelete":"Sure you want to delete?", + + "CopyFormMsg": { + "requiredVendor": "Please pick a target vendor", + "requiredTypes": "Please select product types", + "requiredDept": "Please pick a owner department" + }, + + "#": "#" +} diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 1ffb8ae..dd6e73e 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -10,7 +10,9 @@ "Confirm": "确认", "Close": "关闭", "Save": "保存", + "New": "新增", "Edit": "编辑", + "Audit": "审核", "Delete": "删除", "Add": "添加", "View": "查看", @@ -19,6 +21,23 @@ "Upload": "上传", "preview": "预览", "Total": "总数", + "Action": "操作", + "Import": "导入", + "Export": "导出", + "Copy": "复制", + + "sureCancel": "确定取消?", + "sureDelete":"确定删除?", + "Yes": "是", + + "Success": "成功", + "Failed": "失败", + + "All": "所有", + + "Table": { + "Total": "共 {{total}} 条" + }, "Login": "登录", "Username": "账号", @@ -46,13 +65,32 @@ "lastThreeMonth": "前三个月", "thisYear": "今年" }, + "weekdays": { + "1": "周一", + "2": "周二", + "3": "周三", + "4": "周四", + "5": "周五", + "6": "周六", + "7": "周日" + }, + "weekdaysShort": { + "1": "一", + "2": "二", + "3": "三", + "4": "四", + "5": "五", + "6": "六", + "7": "日" + }, "menu": { "Reservation": "团预订", "Invoice": "账单", "Feedback": "反馈表", "Notice": "通知", "Report": "质量评分", - "Airticket": "机票订票" + "Airticket": "机票订票", + "Products": "产品管理" }, "Validation": { "Title": "温馨提示", diff --git a/public/locales/zh/products.json b/public/locales/zh/products.json new file mode 100644 index 0000000..239af49 --- /dev/null +++ b/public/locales/zh/products.json @@ -0,0 +1,112 @@ +{ + "ProductType": "项目类型", + "type": { + "Experience": "综费", + "Car": "车费", + "Guide": "导游", + "Package": "包价线路", + "Attractions": "景点", + "Meals": "餐费", + "Extras": "附加项目", + "UltraService": "超公里" + }, + "EditComponents": { + "info": "产品信息", + "Quotation": "报价", + "Extras": "附加项目" + }, + "auditState": { + "New": "新增", + "Pending": "待审核", + "Approved": "已通过", + "Rejected": "已拒绝", + "Published": "已发布" + }, + "auditStateAction": { + "New": "新增", + "Pending": "待审核", + "Approved": "审核通过", + "Rejected": "审核拒绝", + "Published": "审核发布" + }, + "PriceUnit": { + "0": "每人", + "1": "每团", + "title": "报价单位" + }, + "Status": "状态", + "State": "状态", + + "Title": "名称", + "Vendor": "供应商", + "AuState": "审核状态", + "CreatedBy": "提交人员", + "CreateDate": "提交时间", + "AuditedBy": "审核人员", + "AuditDate": "审核时间", + + "OpenHours": "游览时间", + "Duration": "游览时长", + "KM": "公里数", + "RecommendsRate": "推荐指数", + "OpenWeekdays": "周开放日", + "DisplayToC": "报价信显示", + "Dept": "小组", + + + "productProject": "产品项目", + "Code": "简码", + "City": "城市", + "Remarks": "备注", + "tourTime": "游览时间", + "recommendationRate": "推荐指数", + "Name":"名称", + "Price":"价格", + "Description":"描述", + "supplierQuotation": "供应商报价", + "addQuotation": "添加报价", + "bindingProducts": "绑定产品", + "addBinding": "添加绑定", + + "adultPrice": "成人价", + "childrenPrice": "儿童价", + "currency": "币种", + "Types": "类型", + "number": "人等", + "validityPeriod":"有效期", + "operation": "操作", + "price": "价格", + + "Quotation": "报价", + "Offer": "报价", + "Unit": "单位", + "GroupSize": "人等", + "UseDates": "使用日期", + + "Weekdays": "周末", + "OnWeekdays": "周: ", + "Unlimited": "不限", + + "UseYear": "年份", + + "AgeType": { + "Type": "人群", + "Adult": "成人", + "Child": "儿童" + }, + + "save":"保存", + "edit":"编辑", + "delete":"删除", + "cancel":"取消", + "sureCancel": "确定取消?", + "sureDelete":"确定删除?", + + "CopyFormMsg": { + "requiredVendor": "请选择目标供应商", + "requiredTypes": "请选择产品类型", + "requiredDept": "请选择所属小组" + }, + + "#": "#" +} diff --git a/src/assets/global.css b/src/assets/global.css index a31e444..cc5d96f 100644 --- a/src/assets/global.css +++ b/src/assets/global.css @@ -1,3 +1,6 @@ @import 'tailwindcss/base'; @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; +.ant-table-wrapper.border-collapse table { + border-collapse: collapse; +} diff --git a/src/components/AuditStateSelector.jsx b/src/components/AuditStateSelector.jsx new file mode 100644 index 0000000..db4c77d --- /dev/null +++ b/src/components/AuditStateSelector.jsx @@ -0,0 +1,12 @@ +import { Select } from 'antd'; +import { useProductsAuditStates } from '@/hooks/useProductsSets'; + +const AuditStateSelector = ({ ...props }) => { + const states = useProductsAuditStates(); + return ( + <> + handleCellChange(e.target.value, record.dateRange, peopleRange, 'adultPrice')} + placeholder="成人价" + style={{ width: '45%' }} + suffix={`${currency}/${type}`} + /> + handleCellChange(e.target.value, record.dateRange, peopleRange, 'childPrice')} + placeholder="儿童价" + style={{ width: '45%' }} + suffix={`${currency}/${type}`} + /> + + ), + } + ])), + ]; + + return ( + <> + + + + + + + + + + + + { + if (dates && dates.length === 2) { + setStartDate(dates[0]); + setEndDate(dates[1]); + } else { + setStartDate(null); + setEndDate(null); + } + }} + /> + + + + + + + setStartPeople(parseInt(e.target.value, 10))} + /> + + setEndPeople(parseInt(e.target.value, 10))} + /> + + + + + + + + + + {dateRanges.map((dateRange, index) => ( + handleRemoveTag(index, 'date')}> + {`${dateRange.startDate.format('YYYY-MM-DD')} ~ ${dateRange.endDate.format('YYYY-MM-DD')}`} + + ))} + {peopleRanges.map((peopleRange, index) => ( + handleRemoveTag(index, 'people')}> + {peopleRange} + + ))} + + + + + ); +}; + +export default BatchImportPrice; + + + diff --git a/src/components/DeptSelector.jsx b/src/components/DeptSelector.jsx new file mode 100644 index 0000000..ab8911c --- /dev/null +++ b/src/components/DeptSelector.jsx @@ -0,0 +1,61 @@ +import { Component } from 'react'; +import { Select } from 'antd'; +// import { groups, leafGroup } from '../../libs/ht'; + +/** + * 小组 + */ +export const groups = [ + { value: '1,2,28,7,33', key: '1,2,28,7,33', label: 'GH事业部', code: 'GH', children: [1, 2, 28, 7, 33] }, + { value: '8,9,11,12,20,21', key: '8,9,11,12,20,21', label: '国际事业部', code: 'INT', children: [8, 9, 11, 12, 20, 21] }, + { value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: '', children: [10, 18, 16, 30] }, + { value: '1', key: '1', label: 'CH直销', code: '', children: [] }, + { value: '2', key: '2', label: 'CH大客户', code: '', children: [] }, + { value: '28', key: '28', label: 'AH亚洲项目组', code: 'AH', children: [] }, + { value: '33', key: '33', label: 'GH项目组', code: '', children: [] }, + { value: '7', key: '7', label: '市场推广', code: '', children: [] }, + { value: '8', key: '8', label: '德语', code: '', children: [] }, + { value: '9', key: '9', label: '日语', code: '', children: [] }, + { value: '11', key: '11', label: '法语', code: '', children: [] }, + { value: '12', key: '12', label: '西语', code: '', children: [] }, + { value: '20', key: '20', label: '俄语', code: '', children: [] }, + { value: '21', key: '21', label: '意语', code: '', children: [] }, + { value: '10', key: '10', label: '商旅', code: '', children: [] }, + { value: '18', key: '18', label: 'CT', code: 'CT', children: [] }, + { value: '16', key: '16', label: 'APP', code: 'APP', children: [] }, + { value: '30', key: '30', label: 'Trippest', code: 'TP', children: [] }, + { value: '31', key: '31', label: '花梨鹰', code: '', children: [] }, +]; +export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.code || c.key)]: c }), {}); +export const groupsMappedByKey = groups.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {}); +export const leafGroup = groups.slice(3); +export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest + +export const DeptSelector = ({show_all, isLeaf,...props}) => { + const _show_all = ['tags', 'multiple'].includes(props.mode) ? false : show_all; + const options = isLeaf===true ? leafGroup : groups; + + return ( +
+ + + ); +}; + +const SearchForm = ({ initialValue, onSubmit, onReset, confirmText, formName, formLayout, loading, ...props }) => { const { t } = useTranslation(); - const presets = usePresets(); + const presets = useDatePresets(); const [formValues, setFormValues] = useFormStore((state) => [state.formValues, state.setFormValues]); const [formValuesToSub, setFormValuesToSub] = useFormStore((state) => [state.formValuesToSub, state.setFormValuesToSub]); const [form] = Form.useForm(); @@ -23,11 +47,11 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => { shows: [], ...props.fieldsConfig, }; - const { confirmText } = props; const readValues = { ...initialValue, ...formValues }; const formValuesMapper = (values) => { const destinationObject = { + 'keyword': { key: 'keyword', transform: (value) => value || '' }, 'referenceNo': { key: 'referenceNo', transform: (value) => value || '' }, 'dates': [ { key: 'startdate', transform: (arrVal) => (arrVal ? arrVal[0].format(DATE_FORMAT) : '') }, @@ -36,6 +60,29 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => { { key: 'endtime', transform: (arrVal) => (arrVal ? arrVal[1].format(SMALL_DATETIME_FORMAT) : '') }, ], 'invoiceStatus': { key: 'invoiceStatus', transform: (value) => value?.value || value?.key || '', default: '' }, + 'audit_state': { key: 'audit_state', transform: (value) => value?.value || value?.key || '', default: '' }, + 'agency': { + key: 'agency', + transform: (value) => { + return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : ''; + }, + }, + 'year': [ + { key: 'year', transform: (arrVal) => (arrVal ? arrVal.format('YYYY') : '') }, + ], + 'products_types': { + key: 'products_types', + transform: (value) => { + return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : ''; + }, + }, + 'dept': { + key: 'dept', + transform: (value) => { + console.log(value); + return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : ''; + }, + }, }; let dest = {}; const { dates, ...omittedValue } = values; @@ -57,9 +104,9 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => { }, []); const onFinish = (values) => { - console.log('Received values of form, origin form value: ', values); + console.log('Received values of form, origin form value: \n', values); const dest = formValuesMapper(values); - console.log('form value send to onSubmit:', dest); + console.log('form value send to onSubmit:\n', dest); const str = new URLSearchParams(dest).toString(); setFormValues(values); setFormValuesToSub(dest); @@ -83,16 +130,21 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => { setFormValuesToSub(dest); // console.log('form onValuesChange', Object.keys(changedValues), args); }; + + const onFinishFailed = ({ values, errorFields }) => { + console.log('form validate failed', '\nform values:', values, '\nerrorFields', errorFields); + }; + return ( <> -
+ {/* */} {getFields({ sort, initialValue: readValues, hides, shows, fieldProps, fieldComProps, form, presets, t })} {/* 'textAlign': 'right' */}
- {/* + ))} + + + ); +}; + +export default Date; diff --git a/src/config.js b/src/config.js index 1d39e0e..36a5ea0 100644 --- a/src/config.js +++ b/src/config.js @@ -31,3 +31,10 @@ export const PERM_DOMESTIC = '/domestic/all' // 机票供应商 // category: air-ticket export const PERM_AIR_TICKET = '/air-ticket/all' + +// 价格管理 +export const PERM_PRODUCTS_MANAGEMENT = '/products/*'; // 管理 +export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核 +export const PERM_PRODUCTS_INFO_PUT = '/products/info/put'; // 信息.录入 +export const PERM_PRODUCTS_OFFER_AUDIT = '/products/offer/audit'; // 价格.审核 +export const PERM_PRODUCTS_OFFER_PUT = '/products/offer/put'; // 价格.录入 diff --git a/src/hooks/usePresets.js b/src/hooks/useDatePresets.js similarity index 61% rename from src/hooks/usePresets.js rename to src/hooks/useDatePresets.js index aa046e6..2eb4c7d 100644 --- a/src/hooks/usePresets.js +++ b/src/hooks/useDatePresets.js @@ -1,8 +1,9 @@ import { useEffect, useState } from 'react'; import dayjs from "dayjs"; import { useTranslation } from 'react-i18next'; +import i18n from '@/i18n'; -const usePresets = () => { +export const useDatePresets = () => { const [presets, setPresets] = useState([]); const { t, i18n } = useTranslation(); @@ -39,4 +40,21 @@ const usePresets = () => { return presets; } -export default usePresets; +export const useWeekdays = () => { + const [data, setData] = useState([]); + const { t, i18n } = useTranslation(); + useEffect(() => { + const newData = [ + { value: '1', label: t('weekdays.1') }, + { value: '2', label: t('weekdays.2') }, + { value: '3', label: t('weekdays.3') }, + { value: '4', label: t('weekdays.4') }, + { value: '5', label: t('weekdays.5') }, + { value: '6', label: t('weekdays.6') }, + { value: '7', label: t('weekdays.7') }, + ]; + setData(newData); + return () => {}; + }, [i18n.language]); + return data; +}; diff --git a/src/hooks/useHTLanguageSets.js b/src/hooks/useHTLanguageSets.js new file mode 100644 index 0000000..73b827e --- /dev/null +++ b/src/hooks/useHTLanguageSets.js @@ -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; +}; diff --git a/src/hooks/useProductsSets.js b/src/hooks/useProductsSets.js new file mode 100644 index 0000000..888ad43 --- /dev/null +++ b/src/hooks/useProductsSets.js @@ -0,0 +1,122 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import useAuthStore from '@/stores/Auth'; +import { PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config'; + +/** + * 产品管理 相关的预设数据 + * 项目类型 + * * 酒店预定 1 + * * 火车 2 + * * 飞机票务 3 + * * 游船 4 + * * 快巴 5 + * * 旅行社(综费) 6 + * * 景点 7 + * * 特殊项目 8 + * * 其他 9 + * * 酒店 A + * * 超公里 B + * * 餐费 C + * * 小包价 D // 包价线路 + * * 站 X + * * 购物 S + * * 餐 R (餐厅) + * * 娱乐 E + * * 精华线路 T + * * 客人testimonial F + * * 线路订单 O + * * 省 P + * * 信息 I + * * 国家 G + * * 城市 K + * * 图片 H + * * 地图 M + * * 包价线路 L (已废弃) + * * 节日节庆 V + * * 火车站 N + * * 手机租赁 Z + * * ---- webht 类型, 20240624 新增HT类型 ---- + * * 导游 Q + * * 车费 J + */ + +export const useProductsTypes = (showAll = false) => { + const [types, setTypes] = useState([]); + const { t, i18n } = useTranslation(); + + useEffect(() => { + const allItem = [{ label: t('All'), value: '', key: '' }]; + const newData = [ + { label: t('products:type.Experience'), value: '6', key: '6' }, + { label: t('products:type.UltraService'), value: 'B', key: 'B' }, + { label: t('products:type.Car'), value: 'J', key: 'J' }, + { label: t('products:type.Guide'), value: 'Q', key: 'Q' }, + { label: t('products:type.Attractions'), value: '7', key: '7' }, + { label: t('products:type.Meals'), value: 'R', key: 'R' }, + { label: t('products:type.Extras'), value: '8', key: '8' }, + { label: t('products:type.Package'), value: 'D', key: 'D' }, + ]; + const res = showAll ? [...allItem, ...newData] : newData; + setTypes(res); + }, [i18n.language]); + + return types; +}; +export const useProductsTypesMapVal = (value) => { + const stateSets = useProductsTypes(); + const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {}); + return stateMapVal; +}; + +export const useProductsAuditStates = () => { + const [types, setTypes] = useState([]); + const { t, i18n } = useTranslation(); + + useEffect(() => { + const newData = [ + { key: '-1', value: '-1', label: t('products:auditState.New'), color: 'gray-500' }, + { key: '0', value: '0', label: t('products:auditState.Pending'), color: '' }, + { key: '2', value: '2', label: t('products:auditState.Approved'), color: 'primary' }, + { key: '3', value: '3', label: t('products:auditState.Rejected'), color: 'red-500' }, + { key: '1', value: '1', label: t('products:auditState.Published'), color: 'primary' }, + // ELSE 未知 + ]; + setTypes(newData); + }, [i18n.language]); + + return types; +}; + +export const useProductsAuditStatesMapVal = (value) => { + const stateSets = useProductsAuditStates(); + const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {}); + return stateMapVal; +}; + +/** + * @ignore + */ +export const useProductsTypesFieldsets = (type) => { + const [isPermitted] = useAuthStore((state) => [state.isPermitted]); + const infoDefault = [['code'], ['title']]; + const infoAdmin = ['remarks', 'dept', 'display_to_c']; + const infoTypesMap = { + '6': [[],[]], + 'B': [['city_id', 'km'], []], + 'J': [['city_id', 'recommends_rate', 'duration', 'display_to_c'], ['description',]], + 'Q': [['city_id', 'duration', ], ['description',]], + 'D': [['city_id', 'recommends_rate','duration',], ['description',]], + '7': [['city_id', 'recommends_rate', 'duration', 'display_to_c', 'open_weekdays'], ['description',]], // todo: 怎么是2个图 + 'R': [['city_id',], ['description',]], + '8': [[],[]], // todo: ? + }; + const thisTypeFieldset = (_type) => { + const adminSet = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? infoAdmin : []; + return [ + [...infoDefault[0], ...infoTypesMap[_type][0], ...adminSet], + [...infoDefault[1], ...infoTypesMap[_type][1]] + ]; + }; + return thisTypeFieldset(type); +} diff --git a/src/i18n/LanguageSwitcher.jsx b/src/i18n/LanguageSwitcher.jsx index ac1f009..f09a07e 100644 --- a/src/i18n/LanguageSwitcher.jsx +++ b/src/i18n/LanguageSwitcher.jsx @@ -1,13 +1,30 @@ -import React, { useState } from 'react'; -import { Dropdown, Menu } from 'antd'; +import { useState, useEffect } from 'react'; +import { Dropdown } from 'antd'; import { useTranslation } from 'react-i18next'; +import { appendRequestParams } from '@/utils/request'; +const i18n_to_htcode = { + 'zh': 2, + 'en': 1, +}; + +export const useDefaultLgc = () => { + const { i18n } = useTranslation(); + return { language: i18n_to_htcode[i18n.language], }; +}; /** * 语言选择组件 */ const Language = () => { - const { t, i18n } = useTranslation(); +const { t, i18n } = useTranslation(); const [selectedKeys, setSelectedKeys] = useState([i18n.language]); + + useEffect(() => { + appendRequestParams('lgc', i18n_to_htcode[i18n.language]); + + return () => {}; + }, [i18n.language]); + // 切换语言事件 const handleChangeLanguage = ({ key }) => { setSelectedKeys([key]); diff --git a/src/i18n/index.js b/src/i18n/index.js index 9efd3bd..edd3f75 100644 --- a/src/i18n/index.js +++ b/src/i18n/index.js @@ -17,7 +17,7 @@ i18n backend: { loadPath: '/locales/{{lng}}/{{ns}}.json', }, - ns: ['common', 'group', 'vendor', 'account'], + ns: ['common', 'group', 'vendor', 'account', 'products'], defaultNS: 'common', detection: { // convertDetectedLanguage: 'Iso15897', diff --git a/src/main.jsx b/src/main.jsx index 1bf374b..008ed86 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -34,7 +34,10 @@ import { usingStorage } from '@/hooks/usingStorage' import useAuthStore from './stores/Auth' import { isNotEmpty } from '@/utils/commons' -import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET } from '@/config' +import ProductsManage from '@/views/products/Manage'; +import ProductsDetail from '@/views/products/Detail'; +import ProductsAudit from '@/views/products/Audit'; +import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config' import './i18n' diff --git a/src/stores/Auth.js b/src/stores/Auth.js index d0f4fa2..0b0a211 100644 --- a/src/stores/Auth.js +++ b/src/stores/Auth.js @@ -193,4 +193,4 @@ const useAuthStore = create((set, get) => ({ })) -export default useAuthStore \ No newline at end of file +export default useAuthStore diff --git a/src/stores/Feedback.js b/src/stores/Feedback.js index dd3feb1..9b2356e 100644 --- a/src/stores/Feedback.js +++ b/src/stores/Feedback.js @@ -1,4 +1,3 @@ -import { makeAutoObservable, runInAction } from 'mobx'; import { fetchJSON, postForm } from '@/utils/request'; import { groupBy } from '@/utils/commons'; import * as config from '@/config'; diff --git a/src/stores/Invoice.js b/src/stores/Invoice.js index 814e69f..09ac0f2 100644 --- a/src/stores/Invoice.js +++ b/src/stores/Invoice.js @@ -1,10 +1,5 @@ -import { makeAutoObservable, runInAction } from "mobx"; import { fetchJSON, postForm } from "@/utils/request"; -import { prepareUrl, isNotEmpty, objectMapper } from "@/utils/commons"; import { HT_HOST } from "@/config"; -import { json } from "react-router-dom"; -import * as config from "@/config"; -import dayjs from "dayjs"; import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; @@ -138,366 +133,3 @@ const useInvoiceStore = create( ); export default useInvoiceStore; - -export class Invoice { - constructor(root) { - makeAutoObservable(this, { rootStore: false }); - this.root = root; - } - - invoiceList = []; //账单列表 - invoicekImages = []; //图片列表 - invoiceGroupInfo = {}; //账单详细 - invoiceProductList = []; //账单细项 - invoiceZDDetail = []; //报账信息 - invoiceCurrencyList = []; //币种 - invoicePicList = []; //多账单图片列表数组 - invoiceFormData = { info_money: 0, info_Currency: "", info_date: "" }; //存储form数据 - - invoicePaid = [] ; //支付账单列表 - invoicePaidDetail = []; //每期账单详细 - - loading = false; - search_date_start = dayjs().subtract(2, "M").startOf("M"); - search_date_end = dayjs().endOf("M"); - - onDateRangeChange = dates => { - console.log(dates); - this.search_date_start = dates==null? null: dates[0]; - this.search_date_end = dates==null? null: dates[1]; - }; - - fetchInvoiceList(VEI_SN, GroupNo, DateStart, DateEnd,OrderType) { - this.loading = true; - const fetchUrl = prepareUrl(HT_HOST + "/service-cusservice/PTSearchGMBPageList") - .append("VEI_SN", VEI_SN) - .append("OrderType", 0) - .append("GroupNo", GroupNo.trim()) - .append("DateStart", DateStart) - .append("DateEnd", DateEnd) - .append("Orderbytype", 1) - .append("TimeType", 0) - .append("limitmarket", "") - .append("mddgroup", "") - .append("SecuryGroup", "") - .append("TotalNum", 0) - .append("PageSize", 2000) - .append("PageIndex", 1) - .append("PayState",OrderType) - .append("token",this.root.authStore.login.token) - .build(); - - return fetchJSON(fetchUrl).then(json => { - runInAction(() => { - this.loading = false; - if (json.errcode == 0) { - if (isNotEmpty(json.Result)) { - this.invoiceList = json.Result.map((data, index) => { - return { - key: data.GMDSN, - gmd_gri_sn: data.GMD_GRI_SN, - gmd_vei_sn: data.GMD_VEI_SN, - GetGDate: data.GetGDate, - GMD_FillWorkers_SN: data.GMD_FillWorkers_SN, - GMD_FWks_LastEditTime: data.GMD_FWks_LastEditTime, - GMD_VerifyUser_SN: data.GMD_VerifyUser_SN, - GMD_Dealed: data.GMD_Dealed, - GMD_VRequestVerify: data.GMD_VRequestVerify, - LeftGDate: data.LeftGDate, - GMD_FillWorkers_Name: data.GMD_FillWorkers_Name, - GroupName: data.GroupName, - AllMoney: data.AllMoney, - PersonNum: data.PersonNum, - GMD_Currency: data.GMD_Currency, - VName: data.VName, - FKState: data.FKState, - }; - }); - } else { - this.invoiceList = []; - } - } else { - throw new Error(json.errmsg + ": " + json.errcode); - } - }); - }); - } - - fetchInvoiceDetail(GMDSN, GSN) { - const fetchUrl = prepareUrl(HT_HOST + "/service-cusservice/PTGetZDDetail") - .append("VEI_SN", this.root.authStore.login.travelAgencyId) - .append("GRI_SN", GSN) - .append("GMD_SN", GMDSN) - .append("LGC", 1) - .append("Bill", 1) - .append("token",this.root.authStore.login.token) - .build(); - - return fetchJSON(fetchUrl).then(json => { - runInAction(() => { - if (json.errcode == 0) { - this.invoiceGroupInfo = json.GroupInfo[0]; - this.invoiceProductList = json.ProductList; - this.invoiceCurrencyList = json.CurrencyList; - this.invoiceZDDetail = json.ZDDetail; - } else { - throw new Error(json.errmsg + ": " + json.errcode); - } - }); - return json; - }); - } - - //获取供应商提交的图片 - getInvoicekImages(VEI_SN, GRI_SN) { - let url = `/service-fileServer/ListFile`; - url += `?GRI_SN=${GRI_SN}&VEI_SN=${VEI_SN}&FilePathName=invoice`; - url += `&token=${this.root.authStore.login.token}`; - fetch(config.HT_HOST + url) - .then(response => response.json()) - .then(json => { - console.log(json); - runInAction(() => { - this.invoicekImages = json.result.map((data, index) => { - return { - uid: -index, //用负数,防止添加删除的时候错误 - name: data.file_name, - status: "done", - url: data.file_url, - }; - }); - }); - }) - .catch(error => { - console.log("fetch data failed", error); - }); - } - - //从数据库获取图片列表 - getInvoicekImages_fromData(jsonData) { - let arrLen = jsonData.length; - let arrPicList = jsonData.map((data, index) => { - const GMD_Pic = data.GMD_Pic; - let picList = []; - if (isNotEmpty(GMD_Pic)) { - let js_Pic = JSON.parse(GMD_Pic); - picList = js_Pic.map((picData, pic_Index) => { - return { - uid: -pic_Index, //用负数,防止添加删除的时候错误 - name: "", - status: "done", - url: picData.url, - }; - }); - } - if (data.GMD_Dealed == false && arrLen == index + 1) { - this.invoicekImages = picList; - } - return picList; - }); - - runInAction(() => { - this.invoicePicList = arrPicList; - }); - } - - //获取数据库的表单默认数据回填。 - getFormData(jsonData) { - let arrLen = jsonData.length; - return jsonData.map((data, index) => { - if (data.GMD_Dealed == false && arrLen == index + 1) { - //只有最后一条账单未审核通过才显示 - runInAction(() => { - this.invoiceFormData = { info_money: data.GMD_Cost, info_Currency: data.GMD_Currency, info_date: isNotEmpty(data.GMD_PayDate) ? dayjs(data.GMD_PayDate) : "" }; - }); - } - }); - } - - removeFeedbackImages(fileurl) { - let url = `/service-fileServer/FileDelete`; - url += `?fileurl=${fileurl}`; - url += `&token=${this.root.authStore.login.token}`; - return fetch(config.HT_HOST + url) - .then(response => response.json()) - .then(json => { - console.log(json); - return json.Result; - }) - .catch(error => { - console.log("fetch data failed", error); - }); - } - - postEditInvoiceDetail(GMD_SN, Currency, Cost, PayDate, Pic, Memo) { - let postUrl = HT_HOST + "/service-cusservice/EditSupplierFK"; - let formData = new FormData(); - formData.append("LMI_SN", this.root.authStore.login.userId); - formData.append("GMD_SN", GMD_SN); - formData.append("Currency", Currency); - formData.append("Cost", Cost); - formData.append("PayDate", isNotEmpty(PayDate) ? PayDate : ""); - formData.append("Pic", Pic); - formData.append("Memo", Memo); - formData.append("token",this.root.authStore.login.token); - - return postForm(postUrl, formData).then(json => { - console.info(json); - return json; - }); - } - - postAddInvoice(GRI_SN, Currency, Cost, PayDate, Pic, Memo) { - let postUrl = HT_HOST + "/service-cusservice/AddSupplierFK"; - let formData = new FormData(); - formData.append("LMI_SN", this.root.authStore.login.userId); - formData.append("VEI_SN", this.root.authStore.login.travelAgencyId); - formData.append("GRI_SN", GRI_SN); - formData.append("Currency", Currency); - formData.append("Cost", Cost); - formData.append("PayDate", isNotEmpty(PayDate) ? PayDate : ""); - formData.append("Pic", Pic); - formData.append("Memo", Memo); - formData.append("token",this.root.authStore.login.token); - return postForm(postUrl, formData).then(json => { - console.info(json); - return json; - }); - } - - //账单状态 - invoiceStatus(FKState) { - switch (FKState - 1) { - case 1: - return "Submitted"; - break; - case 2: - return "Travel Advisor"; - break; - case 3: - return "Finance Dept"; - break; - case 4: - return "Paid"; - break; - default: - return ""; - break; - } - } - - fetchInvoicePaid(VEI_SN, GroupNo, DateStart, DateEnd) { - this.loading = true; - const fetchUrl = prepareUrl(HT_HOST + "/service-Cooperate/Cooperate/GetInvoicePaid") - .append("VEI_SN", VEI_SN) - .append("GroupNo", GroupNo) - .append("DateStart", DateStart) - .append("DateEnd", DateEnd) - .append("token",this.root.authStore.login.token) - .build(); - - return fetchJSON(fetchUrl).then(json => { - runInAction(() => { - this.loading = false; - if (json.errcode == 0) { - if (isNotEmpty(json.Result)) { - this.invoicePaid = json.Result.map((data, index) => { - return { - key: data.fl_id, - fl_finaceNo: data.fl_finaceNo, - fl_vei_sn: data.fl_vei_sn, - fl_year: data.fl_year, - fl_month: data.fl_month, - fl_memo: data.fl_memo, - fl_adddate: data.fl_adddate, - fl_addUserSn: data.fl_addUserSn, - fl_updateUserSn: data.fl_updateUserSn, - fl_updatetime: data.fl_updatetime, - fl_state: data.fl_state, - fl_paid: data.fl_paid, - fl_pic: data.fl_pic, - fcount: data.fcount, - pSum: data.pSum, - }; - }); - } else { - this.invoicePaid = []; - } - } else { - throw new Error(json.errmsg + ": " + json.errcode); - } - }); - }); - - } - - - fetchInvoicePaidDetail(VEI_SN,FLID){ - this.loading = true; - const fetchUrl = prepareUrl(HT_HOST + "/service-Cooperate/Cooperate/GetInvoicePaidDetail") - .append("VEI_SN", VEI_SN) - .append("fl_id", FLID) - .append("token",this.root.authStore.login.token) - .build(); - - return fetchJSON(fetchUrl).then(json => { - runInAction(() => { - this.loading = false; - if (json.errcode == 0) { - if (isNotEmpty(json.Result)) { - this.invoicePaidDetail = json.Result.map((data, index) => { - return { - key: data.fl2_id, - fl2_fl_id: data.fl2_fl_id, - fl2_GroupName: data.fl2_GroupName, - fl2_gri_sn: data.fl2_gri_sn, - fl2_gmd_sn: data.fl2_gmd_sn, - fl2_wl: data.fl2_wl, - fl2_ArriveDate: data.fl2_ArriveDate, - fl2_price: data.fl2_price, - fl2_state: data.fl2_state, - fl2_updatetime: data.fl2_updatetime, - fl2_updateUserSn: data.fl2_updateUserSn, - fl2_memo: data.fl2_memo, - fl2_memo2: data.fl2_memo2, - fl2_paid: data.fl2_paid, - fl2_pic: data.fl2_pic, - }; - }); - } else { - this.invoicePaidDetail = []; - } - } else { - throw new Error(json.errmsg + ": " + json.errcode); - } - }); - }); - } - - /* 测试数据 */ - //账单列表范例数据 - testData = [ - { - GSMSN: 449865, - gmd_gri_sn: 334233, - gmd_vei_sn: 628, - GetDate: "2023-04-2 00:33:33", - GMD_FillWorkers_SN: 8617, - GMD_FWks_LastEditTime: "2023-04-26 12:33:33", - GMD_VerifyUser_SN: 8928, - GMD_Dealed: 1, - GMD_VRequestVerify: 1, - TotalCount: 22, - LeftGDate: "2023-03-30 00:00:00", - GMD_FillWorkers_Name: "", - GroupName: " 中华游230501-CA230402033", - AllMoney: 3539, - FKState: 1, - GMD_Currency: "", - PersonNum: "1大1小", - VName: "", - }, - ]; -} - -// export default Invoice; diff --git a/src/stores/Products/Index.js b/src/stores/Products/Index.js new file mode 100644 index 0000000..02989f6 --- /dev/null +++ b/src/stores/Products/Index.js @@ -0,0 +1,131 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +import { fetchJSON, postForm, postJSON } from '@/utils/request'; +import { HT_HOST } from '@/config'; +import { groupBy } from '@/utils/commons'; + +export const searchAgencyAction = async (param) => { + const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_search`, param); + return errcode !== 0 ? [] : result; +}; + +export const copyAgencyDataAction = async (postbody) => { + const formData = new FormData(); + Object.keys(postbody).forEach((key) => { + formData.append(key, postbody[key]); + }); + const { errcode, result } = await postForm(`${HT_HOST}/agency/products/copy`, formData); + return errcode === 0 ? true : false; +}; + +export const getAgencyProductsAction = async (param) => { + const _param = { ...param, use_year: (param.use_year || '').replace('all', ''), audit_state: (param.audit_state || '').replace('all', '') }; + const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/travel_agency_products`, _param); + return errcode !== 0 ? { agency: {}, products: [] } : result; +}; + +/** + * + */ +export const addProductExtraAction = async (body) => { + const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_add`, body); + return errcode === 0 ? true : false; +}; + +/** + * + */ +export const delProductExtrasAction = async (body) => { + const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_del`, body); + return errcode === 0 ? true : false; +}; + +/** + * 获取指定产品的附加项目 + * @param {object} param { id, travel_agency_id, use_year } + */ +export const getAgencyProductExtrasAction = async (param) => { + const _param = { ...param, use_year: (param.use_year || '').replace('all', '') }; + const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras`, _param); + return errcode !== 0 ? [] : result; +}; + +export const postProductsQuoteAuditAction = async (auditState, quoteRow) => { + const postbody = { + audit_state: auditState, + id: quoteRow.id, + travel_agency_id: quoteRow.travel_agency_id, + }; + const formData = new FormData(); + Object.keys(postbody).forEach((key) => { + formData.append(key, postbody[key]); + }); + const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/quotation_audit`, formData); + return json; + // return errcode !== 0 ? {} : result; +}; + +export const postProductsAuditAction = async (auditState, infoRow) => { + const postbody = { + audit_state: auditState, + id: infoRow.id, + travel_agency_id: infoRow.travel_agency_id, + }; + const formData = new FormData(); + Object.keys(postbody).forEach((key) => { + formData.append(key, postbody[key]); + }); + const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/travel-agency-products-audit`, formData); + return json; + // const { errcode, result } = json; + // return errcode !== 0 ? {} : result; +}; + +const initialState = { + loading: false, + searchValues: {}, + agencyList: [], + activeAgency: {}, + agencyProducts: {}, +}; +export const useProductsStore = create( + devtools((set, get) => ({ + // 初始化状态 + ...initialState, + + // state actions + setLoading: (loading) => set({ loading }), + setSearchValues: (searchValues) => set({ searchValues }), + setAgencyList: (agencyList) => set({ agencyList }), + setActiveAgency: (activeAgency) => set({ activeAgency }), + setAgencyProducts: (agencyProducts) => set({ agencyProducts }), + + reset: () => set(initialState), + + // side effects + searchAgency: async (param) => { + const { setLoading, setAgencyList } = get(); + setLoading(true); + const res = await searchAgencyAction(param); + setAgencyList(res); + setLoading(false); + }, + + getAgencyProducts: async (param) => { + const { setLoading, setActiveAgency, setAgencyProducts } = get(); + setLoading(true); + const res = await getAgencyProductsAction(param); + const productsData = groupBy(res.products, (row) => row.info.product_type_id); + setAgencyProducts(productsData); + setActiveAgency(res.agency); + setLoading(false); + }, + + getAgencyProductExtras: async (param) => { + const res = await getAgencyProductExtrasAction(param); + // todo: + }, + })) +); +export default useProductsStore; diff --git a/src/utils/request.js b/src/utils/request.js index ac97803..b99451d 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -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: { diff --git a/src/views/App.jsx b/src/views/App.jsx index 7fdc984..7739d1a 100644 --- a/src/views/App.jsx +++ b/src/views/App.jsx @@ -75,6 +75,8 @@ function App() { theme={{ token: { colorPrimary: colorPrimary, + // "sizeStep": 3, + // "sizeUnit": 3, }, algorithm: theme.defaultAlgorithm, }}> @@ -117,6 +119,7 @@ function App() { isPermitted(PERM_OVERSEA) ? { key: 'feedback', label: {t('menu.Feedback')} } : null, isPermitted(PERM_OVERSEA) ? { key: 'report', label: {t('menu.Report')} } : null, isPermitted(PERM_AIR_TICKET) ? { key: 'airticket', label: {t('menu.Airticket')} } : null, + { key: 'products', label: {t('menu.Products')} }, { key: 'notice', label: ( diff --git a/src/views/account/Management.jsx b/src/views/account/Management.jsx index 4a32a66..08cb0b1 100644 --- a/src/views/account/Management.jsx +++ b/src/views/account/Management.jsx @@ -331,6 +331,7 @@ function Management() { username: { label: t('account:username') }, realname: { label: t('account:realname') }, }, + sort: { username: 1, realname: 2, dates: 3}, }} onSubmit={() => { handelAccountSearch() diff --git a/src/views/invoice/Index.jsx b/src/views/invoice/Index.jsx index 02033ed..559a718 100644 --- a/src/views/invoice/Index.jsx +++ b/src/views/invoice/Index.jsx @@ -1,6 +1,4 @@ import { NavLink, useNavigate } from "react-router-dom"; -import { useState } from "react"; -import { toJS } from "mobx"; import { Row, Col, Space, Button, Table, App, Steps } from "antd"; import { formatDate, isNotEmpty } from "@/utils/commons"; import { AuditOutlined, SmileOutlined, SolutionOutlined, EditOutlined } from "@ant-design/icons"; @@ -82,7 +80,7 @@ function Index() { fieldsConfig={{ shows: ['referenceNo', 'invoiceStatus', 'dates'], fieldProps: { - referenceNo: { col: 5 }, + referenceNo: { col: 7 }, invoiceStatus: { col: 4}, dates: { col: 10 }, }, @@ -103,7 +101,7 @@ function Index() { -
+
diff --git a/src/views/products/Audit.jsx b/src/views/products/Audit.jsx new file mode 100644 index 0000000..37d2bd5 --- /dev/null +++ b/src/views/products/Audit.jsx @@ -0,0 +1,216 @@ +import { useEffect, useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { App, Button, Collapse, Table, Space, Divider } from 'antd'; +import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets'; +import SecondHeaderWrapper from '@/components/SecondHeaderWrapper'; +import { useTranslation } from 'react-i18next'; +import useProductsStore, { postProductsQuoteAuditAction, } from '@/stores/Products/Index'; +import { isEmpty } from '@/utils/commons'; +// import PrintContractPDF from './PrintContractPDF'; + +const Header = ({ title, agency, refresh, ...props }) => { + const { travel_agency_id, use_year, audit_state } = useParams(); + const { t } = useTranslation(); + const [activeAgency] = useProductsStore((state) => [state.activeAgency]); + const { message, notification } = App.useApp(); + const handleAuditItem = (state, row) => { + postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id }) + .then((json) => { + if (json.errcode === 0) { + message.success(json.errmsg); + if (typeof refresh === 'function') { + refresh(); + } + } + }) + .catch((ex) => { + notification.error({ + message: 'Notification', + description: ex.message, + placement: 'top', + duration: 4, + }); + }); + }; + return ( +
+
+

{title}{(use_year || '').replace('all', '')}

+
+ {/* */} + {/* */} + {t('Edit')} + + {/* */} + + {/* todo: export, 审核完成之后才能导出 */} + + {/* */} +
+ ); +}; + +const PriceTable = ({ productType, dataSource, refresh }) => { + const { t } = useTranslation('products'); + const [loading, activeAgency] = useProductsStore((state) => [state.loading, state.activeAgency]); + const { message, notification } = App.useApp(); + const stateMapVal = useProductsAuditStatesMapVal(); + + // console.log(dataSource); + + const handleAuditPriceItem = (state, row) => { + postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id }) + .then((json) => { + if (json.errcode === 0) { + message.success(json.errmsg); + if (typeof refresh === 'function') { + refresh(); + } + } + }) + .catch((ex) => { + notification.error({ + message: 'Notification', + description: ex.message, + placement: 'top', + duration: 4, + }); + }); + }; + + const rowStyle = (r, tri) => { + const trCls = tri%2 !== 0 ? ' bg-stone-50' : ''; + const [infoI, quoteI] = r.rowSpanI; + const bigTrCls = quoteI === 0 && tri !== 0 ? 'border-collapse border-double border-0 border-t-4 border-stone-300' : ''; + return [trCls, bigTrCls].join(' '); + }; + + const columns = [ + { key: 'title', dataIndex: ['info', 'title'], width: '16rem', title: t('Title'), onCell: (r, index) => ({ rowSpan: r.rowSpan, }), className: 'bg-white', render: (text, r) => text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || '' }, + ...(productType === 'B' ? [{ key: 'km', dataIndex: ['info', 'km'], title: t('KM')}] : []), + { key: 'adult', title: t('AgeType.Adult'), render: (_, { adult_cost, currency, unit_id, unit_name }) => `${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` }, + { key: 'child', title: t('AgeType.Child'), render: (_, { child_cost, currency, unit_id, unit_name }) => `${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` }, + // {key: 'unit', title: t('Unit'), }, + { + key: 'groupSize', + dataIndex: ['group_size_min'], + title: t('GroupSize'), + render: (_, { group_size_min, group_size_max }) => `${group_size_min} - ${group_size_max}`, + }, + { + key: 'useDates', + dataIndex: ['use_dates_start'], + title: t('UseDates'), + render: (_, { use_dates_start, use_dates_end, weekdays }) => `${use_dates_start} ~ ${use_dates_end}`, // + (weekdays ? `, ${t('OnWeekdays')}${weekdays}` : ''), + }, + { key: 'weekdays', dataIndex: ['weekdays'], title: t('Weekdays'), render: (text, r) => text || t('Unlimited') }, + { + key: 'state', + title: t('State'), + render: (_, r) => { + return {stateMapVal[`${r.audit_state_id}`]?.label}; + }, + }, + { + title: '价格审核', + key: 'action', + render: (_, r) => + r.audit_state_id <= 0 ? ( + + + + + ) : null, + }, + ]; + return
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: ( + + 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 ; +}; + +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 ( + <> + } loading={loading} > + {/* debug: 0 */} + {/* */} + + + + + ); +}; +export default Audit; diff --git a/src/views/products/Detail.jsx b/src/views/products/Detail.jsx new file mode 100644 index 0000000..57cd8e7 --- /dev/null +++ b/src/views/products/Detail.jsx @@ -0,0 +1,733 @@ +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { Button, Card, Col, Row, Breadcrumb, Table, Popconfirm, Form, Input, InputNumber, Tag, Modal, Select, Tree } from 'antd'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import Date from '@/components/date'; +import { searchAgencyAction, getAgencyProductsAction } from '@/stores/Products/Index'; +import { useProductsTypes } from '@/hooks/useProductsSets'; +import Extras from './Detail/Extras'; +import { groupBy } from '@/utils/commons'; +import { useParams } from 'react-router-dom'; +import { useHTLanguageSets } from '@/hooks/useHTLanguageSets'; +import { useDefaultLgc } from '@/i18n/LanguageSwitcher'; +import BatchImportPrice from '@/components/BatchImportPrice'; +function Detail() { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [editingid, setEditingid] = useState(''); + const [tags, setTags] = useState([]); + const [isModalVisible, setIsModalVisible] = useState(false); + const [selectedTag, setSelectedTag] = useState(null); + const [saveData, setSaveData] = useState(null); + const [datePickerVisible, setDatePickerVisible] = useState(false); + const [batchImportPriceVisible, setBatchImportPriceVisible] = useState(false); + const [currentid, setCurrentid] = useState(null); + const [languageStatus, setLanguageStatus] = useState(null); + const [selectedNodeid, setSelectedNodeid] = useState(null); + const [remainderLanguage, setRemainderLanguage] = useState([]) + const [selectedDateData, setSelectedDateData] = useState({ dateRange: null, selectedDays: [] }); + const [treeData, setTreeData] = useState([]); + const productsTypes = useProductsTypes(); + const [productsData, setProductsData] = useState(null); + const [quotation, setQuotation] = useState(null); + const [lgc_details, setLgc_details] = useState(null); + const [languageLabel, setLanguageLabel] = useState(null); + const { travel_agency_id } = useParams(); + const { language } = useDefaultLgc(); + const HTLanguageSets = useHTLanguageSets(); + const { Search } = Input; + + + const [expandedKeys, setExpandedKeys] = useState([]); + const [searchValue, setSearchValue] = useState(''); + const [autoExpandParent, setAutoExpandParent] = useState(true); + const [dataList, setDataList] = useState([]); + const [defaultData, setDefaultData] = useState([]); + + + const productProject = { + "6": [], + "B": [ + { code: "code", name: t('products:Code') }, + { code: "city_name", name: t('products:City') }, + { code: "km", name: t('products:KM') }, + { code: "remarks", name: t('products:Remarks') } + ], + "J": [ + { code: "code", name: t('products:Code') }, + { code: "city_name", name: t('products:City') }, + { code: "recommends_rate", name: t('products:recommendationRate') }, + { code: "duration", name: t('products:Duration') }, + { code: "dept_name", name: t('products:Dept') }, + { code: "display_to_c", name: t('products:DisplayToC') }, + { code: "remarks", name: t('products:Remarks') }, + ], + "Q": [ + { code: "code", name: t('products:Code') }, + { code: "city_name", name: t('products:City') }, + { code: "recommends_rate", name: t('products:recommendationRate') }, + { code: "duration", name: t('products:Duration') }, + { code: "dept_name", name: t('products:Dept') }, + { code: "display_to_c", name: t('products:DisplayToC') }, + { code: "remarks", name: t('products:Remarks') }, + ], + "D": [ + { code: "code", name: t('products:Code') }, + { code: "city_name", name: t('products:City') }, + { code: "recommends_rate", name: t('products:recommendationRate') }, + { code: "duration", name: t('products:Duration') }, + { code: "dept_name", name: t('products:Dept') }, + { code: "display_to_c", name: t('products:DisplayToC') }, + { code: "remarks", name: t('products:Remarks') }, + ], + "7": [ + { code: "code", name: t('products:Code') }, + { code: "city_name", name: t('products:City') }, + { code: "recommends_rate", name: t('products:recommendationRate') }, + { code: "duration", name: t('products:Duration') }, + { code: "open_weekdays", name: t('products:OpenWeekdays') }, + { code: "remarks", name: t('products:Remarks') }, + ], + "R": [ + { code: "code", name: t('products:Code') }, + { code: "city_name", name: t('products:City') }, + ] + } + const [selectedCategory, setSelectedCategory] = useState(productProject.B); + + useEffect(() => { + setLanguageStatus(language); + const matchedLanguage = HTLanguageSets.find(HTLanguage => HTLanguage.key === language.toString()); + const languageLabel = matchedLanguage.label + // setTags([languageLabel]) + setLanguageLabel(languageLabel) + setSelectedTag(languageLabel) + setRemainderLanguage(HTLanguageSets.filter(item => item.key !== language.toString())) + }, []); + + + + + useEffect(() => { + const fetchData = async () => { + const a = { travel_agency_id }; + const res = await getAgencyProductsAction(a); + const groupedProducts = groupBy(res.products, (row) => row.info.product_type_id); + + const generateTreeData = (productsTypes, productsData) => { + return productsTypes.map(type => ({ + title: type.label, + key: type.value, + selectable: false, + children: (productsData[type.value] || []).map(product => ({ + title: product.info.title, + key: `${type.value}-${product.info.id}`, + })) + })); + }; + + const treeData = generateTreeData(productsTypes, groupedProducts); + setTreeData(treeData); + setProductsData(groupedProducts); + setDefaultData(treeData); + setDataList(flattenTreeData(treeData)); + }; + + fetchData(); + }, [productsTypes]); + + const flattenTreeData = (tree) => { + let flatList = []; + const flatten = (nodes) => { + nodes.forEach((node) => { + flatList.push({ title: node.title, key: node.key }); + if (node.children) { + flatten(node.children); + } + }); + }; + flatten(tree); + return flatList; + }; + + const getParentKey = (key, tree) => { + let parentKey; + for (let i = 0; i < tree.length; i++) { + const node = tree[i]; + if (node.children) { + if (node.children.some((item) => item.key === key)) { + parentKey = node.key; + } else { + const pKey = getParentKey(key, node.children); + if (pKey) { + parentKey = pKey; + } + } + } + } + return parentKey; + }; + + const titleRender = (node) => { + const index = node.title.indexOf(searchValue); + const beforeStr = node.title.substr(0, index); + const afterStr = node.title.substr(index + searchValue.length); + const highlighted = ( + {searchValue} + ); + + return index > -1 ? ( + + {beforeStr} + {highlighted} + {afterStr} + + ) : ( + {node.title} + ); + }; + + const onChange = (e) => { + const { value } = e.target; + const newExpandedKeys = dataList + .filter(item => item.title.includes(value)) + .map(item => getParentKey(item.key, defaultData)) + .filter((item, i, self) => item && self.indexOf(item) === i); + console.log("newExpandedKeys", newExpandedKeys) + setExpandedKeys(newExpandedKeys); + setSearchValue(value); + setAutoExpandParent(true); + }; + + const onExpand = (keys) => { + setExpandedKeys(keys); + setAutoExpandParent(false); + }; + + const isEditing = (record) => record.id === editingid; + + const edit = (record) => { + form.setFieldsValue({ ...record }); + setEditingid(record.id); + }; + + const cancel = () => { + setEditingid(''); + }; + + const handleSave = async (id) => { + try { + const { info, ...restRow } = await form.validateFields(); + const newData = [...quotation]; + const index = newData.findIndex((item) => id === item.id); + if (index > -1) { + const item = newData[index]; + newData.splice(index, 1, { ...item, ...restRow }); + delete newData[index].quotation + delete newData[index].extras + setQuotation(newData); + setEditingid(''); + } else { + newData.push(restRow); + setQuotation(newData); + setEditingid(''); + } + } catch (errInfo) { + console.log('Validate Failed:', errInfo); + } + }; + + const handleDelete = (id) => { + const newData = [...quotation]; + const index = newData.findIndex((item) => id === item.id); + newData.splice(index, 1); + setQuotation(newData); + }; + + const handleAdd = () => { + const newData = { + id: `${quotation.length + 1}`, + value: '', + currency: '', + unit_name: '', + weekdays: '', + use_dates_start: '', + use_dates_end: '', + group_size_min: '', + group_size_max: '' + }; + setQuotation([...quotation, newData]); + }; + + const handleBatchImport = () => { + setBatchImportPriceVisible(true); + } + + const handleDateSelect = (id) => { + setCurrentid(id); + setDatePickerVisible(true); + }; + + const handleDateChange = ({ dateRange, selectedDays }) => { + + // 计算周末 + const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + let weekDayCount = selectedDays.map(day => weekdays.indexOf(day) + 1).sort().join(','); + if (!weekDayCount || weekDayCount.length === 0) { + weekDayCount = "全年"; + } + const newData = [...quotation]; + const index = newData.findIndex((item) => currentid === item.id); + if (index > -1) { + newData[index].weekdays = weekDayCount; + setQuotation(newData); + } + + setSelectedDateData({ dateRange, selectedDays }) + }; + + const handleDateOk = () => { + const { dateRange } = selectedDateData; + const dateRangeList = dateRange.split('-'); + const use_dates_start = dateRangeList[0]; + const use_dates_end = dateRangeList[1]; + if (currentid !== null) { + const newData = [...quotation]; + const index = newData.findIndex((item) => currentid === item.id); + if (index > -1) { + newData[index].use_dates_start = use_dates_start; + newData[index].use_dates_end = use_dates_end; + setQuotation(newData); + setCurrentid(null); + } + } + setDatePickerVisible(false); + } + +const handleBatchImportOK = () => { + setBatchImportPriceVisible(false); +} + + + const EditableCell = ({ editing, dataIndex, title, inputType, record, children, handleDateSelect, ...restProps }) => { + let inputNode = inputType === 'number' ? : ; + if (dataIndex === 'validityPeriod' && editing) { + return ( + + ); + } + + if (dataIndex === 'unit_name' && editing) { + inputNode = ( + + ); + } + + if (dataIndex === 'currency' && editing) { + inputNode = ( + + ); + } + + if (dataIndex === 'group_size' && editing) { + return ( + + ); + } + + return ( + + ); + }; + + const handleInputGroupSize = (name, id, dataIndex, value) => { + + const newData = [...quotation]; + const index = newData.findIndex((item) => id === item.id); + if (index > -1) { + const item = newData[index]; + newData[index] = { ...item, } + if (name === 'group_size_min') { + newData[index] = { ...item, group_size_min: value }; + } else { + newData[index] = { ...item, group_size_max: value }; + } + setQuotation(newData); + } + + } + + const columns = [ + { title: t('products:adultPrice'), dataIndex: 'adult_cost', width: '10%', editable: true }, + { title: t('products:childrenPrice'), dataIndex: 'child_cost', width: '10%', editable: true }, + { title: t('products:currency'), dataIndex: 'currency', width: '10%', editable: true }, + { title: t('products:Types'), dataIndex: 'unit_name', width: '10%', editable: true }, + { + title: t('products:number'), + dataIndex: 'group_size', + width: '20%', + editable: true, + render: (_, record) => `${record.group_size_min}-${record.group_size_max}` + }, + + { + title: t('products:validityPeriod'), + dataIndex: 'validityPeriod', + width: '20%', + editable: true, + render: (_, record) => `${record.use_dates_start}-${record.use_dates_end}` + }, + + { title: t('products:Weekdays'), dataIndex: 'weekdays', width: '10%' }, + + { + title: t('products:operation'), + dataIndex: 'operation', + render: (_, record) => { + const editable = isEditing(record); + return editable ? ( + + handleSave(record.id)} style={{ marginRight: 8 }}>{t('products:save')} + {t('products:cancel')} + + ) : ( + + edit(record)} style={{ marginRight: 8 }}>{t('products:edit')} + handleDelete(record.id)}> + {t('products:delete')} + + + ); + }, + }, + + ]; + + + const mergedColumns = columns.map((col) => { + if (!col.editable) { + return col; + } + return { + ...col, + onCell: (record) => ({ + record, + inputType: col.dataIndex === 'age' ? 'number' : 'text', + dataIndex: col.dataIndex, + title: col.title, + editing: isEditing(record), + handleDateSelect: handleDateSelect, + }), + }; + }); + + const handleTagClick = (tag) => { + setSelectedTag(tag); + const matchedLanguage = HTLanguageSets.find(language => language.label === tag); + const key = matchedLanguage ? matchedLanguage.key : null; + form.setFieldsValue({ + lgc_details: { + title: lgc_details[key] ? lgc_details[key].title : '', + descriptions: lgc_details[key] ? lgc_details[key].descriptions : '' + } + }); + setLanguageStatus(key) + + }; + + const showModal = () => setIsModalVisible(true); + + const handleOk = () => { + if (!selectedTag) return; + if (!remainderLanguage.some(item => item.label === selectedTag)) return; + if (remainderLanguage.includes(selectedTag)) return; + let tempRemainderLanguage = remainderLanguage.filter((item)=>{ + return item.label !== selectedTag; + }) + setRemainderLanguage(tempRemainderLanguage) + setTags([...tags, selectedTag]) + setSelectedTag(null); + setIsModalVisible(false); + } + + const handleCancel = () => setIsModalVisible(false); + + + const handleTagChange = (value) => { + console.log("handleTagChange", value) + setSelectedTag(value); + console.log("setSelectedTag", selectedTag) + }; + + const handleChange = (field, value) => { + console.log("languageStatus", languageStatus) + console.log("...lgc_details[languageStatus]", { ...lgc_details[languageStatus] }) + // 更新整个 lgc_details 对象 + const updatedLgcDetails = { + ...lgc_details, + [languageStatus]: { ...lgc_details[languageStatus], [field]: value, lgc: languageStatus } + }; + setLgc_details(updatedLgcDetails) + console.log("AAAAAAAAAAAAAA", lgc_details); + + }; + + //树组件方法 + const handleNodeSelect = (_, { node }) => { + setTags([languageLabel]) + // 如果点击的是同一个节点,不做任何操作 + if (selectedNodeid === node.key) return; + const fatherKey = node.key.split('-')[0]; + setSelectedCategory(productProject[fatherKey]) + + console.log("remainderLanguage",remainderLanguage) + let initialQuotationData = null; + let infoData = null; + let lgcDetailsData = null; + productsData[fatherKey].forEach(element => { + if (element.info.title === node.title) { + initialQuotationData = element.quotation; + infoData = element.info; + lgcDetailsData = element.lgc_details; + return true; + } + }); + + console.log("infoData", infoData) + + // 累积 lgc_details 数据 + let newLgcDetails = {}; + lgcDetailsData.forEach(element => { + newLgcDetails[element.lgc] = element; + }); + + // 一次性更新 lgc_details + setLgc_details(newLgcDetails); + + setQuotation(initialQuotationData); + + console.log("descriptions", lgc_details) + // 使用 setTimeout 确保 lgc_details 已经更新 + form.setFieldsValue({ + info: { + title: infoData.title, + code: infoData.code, + product_type_name: infoData.product_type_name, + city_name: infoData.city_name, + remarks: infoData.remarks, + open_weekdays: infoData.open_weekdays, + recommends_rate: infoData.recommends_rate, + duration: infoData.duration, + dept: infoData.dept, + km: infoData.km, + dept_name: infoData.dept_name + }, + lgc_details: { + title: newLgcDetails[language]?.title || '', + descriptions: newLgcDetails[language]?.descriptions || '' + } + }); + }; + + const onSave = (values) => { + const tempData = values; + tempData['quotation'] = quotation; + // tempData['extras'] = bindingData; + // tempData['lgc_details'] = languageStatus; + setSaveData(tempData); + console.log("保存的数据", tempData) + }; + + return ( +
+ +
+ + + + + + + + + 供应商 }, + { title: 综费 }, + { title: '文章列表' } + ]} /> + } + > +

{t('products:productProject')}

+ + {selectedCategory.map((item, index) => ( +
+ + {item.code === "duration" ? ( + + ) : ( + + )} + + + ))} + + + + {tags.map(tag => ( + handleTagClick(tag)} + color={tag === selectedTag ? 'blue' : undefined} + style={{ cursor: 'pointer' }} + > + {tag} + + ))} + + + + }> + + + + + + handleChange('title', e.target.value)} + /> + + + handleChange('descriptions', e.target.value)} + /> + + + + + +

{t('products:supplierQuotation')}

+ +
+ {children} + + + handleInputGroupSize('group_size_min', record.id, 'group_size', value)} + style={{ width: '50%', marginRight: '10px' }} + /> + - + handleInputGroupSize('group_size_max', record.id, 'group_size', value)} + style={{ width: '50%', marginLeft: '10px' }} + /> + + {editing ? ( + + {inputNode} + + ) : ( + children + )} +
+ + + + + + + + + + + + + + + + + + {datePickerVisible && ( + setDatePickerVisible(false)} + > + + + )} + + { + batchImportPriceVisible && ( + setBatchImportPriceVisible(false)} + width="80%" + > + + + ) + } + + ); +} +export default Detail; + + diff --git a/src/views/products/Detail/Extras.jsx b/src/views/products/Detail/Extras.jsx new file mode 100644 index 0000000..0f2d23c --- /dev/null +++ b/src/views/products/Detail/Extras.jsx @@ -0,0 +1,175 @@ +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { App, Table, Button, Modal, Popconfirm } from 'antd'; +import { getAgencyProductExtrasAction, getAgencyProductsAction, addProductExtraAction, delProductExtrasAction } from '@/stores/Products/Index'; +import { cloneDeep, pick } from '@/utils/commons'; +import SearchForm from '@/components/SearchForm'; + +import RequireAuth from '@/components/RequireAuth'; +import { PERM_PRODUCTS_MANAGEMENT } from '@/config'; +import { useProductsTypesMapVal } from '@/hooks/useProductsSets'; + +const NewAddonModal = ({ onPick, ...props }) => { + const { travel_agency_id, use_year } = useParams(); + const { t } = useTranslation(); + const { notification, message } = App.useApp(); + + const productsTypesMapVal = useProductsTypesMapVal(); + + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); // bind loading + const [searchLoading, setSearchLoading] = useState(false); + const [searchResult, setSearchResult] = useState([]); + + const onSearchProducts = async (values) => { + const copyObject = cloneDeep(values); + const { starttime, endtime, ...param } = copyObject; + setSearchLoading(true); + setSearchResult([]); + // debug: audit_state: '1', + const result = await getAgencyProductsAction({ ...param, audit_state: '0', travel_agency_id, use_year }); + setSearchResult(result?.products || []); + setSearchLoading(false); + }; + const handleAddExtras = async (item) => { + if (typeof onPick === 'function') { + onPick(item); + } + }; + + // todo: 如何显示价格表 + const searchResultColumns = [ + { key: 'ptype', dataIndex: ['info', 'product_type_id'], width: '6rem', title: t('products:ProductType'), render: (text, r) => productsTypesMapVal[text].label }, + { key: 'title', dataIndex: ['info', 'title'], width: '16rem', title: t('products:Title') }, + { + title: t('products:price'), + dataIndex: ['quotation', '0', 'adult_cost'], + width: '10rem', + render: (_, { quotation }) => `${quotation[0].adult_cost} ${quotation[0].currency} / ${quotation[0].unit_name}`, + }, + { + key: 'action', + title: '', + width: 150, + render: (_, record) => ( + + ), + }, + ]; + 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 _item = pick(item.info, ['id', 'title', 'code']); + const newSuccess = await addProductExtraAction({ travel_agency_id, id: productId, extras: [_item] }); + newSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`); + await handleGetAgencyProductExtras(); + } + + const handleDelAddon = async (item) => { + const delSuccess = await delProductExtrasAction({ travel_agency_id, id: productId, extras: [item.info.id] }); + delSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`); + await handleGetAgencyProductExtras(); + }; + + useEffect(() => { + handleGetAgencyProductExtras(); + + return () => {}; + }, []); + + const columns = [ + { title: t('products:Title'), dataIndex: ['info', 'title'], width: '16rem', }, + { + title: t('products:Offer'), + dataIndex: ['quotation', '0', 'value'], + width: '10rem', + + render: (_, { quotation }) => `${quotation[0].adult_cost} ${quotation[0].currency} / ${quotation[0].unit_name}`, // todo: 成人 儿童 + }, + // { title: t('products:Types'), dataIndex: 'age_type', width: '40%', }, + { + title: '', + dataIndex: 'operation', + width: '4rem', + render: (_, r) => ( + handleDelAddon(r)} okText={t('Yes')} > + + + ), + }, + ]; + + return ( + <> + +

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

+
r.info.id} /> + + + + ); +}; +export default Extras; diff --git a/src/views/products/Manage.jsx b/src/views/products/Manage.jsx new file mode 100644 index 0000000..524a4ea --- /dev/null +++ b/src/views/products/Manage.jsx @@ -0,0 +1,121 @@ +import { useEffect, useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { App, Space, Table, Button, Modal } from 'antd'; +import SearchForm from '@/components/SearchForm'; +import dayjs from 'dayjs'; +import { useTranslation } from 'react-i18next'; +import useProductsStore, { copyAgencyDataAction } from '@/stores/Products/Index'; +import useFormStore from '@/stores/Form'; +import { objectMapper } from '@/utils/commons'; + +function Index() { + const { notification, message } = App.useApp(); + const navigate = useNavigate() + const { t } = useTranslation(); + const [loading, agencyList, searchAgency] = useProductsStore((state) => [state.loading, state.agencyList, state.searchAgency]); + const [searchValues, setSearchValues] = useProductsStore((state) => [state.searchValues, state.setSearchValues]); + const formValuesToSub = useFormStore(state => state.formValuesToSub); + + const handleSearchAgency = (formVal = undefined) => { + const { starttime, endtime, ...param } = formVal || formValuesToSub; + const searchParam = objectMapper(param, { agency: 'travel_agency_ids', startdate: 'edit_date1', enddate: 'edit_date2', year: 'use_year' }); + setSearchValues(searchParam); + searchAgency(searchParam); + } + + const [copyModalVisible, setCopyModalVisible] = useState(false); + const [sourceAgency, setSourceAgency] = useState({}); + const [copyLoading, setCopyLoading] = useState(false); + const handleCopyAgency = async (param) => { + setCopyLoading(true); + const postbody = objectMapper(param, { agency: 'target_agency', }); + const toID = postbody.target_agency; + const success = await copyAgencyDataAction({...postbody, source_agency: sourceAgency.travel_agency_id}); + setCopyLoading(false); + success ? message.success('复制成功') : message.error('复制失败'); + + setCopyModalVisible(false); + navigate(`/products/${toID}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`); + }; + + const openCopyModal = (from) => { + setSourceAgency(from); + setCopyModalVisible(true); + }; + + useEffect(() => { + // handleSearchAgency(); + }, []); + + const showTotal = (total) => t('Table.Total', { total }); + + const columns = [ + { title: t('products:Vendor'), key: 'vendor', dataIndex: 'travel_agency_name' }, + { title: t('products:CreatedBy'), key: 'created_by', dataIndex: 'created_by_name' }, + { title: t('products:CreateDate'), key: 'create_date', dataIndex: 'create_date' }, + { title: t('products:AuState'), key: 'audit_state', dataIndex: 'audit_state' }, + { title: t('products:AuditedBy'), key: 'audited_by', dataIndex: 'audited_by_name' }, + { title: t('products:AuditDate'), key: 'audit_date', dataIndex: 'audit_date' }, + { + title: '', + key: 'action', + render: (_, r) => ( + + {t('Edit')} + {t('Audit')} + + + ), + }, + ]; + return ( + + { + handleSearchAgency(formVal); + }} + /> +
+ + {/* 复制弹窗 */} + setCopyModalVisible(false)} destroyOnClose> +
复制源: {sourceAgency.travel_agency_name}
+ { + handleCopyAgency(formVal); + }} + /> +
+ + ); +} + +export default Index;