feat: add bill type confirm

This commit is contained in:
LittleBoy 2024-08-09 12:28:48 +08:00
parent 9c6fbd839d
commit 5f16a7b274
18 changed files with 475 additions and 140 deletions

View File

@ -105,6 +105,9 @@ body #root{
}
/***************** semi overrides ****************/
.semi-dropdown-item{
max-width: 500%;
}
.semi-dropdown-item-active {
background-color: var(--semi-color-default-active);
}
@ -145,6 +148,11 @@ body #root{
border: 1px solid var(--semi-color-focus-border);
}
}
.semi-tagInput-wrapper-input{
&:hover{
border-color: transparent;
}
}
.semi-input-wrapper-focus,
.semi-datepicker-range-input-active,

View File

@ -23,7 +23,7 @@ type BillListProps = {
loading?: boolean;
beforeTotalAmount?: React.ReactNode;
}
const CheckNumberCorrect = ({origin, confirmed}: { origin: string, confirmed?: string }) => {
const CheckNumberCorrect = ({origin, confirmed}: { origin: string, confirmed?: string|null }) => {
if (origin == confirmed && origin) {
return (<Space style={{marginTop: 2, color: 'green'}}><span>{origin}</span><IconCheckCircleStroked/></Space>)
}
@ -43,7 +43,7 @@ export const BillList: React.FC<BillListProps> = (props) => {
}>({
showCols: [
"id", "merchant_ref", "student_number", "application_number", "initiated_paid_at", "delivered_at", "paid_at", "student_english_name", "student_email", "programme_chinese_name",
"intake_year", "detail", "_detail", "amount", "pay_amount", "actual_payment_amount", "pay_method", "status", "apply_status"
"intake_year", "detail", "detail_confirms", "amount", "pay_amount", "actual_payment_amount", "pay_method", "status", "apply_status"
]
})
@ -90,16 +90,22 @@ export const BillList: React.FC<BillListProps> = (props) => {
title: t('base.student_number'),
dataIndex: 'student_number',
width: 150,
render: (value, record) => (
<CheckNumberCorrect origin={value} confirmed={record.student_number_confirm || 'test-confirm'}/>)
render: (value:string) => (value|| 'N/A')
},
{
title: t('base.bill_number'),
dataIndex: 'application_number',
width: 150,
render: (value, record) => (
<CheckNumberCorrect origin={value} confirmed={value || record.application_number_confirm}/>)
<CheckNumberCorrect origin={value} confirmed={record.confirm_application_number}/>)
},
// {
// title: t('bill.title_application_number_confirmed'),
// dataIndex: 'application_number',
// width: 150,
// render: (value, record) => (
// <CheckNumberCorrect origin={value} confirmed={value || record.application_number_confirm}/>)
// },
{
title: <div className="table-header-title">{t('bill.title_initiated_paid_at')}
<div className="tips">(PPS Input Date)</div>
@ -182,7 +188,7 @@ export const BillList: React.FC<BillListProps> = (props) => {
},
{
title: t('bill.title_bill_type_confirm'),
dataIndex: '_detail',
dataIndex: 'detail_confirms',
ellipsis: {showTitle: true},
width: 220,
render: (_, record) => (<div style={{
@ -192,8 +198,8 @@ export const BillList: React.FC<BillListProps> = (props) => {
maxWidth: '100%',
whiteSpace: 'normal'
}}>
{record.details.filter(s => s.confirm_status == 'CONFIRMED').map((it) => (
<div key={it.id}>{it.confirm_type}: <MoneyFormat money={it.amount}/></div>))}
{record.detail_confirms?.map((it) => (
<div key={it.id}>{it.bill_type}: <MoneyFormat money={it.amount}/></div>))}
</div>),
},
{
@ -327,8 +333,7 @@ export const BillList: React.FC<BillListProps> = (props) => {
</div>
<div className="table-column-action" style={{marginTop: 20}}>
<Space>
<Button>{t('base.cancel')}</Button>
<Button theme={'solid'}>{t('base.confirm')}</Button>
<Button onClick={()=>setState({showColumnsConfig:false})}>{t('base.close')}</Button>
</Space>
</div>

View File

@ -4,7 +4,7 @@ import dayjs from "dayjs";
import {useTranslation} from "react-i18next";
import {Card} from "@/components/card";
import {BillQueryParams} from "@/service/api/bill.ts";
import {BillTypes} from "@/service/bill-types.ts";
import {useBillTypes} from "@/hooks/useBillTypes.ts";
type SearchFormProps = {
onSearch?: (params: BillQueryParams) => void;
@ -27,6 +27,7 @@ type SearchFormFields = {
sort_by?: string;
}
const SearchForm: React.FC<SearchFormProps> = (props) => {
const BillTypes = useBillTypes()
const formSubmit = (value: SearchFormFields) => {
const params: BillQueryParams = {}

View File

@ -105,4 +105,16 @@ export const IconReconciliation = ({style}: { style?: React.CSSProperties }) =>
</g>
</g>
</svg>
)
export const IconPermission = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 1024 1024">
<path
d="M952.569193 174.896498C762.876124 133.373939 608.868903 72.549279 512 0 415.131097 72.549279 261.123876 133.373939 71.686803 174.845299a25.599604 25.599604 0 0 0-20.479684 25.087612V409.593672c0 250.876124 162.608688 472.261504 409.593672 602.153897a113.099053 113.099053 0 0 0 102.398418 0C810.184193 881.855175 972.792881 660.469796 972.792881 409.593672V199.676915a25.599604 25.599604 0 0 0-20.223688-24.780417zM895.994067 409.593672c0 213.142307-137.777071 412.819222-368.634304 534.212546a37.068227 37.068227 0 0 1-30.719526 0C265.783004 822.412894 128.005933 622.735979 128.005933 409.593672V240.636282c156.720779-37.426622 287.688355-87.909042 383.994067-147.914515 96.305712 60.107871 227.273289 110.590291 383.994067 147.914515z"
fill="currentColor" p-id="4295"></path>
<path
d="M639.998022 691.189321h-102.398418v-76.798813h102.398418a25.599604 25.599604 0 0 0 0-51.199209h-102.398418V457.925725a115.19822 115.19822 0 1 0-51.199208 0V793.587739a25.599604 25.599604 0 0 0 51.199208 0v-51.199209h102.398418a25.599604 25.599604 0 0 0 0-51.199209zM448.000989 345.594661A63.999011 63.999011 0 1 1 512 409.593672a63.999011 63.999011 0 0 1-63.999011-63.999011z"
fill="currentColor" p-id="4296"></path>
</svg>
)

26
src/hooks/useBillTypes.ts Normal file
View File

@ -0,0 +1,26 @@
import {useEffect, useState} from "react";
import {selectBillTypeList} from "@/service/api/bill.ts";
type BillTypeItem = {
value: string;
label: string;
}
const BillTypesCache:BillTypeItem[] = [];
export function useBillTypes(){
const [BillTypes,setBillTypes] = useState<BillTypeItem[]>(BillTypesCache)
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.push(...types)
})
}
},[])
return BillTypes
}

View File

@ -14,6 +14,7 @@
"qr-code": "QRCode",
"query_bill": "Failed to query bill:",
"remove": "Remove",
"save": "Save",
"student_number": "Student Number"
},
"bill": {
@ -24,6 +25,7 @@
"cancel_confirm_bills": "Confirm the check check bill?",
"cancel_success": "Successful cancel bill",
"confirm": "Check",
"confirm_amount_exceed_content": "Amount exceeds total amount",
"confirm_batch": "Batch Confirm",
"confirm_bill": "Confirm Bill Information",
"confirm_bill_number": "Confirm Bill Number",
@ -37,6 +39,7 @@
"download-qr-code": "Download QR Code",
"download_receipt": "Download receipt",
"export_excel": "Export Excel",
"import_bill": "Add Bill",
"import_excel": "Import Bill",
"paid": "Paid",
"paid_confirm": "Please confirm the order status is set to paid",
@ -92,7 +95,8 @@
"menu": {
"bill": "Bill Query",
"check": "Reconciliation",
"manual": "Manual Pay"
"manual": "Manual Pay",
"permission": "Permission"
}
},
"login": {

View File

@ -25,6 +25,7 @@
"cancel_confirm_bills": "确认对账选中账单?",
"cancel_success": "作废账单成功",
"confirm": "对账",
"confirm_amount_exceed_content": "金额超出总金额",
"confirm_batch": "批量对账",
"confirm_bill": "确认账单信息",
"confirm_bill_number": "确认账单编号",
@ -38,6 +39,7 @@
"download-qr-code": "下载二维码",
"download_receipt": "下载收据",
"export_excel": "导出账单",
"import_bill": "添加账单",
"import_excel": "导入账单",
"paid": "已支付",
"paid_confirm": "是否将此订单状态设为已支付",

View File

@ -25,6 +25,7 @@
"cancel_confirm_bills": "確認對帳選取帳單?",
"cancel_success": "作廢帳單成功",
"confirm": "對帳",
"confirm_amount_exceed_content": "金額超出總金額",
"confirm_batch": "批次對帳",
"confirm_bill": "確認帳單資訊",
"confirm_bill_number": "確認帳單編號",
@ -38,6 +39,7 @@
"download-qr-code": "下載二維碼",
"download_receipt": "下載收據",
"export_excel": "導出賬單",
"import_bill": "新增帳單",
"import_excel": "導入賬單",
"paid": "已支付",
"paid_confirm": "是否將此訂單狀態設為已支付",

View File

@ -0,0 +1,137 @@
import {BillDetailItems} from "@/components/bill";
import {Button, Col, Form, Modal, Row, Select, Space} from "@douyinfe/semi-ui";
import React from "react";
import {useTranslation} from "react-i18next";
import {useSetState} from "ahooks";
import {useBillTypes} from "@/hooks/useBillTypes.ts";
type BillPaidModalProps = {
onConfirm: () => void
onCancel?: () => void
}
export const AddBillModal: React.FC<BillPaidModalProps> = (props) => {
const {t} = useTranslation()
const BillTypes = useBillTypes()
const [state, setState] = useSetState<{
loading?: boolean;
open?:boolean
}>({})
const onSubmit = (values: BillUpdateParams) => {
setState({
loading: true
})
}
return (<>
<Button onClick={()=>setState({open:true})} theme={'solid'}>{t('bill.import_bill')}</Button>
<Modal
title={t('bill.import_bill')}
visible={state.open}
closeOnEsc={true}
onCancel={()=>setState({open:false})}
footer={null}
width={600}
okText={t('base.confirm')}
maskClosable={false}
>
<Form<BillUpdateParams> onSubmit={onSubmit} initValues={{
payment_channel: 'FLYWIRE',
payment_method: '',
merchant_ref: props.bill?.merchant_ref,
payment_amount: props.bill?.amount,
actual_payment_amount: props.bill?.amount
}}>
<Row gutter={20}>
<Col span={12}>
<Form.Select
field={t('manual.bill_type')}
style={{width: '100%'}}
placeholder={t('base.please_select')}>
{
BillTypes.map((it, idx) => (
<Select.Option key={idx} value={it.label}>{it.label}</Select.Option>))
}
</Form.Select>
</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
type={'number'}
rules={[
{required: true, message: 'required error'},
]}
showClear field="amount" label={t('bill.title_amount')}
placeholder={t('base.please_enter')} style={{width: '100%'}}/>
</Col>
<Col span={12}>
<Form.Select
rules={[
{required: true, message: 'required error'},
]}
optionList={[
{value: 'card', label: 'Card(VISA,MasterCard,UnionPay,JCB...)'},
{value: 'wechat', label: 'Wechat'},
{value: 'alipay', label: 'Alipay'},
{value: 'other', label: 'Other'},
]}
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.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={6}>
<Form.Input
rules={[
{required: true, message: 'required error'},
]} type={'number'} showClear
field="payment_amount" label={t('bill.title_pay_amount')}
placeholder={t('base.please_enter')} style={{width: '100%'}}/>
</Col>
<Col span={6}>
<Form.Input
type={'number'}
showClear field="actual_payment_amount" label={t('bill.title_actual_payment_amount')}
placeholder={t('base.please_enter')} style={{width: '100%'}}/>
</Col>
</Row>
<Row gutter={20}>
<Col span={24}>
<Form.TextArea
rules={[{required: true, message: 'required error'}]}
showClear field="remark" label={t('bill.title_remark')} placeholder={t('base.please_enter')}
style={{width: '100%'}}/>
</Col>
</Row>
{/*<p style={{marginTop: 10}}>{t('bill.paid_confirm')}</p>*/}
<div className={'text-right'} style={{margin: '10px 0 20px'}}>
<Space spacing={12}>
<Button onClick={props.onCancel} type={'tertiary'}>{t('base.cancel')}</Button>
<Button
loading={state.loading} htmlType={'submit'} theme={'solid'}
type={'primary'}>{t('base.confirm_paid')}</Button>
</Space>
</div>
</Form>
</Modal>
</>)
}

View File

@ -1,73 +1,216 @@
import {Button, Select, Popconfirm, Space, Tag, Divider, Input} from "@douyinfe/semi-ui";
import React, {useState} from "react";
import {Button, Select, Space, Divider, InputNumber, Modal, Toast} from "@douyinfe/semi-ui";
import React, {useMemo} from "react";
import {useSetState} from "ahooks";
import MoneyFormat from "@/components/money-format.tsx";
import {confirmBillType} from "@/service/api/bill.ts";
import {useTranslation} from "react-i18next";
import {BillTypes} from "@/service/bill-types.ts";
import {useBillTypes} from "@/hooks/useBillTypes.ts";
import {NumberConfirm} from "@/pages/bill/components/number_confirm.tsx";
import {BillDetailItems} from "@/components/bill";
import {BillDetailItem} from "@/components/bill/bill-detail-items.tsx";
import {IconStudentId} from "@/components/icons";
import {confirmBillType} from "@/service/api/bill.ts";
type BillTypeConfirmProps = {
data: BillDetail
bill: BillModel;
onClose?: (refresh?: boolean) => void;
onChange?: (confirms: ConfirmedBillDetail[]) => void;
}
export const BillTypeConfirm: React.FC<BillTypeConfirmProps> = (props) => {
const [it, setItem] = useState(props.data)
const BillTypeConfirmItem = (props: { data: BillDetail; onChange: (confirms: ConfirmedBillDetail[]) => void; }) => {
const it = props.data;
const BillTypes = useBillTypes()
const {t} = useTranslation()
const [state, setState] = useSetState<{
billTypeList: BillTypeConfirm[];
billTypeList: ConfirmedBillDetail[];
loading?: boolean,
confirmed?: boolean,
}>({
loading: false,
billTypeList: [
{bill_type:props.data.bill_type,amount:props.data.amount}
{bill_type: props.data.bill_type, bill_detail_id: it.id, amount: Number(props.data.amount)}
]
})
const onConfirmBill = () => {
setState({loading: true})
// confirmBillType({id: it.id, type: state.bill_type}).then(() => {
// setState({loading: false})
// setItem({...it, confirm_status: 'CONFIRMED'})
// }).catch(() => {
// setState({loading: false})
// })
}
const onChange = (value:string,index:number,type:'type'|'amount')=>{
const onChange = (value: string, index: number, type: 'type' | 'amount') => {
if (state.billTypeList.length <= index || !value) return;
const billTypeList = [...state.billTypeList]
if (type == 'type') {
billTypeList[index].bill_type = value
} else {
billTypeList[index].amount = Number(value)
}
// 计算 confirmedTypes 中所有 amount 的总金额
const totalAmount = billTypeList.reduce((total, item) => {
return total + Number(item.amount)
}, 0)
// 判断是否已经超出账单实际金额
if (totalAmount > Number(it.amount)) {
Modal.warning({
title: 'Warning',
content: t('bill.confirm_amount_exceed_content'),
hasCancel: false
})
return;
}
setState({billTypeList})
props.onChange(billTypeList)
}
const onRemove = (index:number) =>{
if(state.billTypeList.length <= 1) return;
const onRemove = (index: number) => {
if (state.billTypeList.length <= 1) return;
const billTypeList = [...state.billTypeList]
billTypeList.splice(index, 1)
setState({billTypeList})
}
const onAdd = () => {
const billTypeList = [
...state.billTypeList,
{bill_type: props.data.bill_type, amount: 0, bill_detail_id: it.id}
]
setState({
billTypeList
})
props.onChange(billTypeList)
}
return (<>
<div className="confirm-item" style={{margin: '20px 0'}}>
<div style={{lineHeight: 1.1}} className={'align-center'}>
<div>{it.bill_type}</div>
<span style={{marginLeft: 100}}>Total Amount:</span>
<span style={{margin: '0 10px 0 120px'}}>Total Amount:</span>
<MoneyFormat money={it.amount}/>
</div>
{state.billTypeList.map((item, index) => {
return (<div key={index} className="confirm-item-btn align-center space-between" style={{marginTop: 10}}>
<Select
value={item.bill_type || it.bill_type}
style={{width: 200}}
onChange={v=>onChange(String(v),index,'type')}
placeholder={t('manual.bill_type')}>
{
BillTypes.map((it, idx) => (
<Select.Option key={idx} value={it.label}>{it.label}</Select.Option>))
}
</Select>
return (
<div key={index} className="confirm-item-btn align-center space-between" style={{marginTop: 10}}>
<Select
value={item.bill_type || it.bill_type}
style={{width: 200}}
onChange={v => onChange(String(v), index, 'type')}
placeholder={t('manual.bill_type')}>
{
BillTypes.map((it, idx) => (
<Select.Option key={idx} value={it.label}>{it.label}</Select.Option>))
}
</Select>
<Space spacing={10}>
<Input value={item.amount} type={'number'} onChange={v=>onChange(v,index,'amount')} style={{width: 120}}/>
<Button disabled={state.billTypeList.length <= 1} onClick={()=>onRemove(index)} theme={'solid'} type={'secondary'}>{t('base.remove')}</Button>
</Space>
</div>)
<Space spacing={10}>
<InputNumber
hideButtons precision={2} value={item.amount} type={'number'}
onChange={v => onChange(String(v), index, 'amount')} style={{width: 120}}/>
<Button
disabled={state.billTypeList.length <= 1} onClick={() => onRemove(index)}
theme={'solid'} type={'secondary'}>{t('base.remove')}</Button>
</Space>
</div>)
})}
<div style={{marginTop:10}}>
<Button theme={'solid'}>{t('base.add')}</Button>
<div style={{marginTop: 10}}>
<Button onClick={onAdd}>{t('base.add')}</Button>
</div>
</div>
<Divider margin='12px'/>
</>)
}
export const BillTypeConfirm: React.FC<BillTypeConfirmProps> = (props) => {
const {t} = useTranslation()
const [state, setState] = useSetState<{
confirm_application_number: string;
confirmed: {
[key: number]: ConfirmedBillDetail[]
}
}>({
confirm_application_number: '', confirmed: {}
})
const details = useMemo(() => {
const {details, detail_confirms} = props.bill;
if (!details) return [];
details.forEach(it => {
if (!detail_confirms) it.confirmed = [];
else it.confirmed = detail_confirms.filter(s => s.bill_detail_id == it.id)
})
return details;
}, [props.bill])
const onChange = (id: number, confirmedTypes: ConfirmedBillDetail[]) => {
const confirmed = {
...state.confirmed
};
confirmed[id] = confirmedTypes
setState({confirmed})
// trigger
const allConfirmed: ConfirmedBillDetail[] = [];
Object.keys(confirmed).forEach(key => {
allConfirmed.push(...confirmed[Number(key)])
})
props.onChange?.(allConfirmed)
}
return (<>
<Divider>Bill Type Confirm</Divider>
{
details.map((it, idx) => (<BillTypeConfirmItem
onChange={(confirmed) => onChange(it.id, confirmed)} data={it} key={idx}/>))
}
</>)
}
export const BillTypeConfirmModal: React.FC<BillTypeConfirmProps> = (props) => {
const {t} = useTranslation()
const [state, setState] = useSetState<{
confirm_application_number: string;
detail_confirms: ConfirmedBillDetail[];
loading?: boolean;
}>({
confirm_application_number: '',
detail_confirms: []
})
const onBillConfirm = () => {
setState({loading: true})
confirmBillType([{
id: props.bill.id,
confirm_student_number: props.bill.student_number,
...state
}]).then(() => {
props.onClose?.(true)
}).finally(() => {
setState({loading: false})
})
}
return (<Modal
title={t('bill.confirm_bill')}
visible={true}
closeOnEsc={true}
width={550}
footer={null}
onCancel={() => props.onClose?.()}
>
<div>
<BillDetailItems bill={props.bill} studentNumberRender={<>
<BillDetailItem
icon={<IconStudentId/>} title={t('manual.student_number')}
value={props.bill.student_number || '-'}/>
<BillDetailItem
icon={<IconStudentId/>} title={t('base.bill_number')}
value={props.bill.application_number || '-'}/>
</>}/>
</div>
<div className="confirm-number-container" style={{padding: '15px 0'}}>
<Divider>Bill Number Confirm</Divider>
{/*{*/}
{/* !state.confirmBill.student_number_confirm &&*/}
{/* <NumberConfirm bill={state.confirmBill} type={'student_number'}/>*/}
{/*}*/}
<NumberConfirm
onChange={confirm_application_number => setState({confirm_application_number})}
bill={props.bill} type={'application_number'}/>
</div>
<BillTypeConfirm bill={props.bill} onChange={detail_confirms => setState({detail_confirms})}/>
<div className={'text-center'} style={{paddingBottom: 20}}>
<Button
onClick={onBillConfirm} loading={state.loading} type={'primary'}
theme={'solid'}>{t('base.confirm')}</Button>
</div>
</Modal>)
}

View File

@ -1,55 +1,52 @@
import {Button, Space, Tag, Input} from "@douyinfe/semi-ui";
import React from "react";
import {Space, Input} from "@douyinfe/semi-ui";
import React, {useEffect} from "react";
import {useSetState} from "ahooks";
import {useTranslation} from "react-i18next";
type NumberConfirmProps = {
bill: BillModel;
type: 'student_number' | 'application_number'
type: 'student_number' | 'application_number';
onChange: (value: string) => void;
}
export const NumberConfirm: React.FC<NumberConfirmProps> = (props) => {
export const NumberConfirm: React.FC<NumberConfirmProps> = ({bill, type, onChange}) => {
const {t} = useTranslation()
const [state, setState] = useSetState({
loading: false,
confirmed: false,
confirmNumber: (props.type == 'application_number' ? props.bill.application_number : props.bill.student_number) || '',
confirmNumber: '',
})
const onConfirm = () => {
if (!state.confirmNumber.length) return
setState({loading: true})
setTimeout(() => {
setState({loading: false, confirmed: true})
}, 500)
// confirmBillType({id:it.id,type:state.bill_type}).then(() => {
// setState({loading:false})
// setItem({...it,confirm_status:'CONFIRMED'})
// }).catch(() => {
// setState({loading:false})
// })
const onValueChange = (confirmNumber: string) => {
setState({confirmNumber})
onChange(confirmNumber)
}
useEffect(() => {
const confirmNumber = (type == 'application_number' ? (bill.application_number_confirm || bill.application_number) : bill.student_number) || '';
onValueChange(confirmNumber)
}, [])
return <div
className="confirm-item align-center space-between"
style={{marginBottom: 15}}>
style={{marginBottom: 15, marginTop: 15}}>
<div>
<div>{t(props.type == 'student_number' ? 'bill.confirm_student_number' : 'bill.confirm_bill_number')}</div>
<div>{t(type == 'student_number' ? 'bill.confirm_student_number' : 'bill.confirm_bill_number')}</div>
</div>
<div className="confirm-item-btn">
<Space spacing={15}>
{!state.confirmed && <Input
onChange={v => setState({confirmNumber: String(v)})}
defaultValue={state.confirmNumber}
style={{width: 180}} placeholder={t('base.please_enter')}/>}
{
state.confirmed ? <Space>
<div>{state.confirmNumber}</div>
<Tag size={'large'} color='light-blue'>CONFIRMED</Tag>
</Space> : <Button
style={{width: 80}} disabled={!state.confirmNumber} onClick={onConfirm}
loading={state.loading} theme={'solid'}>{t('base.confirm')}</Button>
}
<Input
onChange={onValueChange} style={{width: 180}}
value={state.confirmNumber} placeholder={t('base.please_enter')}/>
{/*{*/}
{/* state.confirmed ? <Space>*/}
{/* <div>{state.confirmNumber}</div>*/}
{/* <Tag size={'large'} color='light-blue'>CONFIRMED</Tag>*/}
{/* </Space> : <Button*/}
{/* style={{width: 80}} disabled={!state.confirmNumber} onClick={onConfirm}*/}
{/* loading={state.loading} theme={'solid'}>{t('base.confirm')}</Button>*/}
{/*}*/}
</Space>
</div>
</div>

View File

@ -1,4 +1,4 @@
import {Button, ButtonGroup, Divider, Modal, Notification, Popconfirm, Toast} from "@douyinfe/semi-ui";
import {Button, ButtonGroup, Modal, Notification, Popconfirm, Space, Toast} from "@douyinfe/semi-ui";
import {useState} from "react";
import {useRequest, useSetState} from "ahooks";
import {useTranslation} from "react-i18next";
@ -9,13 +9,10 @@ import BillDetail from "@/components/bill/detail.tsx";
import {billList, BillQueryParams, exportBillList, modifyBillStatus} from "@/service/api/bill.ts";
import {BillStatus, BizError} from "@/service/types.ts";
import {useDownloadReceiptPDF} from "@/service/generate-pdf.ts";
import {BillDetailItems} from "@/components/bill";
import {BillPaidModal} from "@/pages/bill/components/bill_paid_modal.tsx";
import {BillTypeConfirm} from "@/pages/bill/components/bill_type_confirm.tsx";
import {BillTypeConfirmModal} from "@/pages/bill/components/bill_type_confirm.tsx";
import {saveAs} from "file-saver";
import {IconStudentId} from "@/components/icons";
import {BillDetailItem} from "@/components/bill/bill-detail-items.tsx";
import {NumberConfirm} from "@/pages/bill/components/number_confirm.tsx";
import {AddBillModal} from "@/pages/bill/components/add_bill_modal.tsx";
const DownloadButton = ({bill, text}: { bill: BillModel; text: string }) => {
@ -116,12 +113,17 @@ const BillQuery = () => {
console.log(selectKeys)
}
const onBillConfirm = (reload?: boolean) => {
setState({confirmBill: undefined})
if (reload) refresh()
}
return (<div>
<SearchForm showApply loading={loading} onSearch={setBillQueryParams}/>
<BillList
type={'query'} loading={loading} source={data}
operationRender={operation} operationRenderWidth={180}
beforeTotalAmount={<ButtonGroup style={{marginRight: 20}} theme={'solid'}>
beforeTotalAmount={<Space>
{
(selectKeys.length == 0) ? <Button disabled>{t('bill.confirm_bill_type_batch')}</Button> :
<Popconfirm
@ -132,9 +134,12 @@ const BillQuery = () => {
<Button theme={'solid'}>{t('bill.confirm_bill_type_batch')}</Button>
</Popconfirm>
}
<Button onClick={onImportExcel}>{t('bill.import_excel')}</Button>
<Button loading={state.exporting} onClick={onExportExcel}>{t('bill.export_excel')}</Button>
</ButtonGroup>}
<AddBillModal onConfirm={refresh}/>
<ButtonGroup style={{marginRight: 20}} theme={'solid'}>
<Button onClick={onImportExcel}>{t('bill.import_excel')}</Button>
<Button loading={state.exporting} onClick={onExportExcel}>{t('bill.export_excel')}</Button>
</ButtonGroup>
</Space>}
onRowSelection={setSelectedKeys}
onPageChange={(page_number) => {
@ -164,42 +169,7 @@ const BillQuery = () => {
refresh()
}}
/>
<Modal
title={t('bill.confirm_bill')}
visible={!!state.confirmBill}
closeOnEsc={true}
onCancel={() => {
refresh()
setState({confirmBill: undefined})
}}
width={550}
footer={null}
>
{state.confirmBill && <>
<div><BillDetailItems bill={state.confirmBill} studentNumberRender={<>
<BillDetailItem
icon={<IconStudentId/>} title={t('manual.student_number')}
value={state.confirmBill.student_number || '-'}/>
<BillDetailItem
icon={<IconStudentId/>} title={t('base.bill_number')}
value={state.confirmBill.application_number || '-'}/>
</>}/></div>
<div className="confirm-container" style={{padding: '15px 0',marginTop:20}}>
{
!state.confirmBill.student_number_confirm &&
<NumberConfirm bill={state.confirmBill} type={'student_number'}/>
}
{
!state.confirmBill.application_number_confirm &&
<NumberConfirm bill={state.confirmBill} type={'application_number'}/>
}
<Divider>Bill Type Confirm</Divider>
{
state.confirmBill.details.map((it, idx) => (<BillTypeConfirm data={it} key={idx}/>))
}
</div>
</>}
</Modal>
{state.confirmBill && <BillTypeConfirmModal onClose={onBillConfirm} bill={state.confirmBill}/>}
</div>)
}
export default BillQuery

View File

@ -3,7 +3,7 @@ import {useTranslation} from "react-i18next";
import {useRef, useState} from "react";
import {Card} from "@/components/card";
import {BillTypes} from "@/service/bill-types.ts";
import {useBillTypes} from "@/hooks/useBillTypes.ts";
import {BillDetailItems, useBillQRCode} from "@/components/bill";
import styles from './manual.module.less'
@ -43,6 +43,7 @@ export default function Index() {
// useEffect(()=>{
// getBillDetail(100009).then(setBillInfo);
// },[])
const BillTypes = useBillTypes()
const BillInfo = ({bill}: { bill?: BillModel }) => {
if (!bill) return null;

View File

@ -3,7 +3,7 @@ import {useMemo} from "react";
import {useTranslation} from "react-i18next";
import useAuth from "@/hooks/useAuth.ts";
import {IconQRCode, IconQuery, IconReconciliation} from "@/components/logo";
import {IconPermission, IconQRCode, IconQuery, IconReconciliation} from "@/components/logo";
export const AllDashboardMenu = [
{
@ -22,6 +22,12 @@ export const AllDashboardMenu = [
icon: <IconReconciliation/>,
path: '/dashboard/reconciliation',
role: ['root', 'fo']
},
{
key: 'permission',
icon: <IconPermission/>,
path: '/dashboard/permission',
role: ['root']
}
]

View File

@ -48,6 +48,10 @@ export function createManualBill(params: ManualCreateBillParam) {
return post<BillModel>('/manual_payment', params)
}
export function selectBillTypeList(){
return get<BillType[]>('/billing_types')
}
// 获取账单详情
export function getBillDetail(id: number) {
return get<BillModel>('/bills/' + id)
@ -66,8 +70,8 @@ export function modifyBillStatus(id: number,status: BillStatus) {
return put(`/bills/${id}/cancel`,{status})
}
export function confirmBillType({id,type}: {id:number,type: string}) {
return post<BillModel>(`/bill/detail/${id}/confirm`, {confirm_type:type})
export function confirmBillType(bills:BillConfirmParams[]) {
return post<BillModel>(`/bills/confirm`, {bills})
}
export function confirmBills(bill_ids: number[]) {

View File

@ -61,7 +61,7 @@ export function GeneratePdf(bill: BillModel) {
...(bill.details.map(it=>{
return [
`#${it.id}`,
dayjs(bill.paid_at).format('YYYY-MM-DD'),
bill.paid_at?dayjs(bill.paid_at).format('YYYY-MM-DD'):'',
it.bill_type,
`${bill.payment_channel}` + (bill.payment_method && bill.payment_channel != bill.payment_method ? `(${bill.payment_method})` : ''),
`${it.amount}`

23
src/types/bill.d.ts vendored
View File

@ -13,8 +13,14 @@ declare type ManualCreateBillParam = {
declare type BillDetail = {
id: number;
bill_type: string;
confirm_status: ConfirmStatus;
confirm_type: string;
amount: decimal;
confirmed?: ConfirmedBillDetail[];
}
declare type ConfirmedBillDetail = {
id?: number;
bill_detail_id: number;
bill_type: string;
amount: decimal;
}
/**
@ -38,6 +44,10 @@ declare type BillQueryParam = {
sort_field:string;
sort_order:SortOrderType;
}
declare type BillType = {
type: string;
description: string;
}
/**
*
*/
@ -46,7 +56,7 @@ declare type BillModel = {
student_number: string;
student_number_confirm?: string;
application_number: null | string;
application_number_confirm?: null | string;
confirm_application_number?: null | string;
student_email: string;
student_tc_name?: string;
student_sc_name?: string;
@ -80,6 +90,7 @@ declare type BillModel = {
remark: string;
confirm_status: ConfirmStatus;
details: BillDetail[]
detail_confirms: ConfirmedBillDetail[] | null
}
@ -114,4 +125,10 @@ type BillUpdateParams = {
type BillTypeConfirm = {
bill_type: string;
amount: number;
}
type BillConfirmParams = {
id:number;
confirm_application_number:string;
confirm_student_number:string;
detail_confirms:ConfirmedBillDetail[]
}

View File

@ -28,8 +28,8 @@ export default defineConfig(({mode}) => {
port:10086,
proxy: {
'/api': {
// target: 'https://test-payment-be.hkchc.team', //
target: 'http://127.0.0.1:50000', //
target: 'https://test-payment-be.hkchc.team', //
// target: 'http://127.0.0.1:50000', //
changeOrigin: true,
//rewrite: (path) => path.replace(/^\/api/, '')
}