285 lines
11 KiB
TypeScript

import {Space, Table, Typography} from "@douyinfe/semi-ui";
import {ColumnProps} from "@douyinfe/semi-ui/lib/es/table";
import React, {useMemo, useState} from "react";
import {useTranslation} from "react-i18next";
import dayjs from "dayjs";
import MoneyFormat from "@/components/money-format.tsx";
import {Card} from "@/components/card";
import './bill.less'
import {BillStatus} from "@/service/types.ts";
type BillListProps = {
type: 'query' | 'reconciliation';
operationRender?: (record: BillModel) => React.ReactNode;
operationRenderWidth?: number;
onRowSelection?: (selectedRowKeys: (string | number)[]) => void;
source?: RecordList<BillModel>;
onPageChange: (pageIndex:number) => void;
tableFooter?: React.ReactNode;
loading?: boolean;
beforeTotalAmount?: React.ReactNode;
}
export const BillList: React.FC<BillListProps> = (props) => {
const {t, i18n} = useTranslation()
const [currentTotalAmount,setCurrentTotalAmount] = useState(0)
const billStatusText = (billStatus: string) => {
switch (billStatus) {
case 'PENDING':
return t('bill.pay_status_pending')
case 'PAID':
return t('bill.pay_status_paid')
case 'EXPIRED':
return t('bill.pay_status_expired')
case 'CANCELED':
case 'CANCELLED':
return t('bill.pay_status_canceled')
default:
return billStatus
}
}
const applyStatusText = (status:string) => {
switch (status) {
case 'UNCHECKED':
return t('bill.reconciliation_status_pending')
case 'CHECKED':
return t('bill.reconciliation_status_submitted')
default:
return status
}
}
const columns = useMemo<ColumnProps<BillModel>[]>(() => {
const cols: ColumnProps<BillModel>[] = [
{
title: '#ID',
dataIndex: 'id',
width: 120,
},
{
title: 'Merchant Ref',
dataIndex: 'merchant_ref',
width: 200,
// render: (_) => (<MoneyFormat money={_}/>),
},
{
title: t('base.student_number'),
dataIndex: 'student_number',
width: 150,
render: (value) => value?.length ?value: 'N/A'
},
{
title: t('base.bill_number'),
dataIndex: 'application_number',
width: 150,
},
{
title: t('bill.title_initiated_paid_at'),
dataIndex: 'initiated_paid_at',
width: 180,
render: (value) => value?.length ?value: 'N/A'
},
{
title: t('bill.title_delivered_at'),
dataIndex: 'delivered_at',
width: 180,
render: (value) => value?.length ?value: 'N/A'
},
{
title: t('bill.title_paid_at'),
dataIndex: 'paid_at',
width: 180,
render: (value) => value?.length ?value: 'N/A'
},
{
title: t('bill.title_create_at'),
dataIndex: 'create_at',
width: 180,
render: (value) => value?.length ?value: 'N/A'
},
{
title: t('bill.title_student_name'),
dataIndex: 'student_english_name',
width: 180,
render: (_, record) => (<div>{record.student_english_name}<br/>{record.student_chinese_name}</div>)
},
{
title: 'Email',
dataIndex: 'student_email',
width: 200,
render: (value) => value?.length ?value: 'N/A'
// render: (_, record) => (<div>{record.student_english_name}<br/>{record.student_chinese_name}</div>)
},
{
title: t('bill.title_program_name'),
width: 250,
dataIndex: i18n.language == 'en-US' ? 'programme_english_name' : 'programme_chinese_name',
},
{
title: t('bill.title_department'),
width: 200,
dataIndex: i18n.language == 'en-US' ? 'department_english_name' : 'department_chinese_name',
},
{
title: t('bill.title_year'),
dataIndex: 'intake_year',
width: 120,
render: (_, record) => record.intake_year?(<div>{record.intake_year}/{String(record.intake_semester).length == 1 ? '0':''}{record.intake_semester}</div>):"N/A"
},
// {
// title: t('bill.title_semester'),
// dataIndex: 'intake_semester',
// width: 120,
// },
{
title: t('bill.title_bill_detail'),
dataIndex: 'detail',
ellipsis: {showTitle: true},
width: 220,
render: (_, record) => (<div style={{fontSize: 13, lineHeight: 1.2}}>
{record.details.map((it, idx) => (<div key={idx}>{it.bill_type}: <MoneyFormat money={it.amount}/></div>))}
</div>),
},
{
title: t('bill.title_bill_type_confirm'),
dataIndex: '_detail',
ellipsis: {showTitle: true},
width: 220,
render: (_, record) => (<div style={{fontSize: 13, lineHeight: 1.2}}>
{record.details.filter(s=>s.confirm_status == 'CONFIRMED').map((it) => (<div key={it.id}>{it.confirm_type}: <MoneyFormat money={it.amount}/></div>))}
</div>),
},
{
title: t('bill.title_amount'),
dataIndex: 'amount',
width: 150,
render: (_) => (<MoneyFormat money={_}/>),
},
{
title: t('bill.title_pay_amount'),
dataIndex: 'pay_amount',
width: 190,
render: (_, record) => {
if (record.service_charge && record.service_charge > 0) {
return <div>
<MoneyFormat money={record.payment_amount}/><br/>
<Typography.Text type={'quaternary'} size={'small'} style={{position: "absolute"}}>
{t('bill.title_service_charge')}: <MoneyFormat money={record.service_charge}/>
</Typography.Text>
</div>
}
return (<div><MoneyFormat money={record.payment_amount}/></div>)
},
},
{
title: t('bill.title_actual_payment_amount'),
dataIndex: 'actual_payment_amount',
width: 150,
render: (_,record) => (<MoneyFormat money={_} currency={record.currency}/>),
},
{
title: t('bill.title_pay_method'),
dataIndex: 'pay_method',
width: 130,
render: (_, {payment_method, payment_channel}) => (payment_channel?(
<div>
{payment_channel}
{payment_method && payment_method.length > 0 && <div>
<Typography.Text type={'quaternary'} size={'small'} style={{position: "absolute"}}>
({payment_method})
</Typography.Text>
</div>}
</div>
):'N/A'),
},
{
title: t('bill.title_bill_status'),
dataIndex: 'status',
width: 150,
render: value => billStatusText(value),
},
]
if (props.type != 'reconciliation') {
cols.push({
title: t('bill.title_reconciliation_status'),
dataIndex: 'apply_status',
width: 150,
render: value => applyStatusText(value),
})
}
if (props.operationRender) {
cols.push({
title: t('bill.title_operate'),
dataIndex: 'operate',
fixed: 'right',
width: props.operationRenderWidth || (props.type == 'reconciliation'?120:220),
render: (_, record) => props.operationRender?.(record),
})
}
return cols;
}, [props.operationRender, props.type, i18n.language]);
const isExpired = (bill: BillModel) => {
return bill.status == BillStatus.PENDING && dayjs(bill.expiration_time).isBefore(Date.now())
}
const currentList = useMemo(()=>{
const originList = props.source?.list || [];
originList.forEach(s => {
if(isExpired(s)){
s.status = BillStatus.EXPIRED;
}
})
const _total = originList.map(s=>Number(s.amount)).reduce((s, c) => (s + c), 0)
setCurrentTotalAmount(_total)
return originList;
},[props.source])
return <Card
title={t('bill.title_bill_list')}
headerRight={<Space spacing={20}>
{props.beforeTotalAmount}
<div className="bill-info">
<div className="bill-info-item">
<span className="bill-info-title">{t('bill.query_amount_total')} :</span>
<MoneyFormat money={props.source?.pagination.recordTotal || 0}/>
</div>
<div className="bill-info-item">
<span className="bill-info-title current-amount">{t('bill.query_amount_current_page')} :</span>
<MoneyFormat money={currentTotalAmount || 0}/>
</div>
</div>
</Space>}
>
<div className="bill-list-table">
<Table<BillModel>
bordered
columns={columns}
dataSource={currentList}
rowKey={'id'}
pagination={{
currentPage: props.source?.pagination.current,
pageSize: props.source?.pagination.pageSize,
total: props.source?.pagination.total,
onPageChange: props.onPageChange,
formatPageText: (params) => (
<div className="bill-list-pagination">
{props.tableFooter}
{props.source && props.source.pagination.recordTotal > 0 && <span>{t('page.record-show',params)}</span>}
</div>
)
}}
loading={props.loading}
rowSelection={props.onRowSelection ? {
fixed: true,
onChange: (selectedRowKeys) => {
selectedRowKeys && props.onRowSelection?.(selectedRowKeys)
}
} : undefined}
/>
</div>
</Card>
}