385 lines
11 KiB
TypeScript
385 lines
11 KiB
TypeScript
import {
|
|
Button,
|
|
Col,
|
|
Descriptions,
|
|
Divider,
|
|
Form,
|
|
InputNumber,
|
|
Modal,
|
|
Row,
|
|
Select,
|
|
Space,
|
|
Toast
|
|
} from "@douyinfe/semi-ui";
|
|
import {IconAlertCircle} from "@douyinfe/semi-icons";
|
|
import {Data} from "@douyinfe/semi-ui/lib/es/descriptions";
|
|
import React, {useMemo} 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";
|
|
import MoneyFormat from "@/components/money-format.tsx";
|
|
import {BizError} from "@/service/types.ts";
|
|
|
|
type BillPaidModalProps = {
|
|
onConfirm: () => void
|
|
onCancel?: () => void
|
|
}
|
|
type BillTypeListProps = {
|
|
onClose?: (refresh?: boolean) => void;
|
|
onChange?: (confirms: ConfirmedBillDetail[]) => void;
|
|
}
|
|
|
|
export const BillTypeList: React.FC<BillTypeListProps> = (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 (<div className={'bill-type-list'} style={{marginTop: 10}}>
|
|
<Divider>{t('bill.title_bill_detail')}</Divider>
|
|
{
|
|
state.confirmed.map((item, index) => (
|
|
<div key={index} className="confirm-item-btn align-center space-between" style={{marginTop: 20}}>
|
|
<Select
|
|
value={item.bill_type}
|
|
style={{width: 240}}
|
|
onChange={v => onChange(String(v), index, 'type')}
|
|
placeholder={t('base.please_select_bill_type')}>
|
|
{
|
|
BillTypes.map((it, idx) => (
|
|
<Select.Option key={idx} value={it.label}>{it.label}</Select.Option>))
|
|
}
|
|
</Select>
|
|
|
|
<Space spacing={10}>
|
|
<InputNumber
|
|
hideButtons precision={2} value={item.amount} type={'number'}
|
|
onChange={v => onChange(String(v), index, 'amount')} style={{width: 140}}/>
|
|
<Button
|
|
disabled={state.confirmed.length <= 1} onClick={() => addOrRemove(index)}
|
|
theme={'solid'} type={'secondary'}>{t('base.remove')}</Button>
|
|
</Space>
|
|
</div>
|
|
))
|
|
}
|
|
<div style={{marginTop: 10, marginBottom: 20}}>
|
|
<Button onClick={() => addOrRemove(-1)}>{t('base.add')}</Button>
|
|
</div>
|
|
</div>)
|
|
}
|
|
|
|
export const AddBillModal: React.FC<BillPaidModalProps> = (props) => {
|
|
const {t, i18n} = useTranslation()
|
|
const {paymentChannelList, paymentMethodList} = usePaymentChannels();
|
|
|
|
const [state, setState] = useSetState<{
|
|
loading?: boolean;
|
|
confirmLoading?: boolean;
|
|
open?: boolean;
|
|
details: ConfirmedBillDetail[];
|
|
errorMessage?: string;
|
|
errorConfirmMessage?: string;
|
|
errorConfirmOk?: boolean;
|
|
values?: CreateBillRecordModel;
|
|
}>({
|
|
details: []
|
|
})
|
|
|
|
|
|
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;
|
|
}
|
|
}
|
|
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")
|
|
setState({
|
|
loading: true, errorMessage: undefined,
|
|
values,errorConfirmOk:false
|
|
})
|
|
addBillRecord(values).then(() => {
|
|
setState({open: false})
|
|
Toast.success(t('base.add_success'))
|
|
props.onConfirm()
|
|
}).catch((e: BizError) => {
|
|
if (e.code == -50415) { // STUDENT_INFO_NOT_FOUND
|
|
// duplicate
|
|
setState({errorConfirmMessage: e.message,errorConfirmOk:true})
|
|
} else {
|
|
setState({errorConfirmMessage: e.message})
|
|
}
|
|
}).finally(() => {
|
|
setState({
|
|
loading: false
|
|
})
|
|
})
|
|
//
|
|
}
|
|
const handleClose = () => {
|
|
setState({
|
|
loading: false,
|
|
open: false,
|
|
errorMessage: undefined
|
|
})
|
|
}
|
|
|
|
// process confirm
|
|
const handleCloseConfirm = () => {
|
|
setState({errorConfirmMessage: undefined})
|
|
}
|
|
const handleConfirmAdd = () => {
|
|
if (!state.values) return;
|
|
setState({confirmLoading: true})
|
|
state.values.check_student = false;
|
|
addBillRecord(state.values).then(() => {
|
|
setState({
|
|
loading: false,
|
|
open: false,
|
|
errorMessage: undefined,
|
|
errorConfirmMessage: undefined,
|
|
})
|
|
Toast.success(t('base.add_success'))
|
|
props.onConfirm()
|
|
}).catch(e => {
|
|
setState({errorConfirmMessage: e.message})
|
|
}).finally(() => {
|
|
setState({
|
|
confirmLoading: false,
|
|
})
|
|
})
|
|
}
|
|
|
|
const details = useMemo(() => {
|
|
if (!state.values) return;
|
|
const {
|
|
application_number, payment_channel, payment_method,
|
|
student_email, merchant_ref, paid_area,
|
|
initiated_paid_date, paid_date, delivered_date
|
|
} = state.values
|
|
//, span: 2
|
|
const _data: Data[] = [
|
|
{key: 'Merchant Ref', value: merchant_ref},
|
|
{key: t('bill.bill_number'), value: application_number},
|
|
{key: 'Email', value: student_email},
|
|
{key: t('bill.title_pay_channel'), value: payment_channel},
|
|
{key: t('bill.title_pay_method'), value: payment_method},
|
|
{key: t('bill.create.pay_area'), value: paid_area},
|
|
{key: t('bill.title_initiated_paid_at'), value: initiated_paid_date},
|
|
{key: t('bill.title_paid_at'), value: paid_date},
|
|
{key: t('bill.title_delivered_at'), value: delivered_date, span: 2},
|
|
]
|
|
state.details.forEach(it => {
|
|
_data.push({key: t('bill.title_bill_type'), value: it.bill_type})
|
|
_data.push({key: t('bill.title_amount'), value: <MoneyFormat money={it.amount}/>})
|
|
});
|
|
return _data
|
|
}, [i18n.language, state.values]);
|
|
return (<>
|
|
<Button onClick={() => setState({open: true})} theme={'solid'}>{t('bill.add_bill_record')}</Button>
|
|
<Modal
|
|
title={t('bill.add_bill_record')}
|
|
visible={state.open}
|
|
closeOnEsc={true}
|
|
onCancel={handleClose}
|
|
footer={null}
|
|
width={600}
|
|
maskClosable={false}
|
|
>
|
|
<Form<CreateBillRecordModel> onSubmit={onSubmit} initValues={{
|
|
merchant_ref: '',
|
|
application_number: '',
|
|
student_email: '',
|
|
paid_area: '',
|
|
initiated_paid_date: '',
|
|
paid_date: '',
|
|
delivered_date: '',
|
|
payment_method: '',
|
|
payment_channel: '',
|
|
check_student: true,
|
|
details: []
|
|
}}>
|
|
<Row gutter={20}>
|
|
<Col span={12}>
|
|
<Form.Input
|
|
rules={[
|
|
{required: true, message: 'required error'},
|
|
]}
|
|
showClear field="merchant_ref" label="Merchant Ref"
|
|
placeholder={t('base.please_enter')} style={{width: '100%'}}/>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Input
|
|
rules={[
|
|
{required: true, message: 'required error'},
|
|
]}
|
|
showClear field="application_number" label={t('bill.bill_number')}
|
|
placeholder={t('base.please_enter')} style={{width: '100%'}}/>
|
|
</Col>
|
|
</Row>
|
|
<Row gutter={20}>
|
|
<Col span={12}>
|
|
<Form.Input
|
|
rules={[
|
|
{required: true, message: 'required error'},
|
|
{type: 'email', message: t('base.validate.email')}
|
|
]}
|
|
type={'email'}
|
|
showClear field="student_email" label="Email"
|
|
placeholder={t('base.please_enter')} style={{width: '100%'}}/>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Input
|
|
rules={[
|
|
{required: true, message: 'required error'},
|
|
]}
|
|
showClear field="paid_area" label={t('bill.create.pay_area')}
|
|
placeholder={t('base.please_enter')} style={{width: '100%'}}/>
|
|
</Col>
|
|
</Row>
|
|
<Row gutter={20}>
|
|
<Col span={12}>
|
|
<Form.Select
|
|
rules={[
|
|
{required: true, message: 'required error'},
|
|
]}
|
|
optionList={paymentChannelList}
|
|
allowCreate filter showClear field="payment_channel" label={t('bill.title_pay_channel')}
|
|
placeholder={t('base.please_select')} style={{width: '100%'}}/>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Select
|
|
rules={[
|
|
{required: true, message: 'required error'},
|
|
]}
|
|
optionList={paymentMethodList}
|
|
allowCreate filter showClear field="payment_method" label={t('bill.title_pay_method')}
|
|
placeholder={t('base.please_select')} style={{width: '100%'}}/>
|
|
</Col>
|
|
</Row>
|
|
<Row gutter={20}>
|
|
<Col span={12}>
|
|
<Form.DatePicker
|
|
showClear
|
|
rules={[
|
|
{required: true, message: 'required error'},
|
|
]}
|
|
type={'date'}
|
|
field="initiated_paid_date"
|
|
label={t('bill.title_initiated_paid_at')}
|
|
style={{width: '100%'}}
|
|
/>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.DatePicker
|
|
showClear
|
|
rules={[
|
|
{required: true, message: 'required error'},
|
|
]}
|
|
type={'date'}
|
|
field="paid_date"
|
|
label={t('bill.title_paid_at')}
|
|
style={{width: '100%'}}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
<Row gutter={20}>
|
|
<Col span={12}>
|
|
<Form.DatePicker
|
|
showClear
|
|
rules={[
|
|
{required: true, message: 'required error'},
|
|
]}
|
|
type={'date'}
|
|
field="delivered_date"
|
|
label={t('bill.title_delivered_at')}
|
|
style={{width: '100%'}}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
<div>
|
|
<BillTypeList onChange={(details) => setState({details})}/>
|
|
{state.errorMessage &&
|
|
<div className="semi-form-field-error-message align-center" style={{marginBottom: 10}}>
|
|
<IconAlertCircle/>
|
|
<span style={{marginLeft: 5}}>{state.errorMessage}</span>
|
|
</div>}
|
|
<Divider/>
|
|
</div>
|
|
<div className={'text-right'} style={{margin: '20px 0'}}>
|
|
<Space spacing={12}>
|
|
<Button onClick={handleClose} type={'tertiary'}>{t('base.cancel')}</Button>
|
|
<Button
|
|
loading={state.loading} htmlType={'submit'} theme={'solid'}
|
|
type={'primary'}>{t('bill.create.confirm')}</Button>
|
|
</Space>
|
|
</div>
|
|
</Form>
|
|
</Modal>
|
|
|
|
<Modal
|
|
visible={!!state.errorConfirmMessage}
|
|
title={t('base.confirm_information')}
|
|
closeOnEsc={true}
|
|
onCancel={handleCloseConfirm}
|
|
width={600}
|
|
maskClosable={false}
|
|
footer={
|
|
<div className={'text-center'}>
|
|
<Space>
|
|
<Button onClick={handleCloseConfirm} type={'tertiary'}>{t('base.cancel')}</Button>
|
|
{state.errorConfirmOk && <Button loading={state.confirmLoading} onClick={handleConfirmAdd} theme={'solid'}>
|
|
{t('base.confirm_and_add')}
|
|
</Button>}
|
|
</Space>
|
|
</div>
|
|
}
|
|
>
|
|
<Descriptions layout='horizontal' align='plain' data={details} column={2}/>
|
|
<Divider style={{margin: '10px 0'}}>{t('base.title_error_tip')}</Divider>
|
|
{state.errorConfirmMessage &&
|
|
<div className="semi-form-field-error-message align-center" style={{marginBottom: 10}}>
|
|
<IconAlertCircle/>
|
|
<span style={{marginLeft: 5}}>{state.errorConfirmMessage}</span>
|
|
</div>}
|
|
</Modal>
|
|
</>)
|
|
} |