feat: add import billing records

This commit is contained in:
LittleBoy 2024-08-27 16:38:10 +08:00
parent 960f08d92e
commit 1411360614
10 changed files with 271 additions and 149 deletions

View File

@ -0,0 +1,18 @@
import {IconAlertCircle} from "@douyinfe/semi-icons";
import React from "react";
import './style.less'
type AlertProps = {
message: React.ReactNode;
}
function Alert(props: AlertProps) {
if(!props.message) return null;
return <div className={'alert-container'}>
<IconAlertCircle/>
<span className={'alert-text'}>{props.message}</span>
</div>
}
export default Alert

View File

@ -0,0 +1,8 @@
.alert-container{
display: flex;
align-items: center;
color:red;
.alert-text{
margin-left: 5px;
}
}

View File

@ -5,6 +5,7 @@ import {useTranslation} from "react-i18next";
import {Card} from "@/components/card"; import {Card} from "@/components/card";
import {BillQueryParams} from "@/service/api/bill.ts"; import {BillQueryParams} from "@/service/api/bill.ts";
import {useBillTypes} from "@/hooks/useBillTypes.ts"; import {useBillTypes} from "@/hooks/useBillTypes.ts";
import {usePaymentChannels} from "@/hooks/usePaymentChannels.ts";
type SearchFormProps = { type SearchFormProps = {
onSearch?: (params: BillQueryParams) => void; onSearch?: (params: BillQueryParams) => void;
@ -29,6 +30,7 @@ type SearchFormFields = {
} }
const SearchForm: React.FC<SearchFormProps> = (props) => { const SearchForm: React.FC<SearchFormProps> = (props) => {
const BillTypes = useBillTypes() const BillTypes = useBillTypes()
const { paymentChannelList } = usePaymentChannels()
const formSubmit = (value: SearchFormFields) => { const formSubmit = (value: SearchFormFields) => {
const params: BillQueryParams = {} const params: BillQueryParams = {}
@ -146,12 +148,11 @@ const SearchForm: React.FC<SearchFormProps> = (props) => {
</Form.Select> </Form.Select>
</Col> </Col>
<Col xxl={4} xl={6} md={8}> <Col xxl={4} xl={6} md={8}>
<Form.Select showClear field="payment_channel" label={t('bill.title_pay_channel')} <Form.Select
placeholder={t('base.please_select')} style={{width: '100%'}}> showClear field="payment_channel" label={t('bill.title_pay_channel')}
<Form.Select.Option value="FLYWIRE">FLYWIRE</Form.Select.Option> placeholder={t('base.please_select')} style={{width: '100%'}}
<Form.Select.Option value="CBP">CBP</Form.Select.Option> optionList={paymentChannelList}
<Form.Select.Option value="PPS">PPS</Form.Select.Option> />
</Form.Select>
</Col> </Col>
<Col xxl={4} xl={6} md={8}> <Col xxl={4} xl={6} md={8}>
<Form.Select showClear field="is_delivered" label={t('bill.delivered_status')} <Form.Select showClear field="is_delivered" label={t('bill.delivered_status')}

View File

@ -63,6 +63,10 @@
"download-qr-code": "Download QR Code", "download-qr-code": "Download QR Code",
"download_receipt": "Download receipt", "download_receipt": "Download receipt",
"export_excel": "Export Transaction Excel", "export_excel": "Export Transaction Excel",
"import": {
"error_require_file": "Please select the transaction record file to import",
"error_require_payment_channel": "Please select a payment channel"
},
"import_bill": "Import Transaction Record", "import_bill": "Import Transaction Record",
"import_excel": "Import Transaction Excel", "import_excel": "Import Transaction Excel",
"paid": "Paid", "paid": "Paid",

View File

@ -63,6 +63,10 @@
"download-qr-code": "下载二维码", "download-qr-code": "下载二维码",
"download_receipt": "下载收据", "download_receipt": "下载收据",
"export_excel": "导出交易记录", "export_excel": "导出交易记录",
"import": {
"error_require_file": "请选择要导入的交易记录文件",
"error_require_payment_channel": "请选择支付渠道"
},
"import_bill": "导入交易记录", "import_bill": "导入交易记录",
"import_excel": "导入交易记录", "import_excel": "导入交易记录",
"paid": "已支付", "paid": "已支付",

View File

@ -63,6 +63,10 @@
"download-qr-code": "下載二維碼", "download-qr-code": "下載二維碼",
"download_receipt": "下載收據", "download_receipt": "下載收據",
"export_excel": "導出交易记录", "export_excel": "導出交易记录",
"import": {
"error_require_file": "請選擇要匯入的交易記錄文件",
"error_require_payment_channel": "請選擇支付管道"
},
"import_bill": "導入交易記錄", "import_bill": "導入交易記錄",
"import_excel": "導入交易记录", "import_excel": "導入交易记录",
"paid": "已支付", "paid": "已支付",

View File

@ -7,6 +7,8 @@ import {IconUpload} from "@douyinfe/semi-icons";
import readXlsxFile from "read-excel-file"; import readXlsxFile from "read-excel-file";
import dayjs from "dayjs"; import dayjs from "dayjs";
import {OnChangeProps} from "@douyinfe/semi-ui/lib/es/upload/interface"; import {OnChangeProps} from "@douyinfe/semi-ui/lib/es/upload/interface";
import Alert from "@/components/alert";
import {uploadBillingRecordFile} from "@/service/api/bill.ts";
type BillPaidModalProps = { type BillPaidModalProps = {
onConfirm: () => void onConfirm: () => void
@ -20,24 +22,37 @@ export const ImportBillModal: React.FC<BillPaidModalProps> = (props) => {
const [state, setState] = useSetState<{ const [state, setState] = useSetState<{
loading?: boolean; loading?: boolean;
open?: boolean open?: boolean;
errorMessage?:string;
currentFile?: File;
paymentChannel?: string;
}>({}) }>({})
const [records, setImportRecords] = useState<string[][]>([]) const [records, setImportRecords] = useState<string[][]>([])
const closeModal = () => { const closeModal = () => {
setImportRecords([]) setImportRecords([])
setState({open: false, loading: false}) setState({open: false, loading: false,errorMessage:undefined})
} }
const onSubmit = () => { const onSubmit = () => {
if(!state.paymentChannel){
setState({errorMessage:'bill.import.error_require_payment_channel'})
return;
}
if(records.length == 0 || !state.currentFile){
setState({errorMessage:'bill.import.error_require_file'})
return;
}
setState({ setState({
loading: true loading: true,
errorMessage:undefined
}) })
setTimeout(() => { uploadBillingRecordFile(state.currentFile,state.paymentChannel).then(()=>{
closeModal() closeModal()
props.onConfirm() props.onConfirm()
}, 1000) }).catch(e => {
setState({errorMessage: e.message,loading: false})
})
} }
const onFileChange = ({fileList, currentFile}: OnChangeProps) => { const onFileChange = ({fileList, currentFile}: OnChangeProps) => {
if (fileList.length == 0) { if (fileList.length == 0) {
@ -53,6 +68,7 @@ export const ImportBillModal: React.FC<BillPaidModalProps> = (props) => {
}) })
}) })
setImportRecords(records) setImportRecords(records)
setState({currentFile:currentFile.fileInstance})
}) })
} }
} }
@ -76,7 +92,11 @@ export const ImportBillModal: React.FC<BillPaidModalProps> = (props) => {
textAlign: 'right' textAlign: 'right'
}}>{t('bill.title_pay_channel')} }}>{t('bill.title_pay_channel')}
</div> </div>
<Select style={{width: 160}} placeholder={t('base.please_select')} optionList={paymentChannelList}></Select> <Select
style={{width: 160}} placeholder={t('base.please_select')}
optionList={paymentChannelList}
onChange={v=>setState({paymentChannel: String(v)})}
/>
</Space> </Space>
<Space> <Space>
<div style={{ <div style={{
@ -110,7 +130,7 @@ export const ImportBillModal: React.FC<BillPaidModalProps> = (props) => {
</table> </table>
</div> </div>
</div> </div>
{/*<p style={{marginTop: 10}}>{t('bill.paid_confirm')}</p>*/} <Alert message={state.errorMessage?t(state.errorMessage):undefined} />
<div className={'text-right'} style={{margin: '50px 0 20px'}}> <div className={'text-right'} style={{margin: '50px 0 20px'}}>
<Space spacing={12}> <Space spacing={12}>
<Button onClick={closeModal} type={'tertiary'}>{t('base.cancel')}</Button> <Button onClick={closeModal} type={'tertiary'}>{t('base.cancel')}</Button>

View File

@ -1,4 +1,4 @@
import {get, post, put} from "@/service/request.ts"; import {get, post, put, uploadFile} from "@/service/request.ts";
import dayjs from "dayjs"; import dayjs from "dayjs";
import {getAuthToken} from "@/hooks/useAuth.ts"; import {getAuthToken} from "@/hooks/useAuth.ts";
import {stringify} from "qs"; import {stringify} from "qs";
@ -43,6 +43,7 @@ export function exportBillList(params: BillQueryParams) {
}).then(r => r.blob()) }).then(r => r.blob())
// return get<Blob>('/bills/export', params,true) // return get<Blob>('/bills/export', params,true)
} }
//现场支付创建账单接口 //现场支付创建账单接口
export function createManualBill(params: ManualCreateBillParam) { export function createManualBill(params: ManualCreateBillParam) {
return post<BillModel>('/manual_payment', params) return post<BillModel>('/manual_payment', params)
@ -61,6 +62,10 @@ export function addBillRecord(bill:CreateBillRecordModel) {
return post('/add_bill', bill); return post('/add_bill', bill);
} }
export function uploadBillingRecordFile(file: File, payment_channel: string, check_student = 'true') {
return uploadFile('/bill/import', file, {payment_channel, check_student})
}
export function getAsiaPayData(id: number) { export function getAsiaPayData(id: number) {
return get<AsiaPayModel>(`/bills/${id}/asiapay`) return get<AsiaPayModel>(`/bills/${id}/asiapay`)
} }
@ -77,6 +82,7 @@ export function modifyBillStatus(id: number,status: BillStatus) {
export function confirmBillType(bills: BillConfirmParams[]) { export function confirmBillType(bills: BillConfirmParams[]) {
return post<BillModel>(`/bills/confirm`, {bills}) return post<BillModel>(`/bills/confirm`, {bills})
} }
export function cancelConfirmBill(bill_id: number) { export function cancelConfirmBill(bill_id: number) {
return post<BillModel>(`/bill/cancel_confirm`, {bill_id}) return post<BillModel>(`/bill/cancel_confirm`, {bill_id})
} }
@ -98,6 +104,7 @@ type BillUpdateFormParams = {
bill: BillModel; bill: BillModel;
param: BillUpdateParams param: BillUpdateParams
} }
export async function finishAsiapay({param}: BillUpdateFormParams) { export async function finishAsiapay({param}: BillUpdateFormParams) {
const paramUrl = `?prc=0&src=0&Ord=12345678&Ref=${param.merchant_ref}&PayRef=123456&successcode=0&Amt=10.00&Cur=344&Holder=Test Card&AuthId=123456&AlertCode=&remark= const paramUrl = `?prc=0&src=0&Ord=12345678&Ref=${param.merchant_ref}&PayRef=123456&successcode=0&Amt=10.00&Cur=344&Holder=Test Card&AuthId=123456&AlertCode=&remark=
&eci=07&payerAuth=U&sourceIp=192.1.1.1&ipCountry=HK&payMethod=VISA &eci=07&payerAuth=U&sourceIp=192.1.1.1&ipCountry=HK&payMethod=VISA

View File

@ -27,12 +27,13 @@ Axios.interceptors.request.use(config => {
return Promise.reject(err) return Promise.reject(err)
}) })
export function request<T>(url: string, method: RequestMethod, data: AllType = null, getOriginResult = false) { export function request<T>(url: string, method: RequestMethod, data: AllType = null, getOriginResult = false, headers: RequestHeaders = {}) {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
Axios.request<APIResponse<T>>({ Axios.request<APIResponse<T>>({
url, url,
method, method,
data, data,
headers
}).then(res => { }).then(res => {
if (res.status != 200) { if (res.status != 200) {
reject(new BizError("Service Internal Exception,Please Try Later!", res.status)) reject(new BizError("Service Internal Exception,Please Try Later!", res.status))
@ -71,6 +72,58 @@ export function put<T>(url: string, data: AllType = {}) {
return request<T>(url, 'put', data) return request<T>(url, 'put', data)
} }
type UploadFileModel = {
field_name?: string;
origin_file: File
}
export function uploadFile<T>(url: string, file: UploadFileModel | File, data: AllType = null, returnOrigin = false) {
const formData = new FormData();
if (file && file instanceof File) {
formData.append('file', file);
} else {
formData.append(file.field_name || 'file', file.origin_file);
}
if (data) {
Object.keys(data).forEach(key => {
formData.append(key, data[key]);
})
}
return request<T>(url, 'post', data, returnOrigin, {
'Content-Type': 'multipart/form-data'
})
// return new Promise<T>((resolve, reject) => {
// Axios.request<APIResponse<T>>({
// url,
// method,
// data,
// headers:{
// 'Content-Type': 'multipart/form-data'
// }
// }).then(res => {
// if (res.status != 200) {
// reject(new BizError("Service Internal Exception,Please Try Later!", res.status))
// return;
// }
// if (getOriginResult) {
// resolve(res.data as unknown as T)
// return;
// }
// // const
// const {code, message, data,request_id} = res.data
// if (code == 0) {
// resolve(data as unknown as T)
// } else {
// reject(new BizError(message, code,request_id, data as unknown as AllType))
// }
// }).catch(e => {
// reject(new BizError(e.message, 500))
// })
// })
}
export function getFileBlob(url: string) { export function getFileBlob(url: string) {
return new Promise<Blob>((resolve, reject) => { return new Promise<Blob>((resolve, reject) => {
fetch(url).then(res => res.blob()).then(res => { fetch(url).then(res => res.blob()).then(res => {

3
src/types/api.d.ts vendored
View File

@ -1,5 +1,8 @@
// 请求方式 // 请求方式
declare type RequestMethod = 'get' | 'post' | 'put' | 'delete' declare type RequestMethod = 'get' | 'post' | 'put' | 'delete'
declare type RequestHeaders = {
[key:string]:string
}
// 接口返回数据类型 // 接口返回数据类型
declare interface APIResponse<T> { declare interface APIResponse<T> {