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>
</>)
}