diff --git a/package.json b/package.json index 1ee26a5..5413910 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.1.1", - "react-router-dom": "^6.23.1" + "react-router-dom": "^6.23.1", + "read-excel-file": "^5.8.5" }, "devDependencies": { "@types/file-saver": "^2.0.7", diff --git a/src/assets/index.less b/src/assets/index.less index 26bf71f..ba39b66 100644 --- a/src/assets/index.less +++ b/src/assets/index.less @@ -201,9 +201,98 @@ body #root{ } } } +.semi-popconfirm{ + .semi-popconfirm-inner{ + padding: 15px 15px; + min-width: 260px; + } + .semi-popconfirm-header{ + //align-items: center; + margin-bottom: 5px; + } + .semi-popconfirm-header-icon{ + width: 18px; + } + .semi-popconfirm-header-title{ + line-height: 16px; + } + .semi-icon-extra-large{ + font-size: 20px; + } + .semi-popconfirm-body-withIcon{ + margin-left: 30px; + } + .semi-popconfirm-footer{ + margin-top: 15px; + .semi-button{ + //padding: 2px 10px; + //line-height: 16px; + height: 28px; + } + } +} +.upload-wrapper{ + position: relative; + .semi-upload-file-list-title{ + display: none; + } + .semi-upload-file-list{ + //flex-basis: auto; + margin: 0; + left:170px; + top:0px; + //top:40px; + position: absolute; + } + .semi-upload-file-card{ + height: auto; + border-radius: var(--semi-border-radius-small); + --semi-color-fill-0: rgba(0,0,0,0.05) + } + .semi-upload-file-card-preview-placeholder{ + width: 24px; + height: 22px; + margin: 5px; + } + .semi-icon-file{ + font-size: 16px; + } +} /************ end overrides ****************/ - +.bill-pdf-previewer{ + border-radius: 5px; + overflow: hidden; + .bill-pdf-container{ + height: 550px; + overflow: hidden; + width: 100%; + border-radius: 5px; + border:none; + display: block; + } +} +.import-record-wrapper{ + margin: 15px 0; + .table-list{ + max-width: 100%; + overflow: auto; + max-height: 350px; + } + table{ + width: 100%; + border-collapse: collapse; + tr{ + &:first-child td{ + white-space: nowrap; + } + } + td{ + padding: 6px 10px; + border:solid 1px #eee; + } + } +} .dashboard-menu-container { padding: var(--dashboard-layout-padding, 15px); position: sticky; diff --git a/src/hooks/usePaymentChannels.ts b/src/hooks/usePaymentChannels.ts new file mode 100644 index 0000000..ea619cd --- /dev/null +++ b/src/hooks/usePaymentChannels.ts @@ -0,0 +1,42 @@ +import {useState} from "react"; + + + + +export function usePaymentChannels(){ + //_setPaymentChannelList + const [paymentChannelList] = useState([ + {value: 'FLYWIRE', label: 'FlyWire'}, + {value: 'PPS', label: 'PPS'}, + {value: 'CBP', label: 'CBP'}, + // {value:'CASH', label:'Cash'}, + // {value: 'ASIAPAY', label: 'AsiaPay'}, + // {value: 'OTHER', label: 'Other'}, + ]) + //_setPaymentChannelList + const [paymentMethodList] = useState([ + {value: 'card', label: 'Card(VISA,MasterCard,UnionPay,JCB...)'}, + {value: 'wechat', label: 'Wechat'}, + {value: 'alipay', label: 'Alipay'}, + {value:'cash', label:'Cash'}, + {value: 'other', label: 'Other'}, + ]) + + // useEffect(()=>{ + // if(BillTypes.length == 0){ + // selectBillTypeList().then(ret => { + // const types = ret.filter(it=>!it.description.toUpperCase().startsWith('ADJUSTMENT')) + // .map(it=>({value: it.type, label: it.description})) + // setBillTypes(types) + // // 避免出现多次 + // BillTypesCache.length = 0; + // BillTypesCache.push(...types) + // }) + // } + // },[]) + + return { + paymentChannelList, + paymentMethodList + } +} \ No newline at end of file diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index fcb94ff..ff9b2a1 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -7,12 +7,13 @@ "close": "Close", "confirm": "Confirm", "confirm_delete": "Please confirm delete record", - "confirm_next_operation": "Please confirm this operation", + "confirm_import": "Confirm Import", + "confirm_next_operation": "Are you sure to process this action", "confirm_paid": "Confirm paid", "copy-pay-url": "Copy payment link", "delete": "Delete", "operate_fail": "Operation failed", - "operate_success": "Operation success", + "operate_success": "Process success", "please_enter": "Please Enter", "please_select": "Please Select", "please_select_bill_type": "Please Select Bill Type", @@ -20,12 +21,20 @@ "query_bill": "Failed to query bill:", "remove": "Remove", "save": "Save", + "select_excel_file": "Select File", + "select_upload_file": "Select File", "student_number": "Student Number", + "validate": { + "email": "Email format is incorrect", + "error_details_message": "Please set bill details" + }, "warning": "Warning" }, "bill": { + "add_bill_record": "Add Transaction Record", "bill_date": "In", "bill_number": "Bill Number", + "btn_cancel_confirm": "Cancel Confirm", "cancel": "Cancel", "cancel_confirm": "Please make sure to cancel the bill", "cancel_confirm_bills": "Confirm the check check bill?", @@ -44,13 +53,17 @@ "confirm_student_number": "Confirm Student Number", "confirm_success": "Confirm success!", "confirmed": "Confirmed", + "create": { + "confirm": "Confirm Add", + "pay_area": "Payment Area" + }, "delivered_status": "Delivered Status", "delivered_status_no": "Undivided", "delivered_status_yes": "Delivered", "download-qr-code": "Download QR Code", "download_receipt": "Download receipt", "export_excel": "Export Transaction Excel", - "import_bill": "Add Transaction Record", + "import_bill": "Import Transaction Record", "import_excel": "Import Transaction Excel", "paid": "Paid", "paid_confirm": "Please confirm the order status is set to paid", diff --git a/src/i18n/translations/sc.json b/src/i18n/translations/sc.json index 041ea45..a567f6e 100644 --- a/src/i18n/translations/sc.json +++ b/src/i18n/translations/sc.json @@ -7,6 +7,7 @@ "close": "关闭", "confirm": "确定", "confirm_delete": "请确认是否删除此数据", + "confirm_import": "确认导入", "confirm_next_operation": "请确认是否进行此操作", "confirm_paid": "确认已支付", "copy-pay-url": "复制支付链接", @@ -20,12 +21,20 @@ "query_bill": "查询账单失败:", "remove": "删除", "save": "保存", + "select_excel_file": "选择文件", + "select_upload_file": "选择文件", "student_number": "学号", + "validate": { + "email": "Email格式不正确", + "error_details_message": "请设置账单详情" + }, "warning": "警告" }, "bill": { + "add_bill_record": "添加交易记录", "bill_date": "开始支付时间", "bill_number": "账单编号", + "btn_cancel_confirm": "取消确认", "cancel": "作废", "cancel_confirm": "确定作废此账单", "cancel_confirm_bills": "确认对账选中账单?", @@ -44,13 +53,17 @@ "confirm_student_number": "确认学号", "confirm_success": "对账成功!", "confirmed": "已对账", + "create": { + "confirm": "确认添加", + "pay_area": "支付区域" + }, "delivered_status": "分账状态", "delivered_status_no": "未分账", "delivered_status_yes": "已分账", "download-qr-code": "下载二维码", "download_receipt": "下载收据", "export_excel": "导出交易记录", - "import_bill": "添加交易记录", + "import_bill": "导入交易记录", "import_excel": "导入交易记录", "paid": "已支付", "paid_confirm": "是否将此订单状态设为已支付", diff --git a/src/i18n/translations/tc.json b/src/i18n/translations/tc.json index 2fceb31..a2dd0b8 100644 --- a/src/i18n/translations/tc.json +++ b/src/i18n/translations/tc.json @@ -7,6 +7,7 @@ "close": "關閉", "confirm": "確定", "confirm_delete": "請確認是否刪除此數據", + "confirm_import": "確認導入", "confirm_next_operation": "請確認是否進行此操作", "confirm_paid": "確認已支付", "copy-pay-url": "複製付款連結", @@ -20,12 +21,20 @@ "query_bill": "查詢帳單失敗:", "remove": "刪除", "save": "儲存", + "select_excel_file": "選擇文件", + "select_upload_file": "選擇文件", "student_number": "學號", + "validate": { + "email": "Email格式不正確", + "error_details_message": "請設定帳單詳情" + }, "warning": "警告" }, "bill": { + "add_bill_record": "新增交易記錄", "bill_date": "開始支付時間", "bill_number": "帳單編號", + "btn_cancel_confirm": "取消確認", "cancel": "作廢", "cancel_confirm": "確定作廢此帳單", "cancel_confirm_bills": "確認對帳選取帳單?", @@ -44,13 +53,17 @@ "confirm_student_number": "確認學號", "confirm_success": "對帳成功!", "confirmed": "已對帳", + "create": { + "confirm": "確認新增", + "pay_area": "支付區域" + }, "delivered_status": "分帳狀態", "delivered_status_no": "未分帳", "delivered_status_yes": "已分賬", "download-qr-code": "下載二維碼", "download_receipt": "下載收據", "export_excel": "導出交易记录", - "import_bill": "新增交易记录", + "import_bill": "導入交易記錄", "import_excel": "導入交易记录", "paid": "已支付", "paid_confirm": "是否將此訂單狀態設為已支付", diff --git a/src/pages/bill/components/add_bill_modal.tsx b/src/pages/bill/components/add_bill_modal.tsx index 8d47f07..b00b85e 100644 --- a/src/pages/bill/components/add_bill_modal.tsx +++ b/src/pages/bill/components/add_bill_modal.tsx @@ -1,60 +1,173 @@ -import {Button, Col, Form, Modal, Row, Select, Space} from "@douyinfe/semi-ui"; +import {Button, Col, Divider, Form, InputNumber, Modal, Row, Select, Space} from "@douyinfe/semi-ui"; +import {IconAlertCircle} from "@douyinfe/semi-icons"; import React from "react"; import {useTranslation} from "react-i18next"; import {useSetState} from "ahooks"; + import {useBillTypes} from "@/hooks/useBillTypes.ts"; +import {usePaymentChannels} from "@/hooks/usePaymentChannels.ts"; +import {addBillRecord} from "@/service/api/bill.ts" +import dayjs from "dayjs"; type BillPaidModalProps = { onConfirm: () => void onCancel?: () => void } +type BillTypeListProps = { + onClose?: (refresh?: boolean) => void; + onChange?: (confirms: ConfirmedBillDetail[]) => void; +} + +export const BillTypeList: React.FC = (props) => { + const {t} = useTranslation() + const [state, setState] = useSetState<{ + confirmed: ConfirmedBillDetail[] + }>({ + confirmed: [{bill_type: '', amount: 0}] + }) + const BillTypes = useBillTypes() + + const onChange = (value: string, index: number, type: 'type' | 'amount') => { + if (state.confirmed.length <= index || !value) return; + const confirmed = [...state.confirmed] + if (type == 'type') { + confirmed[index].bill_type = value + } else { + confirmed[index].amount = Number(value) + } + + setState({confirmed}) + props.onChange?.(confirmed) + } + + const addOrRemove = (index: number) => { + // 不允许删除最后一个 + if (index > -1 && state.confirmed.length <= 1) return; + const confirmed = [...state.confirmed, ...(index == -1 ? [{bill_type: '', amount: 0}] : [])] + if (index > -1) confirmed.splice(index, 1) + setState({confirmed}) + props.onChange?.(confirmed) + } + + + return (
+ {t('bill.title_bill_detail')} + { + state.confirmed.map((item, index) => ( +
+ + + + onChange(String(v), index, 'amount')} style={{width: 140}}/> + + +
+ )) + } +
+ +
+
) +} + export const AddBillModal: React.FC = (props) => { const {t} = useTranslation() - const BillTypes = useBillTypes() + const {paymentChannelList, paymentMethodList} = usePaymentChannels(); const [state, setState] = useSetState<{ loading?: boolean; - open?:boolean - }>({}) + open?: boolean; + details: ConfirmedBillDetail[]; + errorMessage?: string; + }>({ + details: [] + }) - const onSubmit = (values: BillUpdateParams) => { + const onSubmit = (values: CreateBillRecordModel) => { + if (state.details.length == 0) { + setState({errorMessage: t('base.validate.error_details_message')}) + return; + } + for (const it of state.details) { + if (!it.bill_type || it.amount <= 0) { + setState({errorMessage: t('base.validate.error_details_message')}) + return; + } + } setState({ - loading: true + loading: true, errorMessage: undefined + }) + values.details = state.details + values.check_student = true; + values.initiated_paid_date = dayjs(values.initiated_paid_date).format("YYYY-MM-DD") + values.paid_date = dayjs(values.paid_date).format("YYYY-MM-DD") + values.delivered_date = dayjs(values.delivered_date).format("YYYY-MM-DD") + addBillRecord(values).then(()=>{ + setState({open:false}) + props.onConfirm() + }).catch(e => { + setState({errorMessage: e.message}) + }).finally(() => { + setState({ + loading: false + }) + }) + // + } + const handleClose = ()=>{ + setState({ + loading: false, + open:false, + errorMessage:undefined }) - console.log(values) } return (<> - + setState({open:false})} + onCancel={handleClose} footer={null} width={600} - okText={t('base.confirm')} maskClosable={false} > - onSubmit={onSubmit} initValues={{ - payment_channel: 'FLYWIRE', - payment_method: '', + onSubmit={onSubmit} initValues={{ merchant_ref: '', - payment_amount: '', - actual_payment_amount: '', + application_number: '', + student_email: '', + paid_area: '', + initiated_paid_date: '', + paid_date: '', + delivered_date: '', + payment_method: '', + payment_channel: '', + check_student: true, + details: [] }}> - - { - BillTypes.map((it, idx) => ( - {it.label})) - } - + = (props) => { + + + + + + + + - + type={'date'} + field="initiated_paid_date" + label={t('bill.title_initiated_paid_at')} + style={{width: '100%'}} + /> - - + - - - + ]} + type={'date'} + field="paid_date" + label={t('bill.title_paid_at')} + style={{width: '100%'}} + /> - - + + - {/*

{t('bill.paid_confirm')}

*/} -
+
+ setState({details})}/> + {state.errorMessage &&
+ + {state.errorMessage} +
} + +
+
- + + type={'primary'}>{t('bill.create.confirm')}
diff --git a/src/pages/bill/components/bill_type_confirm.tsx b/src/pages/bill/components/bill_type_confirm.tsx index f1331fa..f055801 100644 --- a/src/pages/bill/components/bill_type_confirm.tsx +++ b/src/pages/bill/components/bill_type_confirm.tsx @@ -163,9 +163,12 @@ export const BillTypeConfirmModal: React.FC = (props) => {
setState({detail_confirms})}/>
- + + + +
) } \ No newline at end of file diff --git a/src/pages/bill/components/import_bill_modal.tsx b/src/pages/bill/components/import_bill_modal.tsx new file mode 100644 index 0000000..d8f00f7 --- /dev/null +++ b/src/pages/bill/components/import_bill_modal.tsx @@ -0,0 +1,125 @@ +import {Button, Modal, Select, Space, Upload} from "@douyinfe/semi-ui"; +import React, {useState} from "react"; +import {useTranslation} from "react-i18next"; +import {useSetState} from "ahooks"; +import {usePaymentChannels} from "@/hooks/usePaymentChannels.ts"; +import {IconUpload} from "@douyinfe/semi-icons"; +import readXlsxFile from "read-excel-file"; +import dayjs from "dayjs"; +import {OnChangeProps} from "@douyinfe/semi-ui/lib/es/upload/interface"; + +type BillPaidModalProps = { + onConfirm: () => void + onCancel?: () => void +} +// type ImportRecordValue = number | string | null | Date; + +export const ImportBillModal: React.FC = (props) => { + const {t, i18n} = useTranslation() + const {paymentChannelList} = usePaymentChannels() + + const [state, setState] = useSetState<{ + loading?: boolean; + open?: boolean + }>({}) + + const [records, setImportRecords] = useState([]) + + + const closeModal = () => { + setImportRecords([]) + setState({open: false, loading: false}) + } + const onSubmit = () => { + setState({ + loading: true + }) + setTimeout(() => { + closeModal() + props.onConfirm() + }, 1000) + } + const onFileChange = ({fileList, currentFile}: OnChangeProps) => { + if (fileList.length == 0) { + setImportRecords([]) + return; + } + if (currentFile.fileInstance) { + readXlsxFile(currentFile.fileInstance).then(rows => { + const records = rows.map((row) => { + return row.map((item) => { + if (item && item instanceof Date) return dayjs(item).format('YYYY-MM-DD HH:mm:ss'); + return String(item) + }) + }) + setImportRecords(records) + }) + } + } + return (<> + + + + + +
{t('bill.title_pay_channel')}: +
+ +
+ +
{t('base.select_excel_file')}: +
+
+ onFileChange(e)} + accept={'.xlsx,.csv'} uploadTrigger="custom" action={''} + > + + +
+
+
+
+
+ + {records.map((record, index) => { + return + {record.map((item, index) => { + return + })} + + })} +
+
{item}
+
+
+
+ {/*

{t('bill.paid_confirm')}

*/} +
+ + + + +
+
+ ) +} \ No newline at end of file diff --git a/src/pages/bill/query.tsx b/src/pages/bill/query.tsx index c46f13b..f00f23d 100644 --- a/src/pages/bill/query.tsx +++ b/src/pages/bill/query.tsx @@ -2,19 +2,20 @@ import {Button, ButtonGroup, Modal, Notification, Popconfirm, Space, Toast} from import {useState} from "react"; import {useRequest, useSetState} from "ahooks"; import {useTranslation} from "react-i18next"; +import {saveAs} from "file-saver"; import {BillList} from "@/components/bill/list.tsx"; import SearchForm from "@/components/bill/search-form.tsx"; import BillDetail from "@/components/bill/detail.tsx"; -import {billList, BillQueryParams, exportBillList, modifyBillStatus} from "@/service/api/bill.ts"; +import {billList, BillQueryParams, cancelConfirmBill, exportBillList, modifyBillStatus} from "@/service/api/bill.ts"; import {BillStatus, BizError} from "@/service/types.ts"; import {useDownloadReceiptPDF} from "@/service/generate-pdf.ts"; import {BillPaidModal} from "@/pages/bill/components/bill_paid_modal.tsx"; import {BillTypeConfirmModal} from "@/pages/bill/components/bill_type_confirm.tsx"; -import {saveAs} from "file-saver"; // import {AddBillModal} from "@/pages/bill/components/add_bill_modal.tsx"; import {BillTypeConfirmBatch} from "@/pages/bill/components/bill_type_confirm_batch.tsx"; import {AddBillModal} from "@/pages/bill/components/add_bill_modal.tsx"; +import {ImportBillModal} from "@/pages/bill/components/import_bill_modal.tsx"; const DownloadButton = ({bill, text}: { bill: BillModel; text: string }) => { @@ -28,12 +29,14 @@ const DownloadButton = ({bill, text}: { bill: BillModel; text: string }) => { // } const BillQuery = () => { + // const {createPDF,downloadPDF} = useDownloadReceiptPDF() const [state, setState] = useSetState<{ - updateBill?: BillModel - confirmBill?: BillModel + updateBill?: BillModel; + confirmBill?: BillModel; + previewPDFUrl?: string; updateLoading?: boolean - exporting?: boolean - confirmBillId: number + exporting?: boolean; + confirmBillId: number; }>({confirmBillId: 0}) const [showBill, setShowBill] = useState() const [queryParams, setBillQueryParams] = useState({ @@ -42,8 +45,8 @@ const BillQuery = () => { }); const {data, loading, refresh} = useRequest(() => billList(queryParams), { refreshDeps: [queryParams], - onSuccess:()=>{ - document.documentElement.scrollTo({top:0}); + onSuccess: () => { + document.documentElement.scrollTo({top: 0}); }, onError: (e) => { Notification.error({title: 'Error', content: e.message}) @@ -63,6 +66,26 @@ const BillQuery = () => { }) } + const onCancelBillConfirm = (id: number) => { + cancelConfirmBill(id).then(() => { + Toast.success({ + content: `${t('base.operate_success')}`, + duration: 3 + }) + refresh() + }).catch((e: BizError) => { + Toast.error({ + content: `${t('base.operate_fail')}:${e.message}`, + duration: 3 + }) + }) + } + + // const showBillPDF = (bill: BillModel) => { + // setState({ + // previewPDFUrl: createPDF(bill).output('bloburi').toString() + // }) + // } const operation = (bill: BillModel) => { return (
{bill.status != BillStatus.PAID && @@ -83,11 +106,19 @@ const BillQuery = () => { } { bill.status == BillStatus.PAID && <> - - {bill.confirm_status == 'UNCONFIRMED' && } + + {/* showBillPDF(bill)} size={'small'}>download pdf*/} + {bill.confirm_status == 'UNCONFIRMED' ? + : onCancelBillConfirm(bill.id)} position={'topRight'} + content={`${t('base.confirm_next_operation')}?`} + > + + } }
) @@ -111,9 +142,7 @@ const BillQuery = () => { }) } - const onImportExcel = () => { - Toast.warning({content: 'Not implemented'}) - } + const [selectKeys, setSelectedKeys] = useState([]) @@ -131,7 +160,8 @@ const BillQuery = () => { - + {/**/} + } @@ -162,6 +192,21 @@ const BillQuery = () => { > {showBill && setShowBill(undefined)}/>} + {/* preview and download pdf */} + {/* setState({previewPDFUrl: undefined})}*/} + {/* onOk={()=>{*/} + {/* if(state.previewPDFUrl) {*/} + {/* saveAs(state.previewPDFUrl, 'pdf.pdf')*/} + {/* }*/} + {/* }}*/} + {/*>*/} + {/* {state.previewPDFUrl &&
*/} + {/*