diff --git a/src/assets/index.less b/src/assets/index.less index bad1278..eb18a57 100644 --- a/src/assets/index.less +++ b/src/assets/index.less @@ -132,7 +132,7 @@ body #root{ } // input -.semi-input-wrapper, .semi-select,.semi-datepicker-range-input { +.semi-input-wrapper, .semi-select,.semi-datepicker-range-input,.semi-input-textarea-wrapper { background-color: transparent; border: 1px var(--semi-color-fill-2) solid; border-radius: var(--semi-border-radius-small); diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 4c7c6c4..477d65d 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -41,6 +41,7 @@ "reconciliation_status_pending": "UNCHECKED", "reconciliation_status_submitted": "CHECKED", "require_student_number": "Search Student Number", + "set_bill_paid": "Set Bill Paid", "sort_asc": "ASC", "sort_desc": "DESC", "title_actual_payment_amount": "Actually Paid", @@ -53,11 +54,13 @@ "title_operate": "Operation", "title_paid_at": "Transaction Date", "title_pay_amount": "Pay Amount", + "title_pay_channel": "Payment Channel", "title_pay_method": "Pay Method", "title_pay_sort": "Sort By", "title_program_id": "Program ID", "title_program_name": "Program", "title_reconciliation_status": "Reconciliation", + "title_remark": "Remark", "title_semester": "Semester", "title_service_charge": "Service Charge", "title_student_name": "Student Name", diff --git a/src/i18n/translations/sc.json b/src/i18n/translations/sc.json index baa0037..3466d87 100644 --- a/src/i18n/translations/sc.json +++ b/src/i18n/translations/sc.json @@ -41,6 +41,7 @@ "reconciliation_status_pending": "未对账", "reconciliation_status_submitted": "已对账", "require_student_number": "请输入查询学号", + "set_bill_paid": "设置账单支付完成", "sort_asc": "升序", "sort_desc": "降序", "title_actual_payment_amount": "实付金额", @@ -53,11 +54,13 @@ "title_operate": "操作", "title_paid_at": "支付时间", "title_pay_amount": "应付金额", + "title_pay_channel": "支付渠道", "title_pay_method": "支付方式", "title_pay_sort": "排序方式", "title_program_id": "专业ID", "title_program_name": "就读专业", "title_reconciliation_status": "对账状态", + "title_remark": "备注", "title_semester": "学期", "title_service_charge": "手续费", "title_student_name": "学生姓名", diff --git a/src/i18n/translations/tc.json b/src/i18n/translations/tc.json index f16a23d..b9e1073 100644 --- a/src/i18n/translations/tc.json +++ b/src/i18n/translations/tc.json @@ -41,6 +41,7 @@ "reconciliation_status_pending": "未對帳", "reconciliation_status_submitted": "已對帳", "require_student_number": "請輸入查詢學號", + "set_bill_paid": "設定帳單支付完成", "sort_asc": "升序", "sort_desc": "降序", "title_actual_payment_amount": "實付金額", @@ -53,11 +54,13 @@ "title_operate": "操作", "title_paid_at": "付款時間", "title_pay_amount": "應付金額", + "title_pay_channel": "支付渠道", "title_pay_method": "付款方式", "title_pay_sort": "排序方式", "title_program_id": "專業ID", "title_program_name": "就讀專業", "title_reconciliation_status": "對帳狀態", + "title_remark": "備註", "title_semester": "學期", "title_service_charge": "手續費", "title_student_name": "學生姓名", diff --git a/src/pages/bill/components/bill_paid_modal.tsx b/src/pages/bill/components/bill_paid_modal.tsx new file mode 100644 index 0000000..081b6f4 --- /dev/null +++ b/src/pages/bill/components/bill_paid_modal.tsx @@ -0,0 +1,147 @@ +import {BillDetailItems} from "@/components/bill"; +import {Button, Col, Form, Modal, Notification, Row, Space, Toast} from "@douyinfe/semi-ui"; +import React from "react"; +import {useTranslation} from "react-i18next"; +import {finishBill, modifyBillStatus} from "@/service/api/bill.ts"; +import {BizError} from "@/service/types.ts"; +import {useSetState} from "ahooks"; + +type BillPaidModalProps = { + bill: BillModel; + open?: boolean; + onConfirm: () => void + onCancel: () => void +} +export const BillPaidModal: React.FC = (props) => { + const {t} = useTranslation() + + const [state, setState] = useSetState<{ + loading?: boolean + }>({}) + const onConfirmPaid = () => { + if (!props.bill) return; + setState({ + loading: true + }) + modifyBillStatus(props.bill.id, 'PAID').then(() => { + setState({loading: false}) + Notification.success({title: 'Notice', content: t('base.operate_success')}) + props.onConfirm() + }).catch((e: BizError) => { + setState({loading: false}) + Toast.error({ + content: `${t('base.operate_fail')}:${e.message}`, + duration: 3 + }) + }) + } + const onSubmit = (values: BillUpdateParams) => { + if (!props.bill) return; + finishBill({bill:props.bill,param:values}).then(props.onConfirm) + // setState({ + // loading: true + // }) + // modifyBillStatus(props.bill.id, 'PAID', values).then(() => { + // setState({loading: false}) + // Notification.success({title: 'Notice', content: t('base.operate_success')}) + // props.onConfirm() + // }).catch((e: BizError) => { + // setState({loading: false}) + // Toast.error({ + // content: `${t('base.operate_fail')}:${e.message}`, + // duration: 3 + // }) + // }) + } + return + {props.bill &&
} + + onSubmit={onSubmit} initValues={{ + payment_channel: 'FLYWIRE', + payment_method: 'card', + merchant_ref: props.bill?.merchant_ref, + payment_amount: props.bill?.amount, + actual_payment_amount: props.bill?.amount + }}> + + + + FLYWIRE + {/*ASIAPAY*/} + {/*PPS*/} + + + + + Card(VISA,MasterCard,UnionPay,JCB...) + Wechat + Alipay + Other + {/*ASIAPAY*/} + {/*PPS*/} + + + + + + + + + + + + + + + + + + + + {/*

{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 3b5401e..c05e172 100644 --- a/src/pages/bill/query.tsx +++ b/src/pages/bill/query.tsx @@ -9,9 +9,9 @@ import BillDetail from "@/components/bill/detail.tsx"; import {billList, BillQueryParams, modifyBillStatus} from "@/service/api/bill.ts"; import {BillStatus, BizError} from "@/service/types.ts"; import {useDownloadReceiptPDF} from "@/service/generate-pdf.ts"; -import useAuth from "@/hooks/useAuth.ts"; import {BillDetailItems} from "@/components/bill"; import MoneyFormat from "@/components/money-format.tsx"; +import {BillPaidModal} from "@/pages/bill/components/bill_paid_modal.tsx"; const DownloadButton = ({bill, text}: { bill: BillModel; text: string }) => { @@ -54,27 +54,6 @@ const BillQuery = () => { }) } - const onConfirmPaid = () => { - if (!state.updateBill) return; - setState({ - updateLoading: true - }) - modifyBillStatus(state.updateBill.id, 'PAID').then(() => { - setState({ - updateBill: undefined, updateLoading: false - }) - Notification.success({title: 'Notice', content: t('base.operate_success')}) - refresh() - }).catch((e: BizError) => { - setState({ - updateLoading: false - }) - Toast.error({ - content: `${t('base.operate_fail')}:${e.message}`, - duration: 3 - }) - }) - } const operation = (bill: BillModel) => { return (
@@ -128,21 +107,15 @@ const BillQuery = () => { > {showBill && setShowBill(undefined)}/>} - { - setState({updateBill: undefined, updateLoading: false}) + setState({updateBill:undefined})} + bill={state.updateBill!} + onConfirm={()=>{ + setState({updateBill:undefined}) + refresh() }} - confirmLoading={state.updateLoading} - onOk={onConfirmPaid} - okText={t('base.confirm_paid')} - maskClosable={false} - > - {state.updateBill &&
} -

{t('bill.paid_confirm')}

-
+ /> {
- {!!it.confirm_status ? CONFIRMED : + {it.confirm_status == 'CONFIRMED' ? CONFIRMED : } -
)) } diff --git a/src/pages/bill/reconciliation.tsx b/src/pages/bill/reconciliation.tsx index 475339e..ee0d264 100644 --- a/src/pages/bill/reconciliation.tsx +++ b/src/pages/bill/reconciliation.tsx @@ -10,98 +10,102 @@ import useAuth from "@/hooks/useAuth.ts"; import {BizError} from "@/service/types.ts"; const BillReconciliation = () => { - const {t} = useTranslation() - const {user} = useAuth(); - const [queryParams, setBillQueryParams] = useState({ - apply_status:'UNCHECKED' - }); - const {data, loading, refresh} = useRequest(() => billList({ - ...queryParams, - status: 'PAID', - department: user?.department == 'RO' ? 'RO' : 'FO', - }), { - refreshDeps: [queryParams], - onError: (e:Error) => { - Toast.error({ - content: `${t('base.query_bill')}:${e.message}`, - duration: 3 - }) - } - }) + const {t} = useTranslation() + const {user} = useAuth(); + const [queryParams, setBillQueryParams] = useState({ + apply_status: 'UNCHECKED' + }); + const {data, loading, refresh} = useRequest(() => billList({ + ...queryParams, + status: 'PAID', + confirm_status: 'CONFIRMED', + department: user?.department == 'RO' ? 'RO' : 'FO', + }), { + refreshDeps: [queryParams], + onError: (e: Error) => { + Toast.error({ + content: `${t('base.query_bill')}:${e.message}`, + duration: 3 + }) + } + }) - const [selectKeys, setSelectedKeys] = useState<(string | number)[]>([]) - const [state,setState] = useSetState({ - checkingId: -1 - }) - const confirmBill = (records: number[]) => { - if (records.length == 0) { - Notification.error({title: 'Notice', content: t('bill.confirm_select_empty')}) - return - } - setState({checkingId: records.length > 1 ? 0 : records[0] }) - confirmBills(records).then(() => { - Notification.success({title: 'Notice', content: t('bill.confirm_success')}) - refresh() - }).catch((e:BizError) => { - Toast.error({ - content: `${t('base.operate_fail')}:${e.message}`, - duration: 3 - }) - }).finally(() => {setState({checkingId: -1})}) - } - const operation = (_record: BillModel) => { - return ( - confirmBill([_record.id])} - > - - - ) - } + const [selectKeys, setSelectedKeys] = useState<(string | number)[]>([]) + const [state, setState] = useSetState({ + checkingId: -1 + }) + const confirmBill = (records: number[]) => { + if (records.length == 0) { + Notification.error({title: 'Notice', content: t('bill.confirm_select_empty')}) + return + } + setState({checkingId: records.length > 1 ? 0 : records[0]}) + confirmBills(records).then(() => { + Notification.success({title: 'Notice', content: t('bill.confirm_success')}) + refresh() + }).catch((e: BizError) => { + Toast.error({ + content: `${t('base.operate_fail')}:${e.message}`, + duration: 3 + }) + }).finally(() => { + setState({checkingId: -1}) + }) + } + const operation = (_record: BillModel) => { + return ( + confirmBill([_record.id])} + > + + + ) + } - return (
+ return (
- setBillQueryParams({ - apply_status, - status: 'PAID' - })}> - {t('bill.reconciliation_status_pending')}} itemKey="UNCHECKED"/> - {t('bill.reconciliation_status_submitted')}} itemKey="CHECKED"/> - - )} - loading={loading} - onSearch={(params) => { - setBillQueryParams({...params, apply_status: queryParams.apply_status}) - }} - /> - { - setSelectedKeys(keys); - }} - loading={loading} - onPageChange={(page_number) => setBillQueryParams({ - ...queryParams, - page_number - })} - tableFooter={queryParams.apply_status != 'CHECKED' && selectKeys?.length > 0 && ( - confirmBill(selectKeys as number[])} - > - - - )} - /> -
) + setBillQueryParams({ + apply_status, + status: 'PAID' + })}> + {t('bill.reconciliation_status_pending')}} itemKey="UNCHECKED"/> + {t('bill.reconciliation_status_submitted')}} itemKey="CHECKED"/> + + )} + loading={loading} + onSearch={(params) => { + setBillQueryParams({...params, apply_status: queryParams.apply_status}) + }} + /> + { + setSelectedKeys(keys); + }} + loading={loading} + onPageChange={(page_number) => setBillQueryParams({ + ...queryParams, + page_number + })} + tableFooter={queryParams.apply_status != 'CHECKED' && selectKeys?.length > 0 && ( + confirmBill(selectKeys as number[])} + > + + + )} + /> +
) } export default BillReconciliation \ No newline at end of file diff --git a/src/service/api/bill.ts b/src/service/api/bill.ts index 522e28f..e14d63c 100644 --- a/src/service/api/bill.ts +++ b/src/service/api/bill.ts @@ -1,4 +1,5 @@ import {get, post, put} from "@/service/request.ts"; +import dayjs from "dayjs"; export type BillQueryResult = { result: BillModel[]; @@ -62,4 +63,58 @@ export function updateBillPaymentSuccess(bill_id: number, merchant_ref: string, export function createExternalBill(params: ExternalCreateParamsType) { return post('/bill', params) +} + +type BillUpdateFormParams = { + bill:BillModel; + param:BillUpdateParams +} +export async function finishAsiapay({bill,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= +&eci=07&payerAuth=U&sourceIp=192.1.1.1&ipCountry=HK&payMethod=VISA +x&cardIssuingCountry=HK&channelType=SPC&` + const ret = await post(`/flywire/feedback?${paramUrl}`, {},true) + + return ret?.toLowerCase() == 'ok' +} +export async function finishFlywire({bill,param}: BillUpdateFormParams){ + const eventData = { + "event_type": "guaranteed", + "event_date": dayjs().format('YYYY-MM-DDTHH:mm:ss[Z]'), + "event_resource": "payments", + "data": { + "remark": param.remark, + "payment_id": param.merchant_ref, + "amount_from": param.actual_payment_amount, + "currency_from": "HKD", + "amount_to": param.payment_amount, + "currency_to": "HKD", + "status": "guaranteed", + "expiration_date": dayjs().format('YYYY-MM-DDTHH:mm:ss[Z]'), + "external_reference": bill.id, + "country": "CN", + "payment_method": { + "type": param.payment_channel + }, + "fields": { + "student_email": bill.student_email, + "student_last_name": bill.student_last_name || bill.student_english_name, + "student_id": bill.student_number, + "resident_no": null, + "contact_no": "", + "payment_type_other": null, + "payment_type": null + } + } + } + const ret = await post('/flywire/feedback', eventData,true) + + return ret?.toLowerCase() == 'ok' +} + +export function finishBill(params: BillUpdateFormParams) { + if(params.param.payment_channel === 'flywire'){ + return finishAsiapay(params) + } + return finishFlywire(params) } \ No newline at end of file diff --git a/src/service/generate-pdf.ts b/src/service/generate-pdf.ts index 09d4090..f5b77f5 100644 --- a/src/service/generate-pdf.ts +++ b/src/service/generate-pdf.ts @@ -27,7 +27,7 @@ export function GeneratePdf(bill: BillModel) { doc.setFont('Helvetica', 'normal', 'normal'); drawItem(doc, {title: 'Student Name:', content: bill.student_english_name || bill.student_chinese_name}, 40) - drawItem(doc, {title: 'Reference Number:', content: `${bill.application_number}`}, 40, "right") + drawItem(doc, {title: 'Reference Number:', content: `${bill.id}`}, 40, "right") drawItem(doc, {title: 'Student Number:', content: `${bill.student_number || bill.application_number}`}, 48) drawItem(doc, {title: 'Print Date:', content: dayjs().format('YYYY-MM-DD')}, 48, "right") @@ -58,10 +58,10 @@ export function GeneratePdf(bill: BillModel) { body: [ ...(bill.details.map(it=>{ return [ - `#${bill.id}${it.id?'-' + it.id:''}`, + `#${it.id}`, dayjs(bill.paid_at).format('YYYY-MM-DD'), it.bill_type, - `${bill.payment_channel}(${bill.payment_method})`, + `${bill.payment_channel}` + bill.payment_channel != bill.payment_method ? `(${bill.payment_method})` : '', `${it.amount}` ]; })), diff --git a/src/service/request.ts b/src/service/request.ts index 77e039a..c5ce661 100644 --- a/src/service/request.ts +++ b/src/service/request.ts @@ -38,13 +38,13 @@ export function request(url: string, method: RequestMethod, data: AllType = n 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) { - if (getOriginResult) { - resolve(res.data as unknown as T) - return; - } resolve(data as unknown as T) } else { reject(new BizError(message, code,request_id, data as unknown as AllType)) diff --git a/src/types/bill.d.ts b/src/types/bill.d.ts index e709818..61e22ee 100644 --- a/src/types/bill.d.ts +++ b/src/types/bill.d.ts @@ -1,6 +1,8 @@ declare type BaseDate = number | Date | string | null; declare type BillStatus = 'PAID' | 'CANCELLED' | 'PENDING' +declare type ConfirmStatus = 'UNCONFIRMED' | 'CONFIRMED' +declare type SortOrderType = 'ASC'|'DESC' | string; // 现场支付账单数据 declare type ManualCreateBillParam = { @@ -12,10 +14,9 @@ declare type BillDetail = { id: string | number; bill_id: string | number; bill_type: string; - confirm_status?: number; + confirm_status: ConfirmStatus; amount: decimal; } -declare type SortOrderType = 'ASC'|'DESC' | string; /** * 账单查询参数 */ @@ -31,6 +32,7 @@ declare type BillQueryParam = { start_date:string; end_date:string; department:string; + confirm_status: ConfirmStatus; sort_field:string; sort_order:SortOrderType; } @@ -57,19 +59,22 @@ declare type BillModel = { service_charge?: number; payment_amount?: number; actual_payment_amount?: null | number; - payment_method?: null | string | number; + payment_method?: null | string; currency?: string; - payment_channel?: null | string | number; + payment_channel?: null | string ; expiration_time?: BaseDate; status: string; apply_status: string; paid_area?: null | string | number; paid_at?:BaseDate; - merchant_ref: null | string; + merchant_ref?: string; payment_id?: null | string | number; create_at: BaseDate; update_at: BaseDate; - + student_first_name: string; + student_last_name: string; + remark: string; + confirm_status: ConfirmStatus; details: BillDetail[] } @@ -91,3 +96,12 @@ declare type AsiaPayModel = { type ExternalCreateParamsType = { [key: string]: string | null | BillDetail[]; } + +type BillUpdateParams = { + payment_channel: string; + payment_method: string; + actual_payment_amount?: number | string; + remark?: string; + merchant_ref?: string; + payment_amount?: number | string; +} \ No newline at end of file