feature/bill-split #1
@ -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,
|
||||
|
@ -1,296 +1,370 @@
|
||||
import {Space, Table, Typography} from "@douyinfe/semi-ui";
|
||||
import {Button, Checkbox, CheckboxGroup, Space, Table, Typography} from "@douyinfe/semi-ui";
|
||||
import {ColumnProps} from "@douyinfe/semi-ui/lib/es/table";
|
||||
import React, {useMemo, useState} from "react";
|
||||
import React, {ReactNode, useMemo, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import dayjs from "dayjs";
|
||||
import {IconCheckCircleStroked} from "@douyinfe/semi-icons";
|
||||
import {IconCheckCircleStroked, IconSetting} from "@douyinfe/semi-icons";
|
||||
|
||||
import MoneyFormat from "@/components/money-format.tsx";
|
||||
import {Card} from "@/components/card";
|
||||
import './bill.less'
|
||||
import {BillStatus} from "@/service/types.ts";
|
||||
import {useSetState} from "ahooks";
|
||||
import {clone} from "lodash";
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
const CheckNumberCorrect = ({origin,confirmed}:{origin: string,confirmed?:string}) => {
|
||||
if(origin == confirmed && origin){
|
||||
return (<Space style={{marginTop:2,color:'green'}}><span>{origin}</span><IconCheckCircleStroked /></Space>)
|
||||
}
|
||||
return <div style={{lineHeight:1}}>
|
||||
<div style={confirmed?{color:'red'}:{}}>{origin?.length ?origin: 'N/A'}</div>
|
||||
{confirmed&&<Space style={{marginTop:2,color:'green'}}><span>{confirmed}</span><IconCheckCircleStroked /></Space>}
|
||||
</div>
|
||||
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>)
|
||||
}
|
||||
return <div style={{lineHeight: 1}}>
|
||||
<div style={confirmed ? {color: 'red'} : {}}>{origin?.length ? origin : 'N/A'}</div>
|
||||
{confirmed &&
|
||||
<Space style={{marginTop: 2, color: 'green'}}><span>{confirmed}</span><IconCheckCircleStroked/></Space>}
|
||||
</div>
|
||||
}
|
||||
|
||||
export const BillList: React.FC<BillListProps> = (props) => {
|
||||
const {t, i18n} = useTranslation()
|
||||
const [currentTotalAmount,setCurrentTotalAmount] = useState(0)
|
||||
const {t, i18n} = useTranslation()
|
||||
const [currentTotalAmount, setCurrentTotalAmount] = useState(0)
|
||||
const [state, setState] = useSetState<{
|
||||
showColumnsConfig?: boolean;
|
||||
showCols: string[]
|
||||
}>({
|
||||
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_confirms", "amount", "pay_amount", "actual_payment_amount", "pay_method", "status", "apply_status"
|
||||
]
|
||||
})
|
||||
|
||||
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 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 allCols = 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:string) => (value|| 'N/A')
|
||||
},
|
||||
{
|
||||
title: t('base.bill_number'),
|
||||
dataIndex: 'application_number',
|
||||
width: 150,
|
||||
render: (value, record) => (
|
||||
<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>
|
||||
</div>,
|
||||
dataIndex: 'initiated_paid_at',
|
||||
width: 180,
|
||||
render: (value) => value?.length ? value : 'N/A'
|
||||
},
|
||||
{
|
||||
title: <div className="table-header-title">{t('bill.title_delivered_at')}
|
||||
<div className="tips">(PPS Statement Date)</div>
|
||||
</div>,
|
||||
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: '',
|
||||
render: (_, record) => (i18n.language == 'en-US' ? record.department_english_name : record.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,
|
||||
wordBreak: 'break-all',
|
||||
maxWidth: '100%',
|
||||
whiteSpace: 'normal'
|
||||
}}>
|
||||
{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_confirms',
|
||||
ellipsis: {showTitle: true},
|
||||
width: 220,
|
||||
render: (_, record) => (<div style={{
|
||||
fontSize: 13,
|
||||
lineHeight: 1.2,
|
||||
wordBreak: 'break-all',
|
||||
maxWidth: '100%',
|
||||
whiteSpace: 'normal'
|
||||
}}>
|
||||
{record.detail_confirms?.map((it) => (
|
||||
<div key={it.id}>{it.bill_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) => {
|
||||
|
||||
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,record) => (<CheckNumberCorrect origin={value} confirmed={record.student_number_confirm || 'test-confirm'} />)
|
||||
},
|
||||
{
|
||||
title: t('base.bill_number'),
|
||||
dataIndex: 'application_number',
|
||||
width: 150,
|
||||
render: (value,record) => (<CheckNumberCorrect origin={value} confirmed={value||record.application_number_confirm} />)
|
||||
},
|
||||
{
|
||||
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,wordBreak:'break-all',maxWidth:'100%',whiteSpace:'normal'}}>
|
||||
{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,wordBreak:'break-all',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>))}
|
||||
</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>
|
||||
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]);
|
||||
</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),
|
||||
})
|
||||
}
|
||||
return cols;
|
||||
}, [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])
|
||||
const columns = useMemo<ColumnProps<BillModel>[]>(() => {
|
||||
const _cols = clone(allCols);
|
||||
const cols = state.showCols.length == 0 ? _cols : _cols.filter((it: ColumnProps<BillModel>) => !it.dataIndex || state.showCols.includes(it.dataIndex))
|
||||
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, allCols, state.showCols]);
|
||||
|
||||
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>
|
||||
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={<Space>
|
||||
<span>{t('bill.title_bill_list')}</span>
|
||||
<span className={'cursor-pointer'} onClick={() => setState({showColumnsConfig: true})}>
|
||||
<IconSetting size={'small'}/>
|
||||
</span>
|
||||
</Space>}
|
||||
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>}
|
||||
>
|
||||
{state.showColumnsConfig && <div style={{
|
||||
marginBottom: 20,
|
||||
padding: 20,
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: 5,
|
||||
border: 'solid 1px #f0f0f0'
|
||||
}}>
|
||||
<div className="table-column-config">
|
||||
<CheckboxGroup direction="horizontal" defaultValue={state.showCols}
|
||||
onChange={showCols => setState({showCols})}>
|
||||
{allCols.map((it) => {
|
||||
return (<Checkbox value={it.dataIndex}><span>{it.title as ReactNode}</span></Checkbox>)
|
||||
})}
|
||||
</CheckboxGroup>
|
||||
</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>
|
||||
<div className="table-column-action" style={{marginTop: 20}}>
|
||||
<Space>
|
||||
<Button onClick={()=>setState({showColumnsConfig:false})}>{t('base.close')}</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
<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>
|
||||
}
|
@ -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 = {}
|
||||
|
||||
|
@ -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
26
src/hooks/useBillTypes.ts
Normal 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
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"base": {
|
||||
"add": "Add",
|
||||
"bill_number": "Bill Number",
|
||||
"btn_search_submit": "Search",
|
||||
"cancel": "Cancel",
|
||||
@ -12,6 +13,8 @@
|
||||
"please_select": "Please Select",
|
||||
"qr-code": "QRCode",
|
||||
"query_bill": "Failed to query bill:",
|
||||
"remove": "Remove",
|
||||
"save": "Save",
|
||||
"student_number": "Student Number"
|
||||
},
|
||||
"bill": {
|
||||
@ -22,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",
|
||||
@ -35,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",
|
||||
@ -90,7 +95,8 @@
|
||||
"menu": {
|
||||
"bill": "Bill Query",
|
||||
"check": "Reconciliation",
|
||||
"manual": "Manual Pay"
|
||||
"manual": "Manual Pay",
|
||||
"permission": "Permission"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"base": {
|
||||
"add": "增加",
|
||||
"bill_number": "账单编号",
|
||||
"btn_search_submit": "搜索",
|
||||
"cancel": "取消",
|
||||
@ -12,6 +13,8 @@
|
||||
"please_select": "请选择",
|
||||
"qr-code": "二维码",
|
||||
"query_bill": "查询账单失败:",
|
||||
"remove": "删除",
|
||||
"save": "保存",
|
||||
"student_number": "学号"
|
||||
},
|
||||
"bill": {
|
||||
@ -22,6 +25,7 @@
|
||||
"cancel_confirm_bills": "确认对账选中账单?",
|
||||
"cancel_success": "作废账单成功",
|
||||
"confirm": "对账",
|
||||
"confirm_amount_exceed_content": "金额超出总金额",
|
||||
"confirm_batch": "批量对账",
|
||||
"confirm_bill": "确认账单信息",
|
||||
"confirm_bill_number": "确认账单编号",
|
||||
@ -35,6 +39,7 @@
|
||||
"download-qr-code": "下载二维码",
|
||||
"download_receipt": "下载收据",
|
||||
"export_excel": "导出账单",
|
||||
"import_bill": "添加账单",
|
||||
"import_excel": "导入账单",
|
||||
"paid": "已支付",
|
||||
"paid_confirm": "是否将此订单状态设为已支付",
|
||||
@ -62,9 +67,9 @@
|
||||
"title_bill_type_confirm": "确认账单",
|
||||
"title_confirm_status": "确认状态",
|
||||
"title_create_at": "创建时间",
|
||||
"title_delivered_at": "到账时间",
|
||||
"title_delivered_at": "交付学院时间",
|
||||
"title_department": "学系",
|
||||
"title_initiated_paid_at": "开始支付时间",
|
||||
"title_initiated_paid_at": "渠道支付时间",
|
||||
"title_operate": "操作",
|
||||
"title_paid_at": "支付时间",
|
||||
"title_pay_amount": "应付金额",
|
||||
@ -90,7 +95,8 @@
|
||||
"menu": {
|
||||
"bill": "账单查询",
|
||||
"check": "对账",
|
||||
"manual": "现场支付"
|
||||
"manual": "现场支付",
|
||||
"permission": "权限管理"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"base": {
|
||||
"add": "增加",
|
||||
"bill_number": "帳單編號",
|
||||
"btn_search_submit": "搜尋",
|
||||
"cancel": "取消",
|
||||
@ -12,6 +13,8 @@
|
||||
"please_select": "請選擇",
|
||||
"qr-code": "QRCode",
|
||||
"query_bill": "查詢帳單失敗:",
|
||||
"remove": "刪除",
|
||||
"save": "儲存",
|
||||
"student_number": "學號"
|
||||
},
|
||||
"bill": {
|
||||
@ -22,6 +25,7 @@
|
||||
"cancel_confirm_bills": "確認對帳選取帳單?",
|
||||
"cancel_success": "作廢帳單成功",
|
||||
"confirm": "對帳",
|
||||
"confirm_amount_exceed_content": "金額超出總金額",
|
||||
"confirm_batch": "批次對帳",
|
||||
"confirm_bill": "確認帳單資訊",
|
||||
"confirm_bill_number": "確認帳單編號",
|
||||
@ -35,6 +39,7 @@
|
||||
"download-qr-code": "下載二維碼",
|
||||
"download_receipt": "下載收據",
|
||||
"export_excel": "導出賬單",
|
||||
"import_bill": "新增帳單",
|
||||
"import_excel": "導入賬單",
|
||||
"paid": "已支付",
|
||||
"paid_confirm": "是否將此訂單狀態設為已支付",
|
||||
@ -64,7 +69,7 @@
|
||||
"title_create_at": "創建時間",
|
||||
"title_delivered_at": "到帳時間",
|
||||
"title_department": "學系",
|
||||
"title_initiated_paid_at": "開始支付時間",
|
||||
"title_initiated_paid_at": "渠道支付時間",
|
||||
"title_operate": "操作",
|
||||
"title_paid_at": "付款時間",
|
||||
"title_pay_amount": "應付金額",
|
||||
@ -90,7 +95,8 @@
|
||||
"menu": {
|
||||
"bill": "帳單查詢",
|
||||
"check": "對帳",
|
||||
"manual": "現場支付"
|
||||
"manual": "現場支付",
|
||||
"permission": "權限管理"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
|
88
src/pages/auth/permission.tsx
Normal file
88
src/pages/auth/permission.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import {Card} from "@/components/card";
|
||||
import {useSetState} from "ahooks";
|
||||
import {Button, Space, Spin, TagInput, Toast} from "@douyinfe/semi-ui";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useEffect} from "react";
|
||||
import {getPermissionList, savePermissionList} from "@/service/api/user.ts";
|
||||
|
||||
const DEFAULT_ROLES = [
|
||||
'root','ro','fo'
|
||||
]
|
||||
|
||||
const Permission = ()=>{
|
||||
const {t} = useTranslation()
|
||||
const [state,setState] = useSetState<{
|
||||
list:PermissionUserList[];
|
||||
loading?:boolean;
|
||||
}>({
|
||||
list:[]
|
||||
})
|
||||
const onUsernameChange = (role_name:string,username_list: string[])=>{
|
||||
const index = state.list.findIndex(it=>it.role_name == role_name)
|
||||
if(index != -1){
|
||||
state.list[index] = {
|
||||
role_name,username_list
|
||||
};
|
||||
setState({
|
||||
list:state.list
|
||||
})
|
||||
}
|
||||
}
|
||||
const saveRoles = ()=>{
|
||||
setState({
|
||||
loading:true
|
||||
})
|
||||
savePermissionList(state.list).then(()=>{
|
||||
Toast.success({content:`Save Success`,duration: 3,})
|
||||
}).catch(e=>{
|
||||
Toast.error({
|
||||
content:`Save Error:${e.message}`,
|
||||
duration: 3,
|
||||
})
|
||||
}).finally(()=>{
|
||||
setState({loading:false})
|
||||
})
|
||||
}
|
||||
const loadAllPermission = ()=>{
|
||||
setState({
|
||||
loading:true
|
||||
})
|
||||
getPermissionList().then(list=>{
|
||||
const roles:{
|
||||
[key:string]:string[]
|
||||
} = {}
|
||||
// array to object
|
||||
list.forEach(it=>{
|
||||
roles[it.role_name] = it.username_list
|
||||
})
|
||||
const permissionList:PermissionUserList[] = [];
|
||||
DEFAULT_ROLES.forEach(role_name=>{
|
||||
permissionList.push({
|
||||
role_name,username_list: roles[role_name]
|
||||
})
|
||||
})
|
||||
setState({list:permissionList})
|
||||
}).finally(()=>{
|
||||
setState({loading:false})
|
||||
})
|
||||
}
|
||||
useEffect(loadAllPermission,[])
|
||||
|
||||
return (<Card style={{marginBottom: 20}}>
|
||||
{state.list.map(it=>(<div key={it.role_name} style={{marginBottom:20}}>
|
||||
<div className="permission-title" style={{marginBottom:5}}>{it.role_name.toUpperCase()}</div>
|
||||
<TagInput
|
||||
defaultValue={it.username_list}
|
||||
size={'large'}
|
||||
addOnBlur={true} allowDuplicates={false}
|
||||
placeholder={t('base.please_enter')}
|
||||
onChange={users => onUsernameChange(it.role_name,users)}
|
||||
/>
|
||||
</div>))}
|
||||
<Space>
|
||||
<Button loading={state.loading} onClick={saveRoles} theme={'solid'}>{state.loading ? 'Loading' :t('base.save')}</Button>
|
||||
<div>{state.message||''}</div>
|
||||
</Space>
|
||||
</Card>)
|
||||
}
|
||||
export default Permission
|
137
src/pages/bill/components/add_bill_modal.tsx
Normal file
137
src/pages/bill/components/add_bill_modal.tsx
Normal 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>
|
||||
</>)
|
||||
}
|
@ -1,57 +1,216 @@
|
||||
import {Button, Select, Popconfirm, Space, Tag} 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;
|
||||
}
|
||||
|
||||
const BillTypeConfirmItem = (props: { data: BillDetail; onChange: (confirms: ConfirmedBillDetail[]) => void; }) => {
|
||||
const it = props.data;
|
||||
const BillTypes = useBillTypes()
|
||||
const {t} = useTranslation()
|
||||
|
||||
const [state, setState] = useSetState<{
|
||||
billTypeList: ConfirmedBillDetail[];
|
||||
loading?: boolean,
|
||||
confirmed?: boolean,
|
||||
}>({
|
||||
loading: false,
|
||||
billTypeList: [
|
||||
{bill_type: props.data.bill_type, bill_detail_id: it.id, amount: Number(props.data.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 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={{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>
|
||||
|
||||
<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 onClick={onAdd}>{t('base.add')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Divider margin='12px'/>
|
||||
</>)
|
||||
}
|
||||
|
||||
export const BillTypeConfirm: React.FC<BillTypeConfirmProps> = (props) => {
|
||||
const [it,setItem] = useState(props.data)
|
||||
const {t} = useTranslation()
|
||||
const [state,setState] = useSetState({
|
||||
loading:false,
|
||||
bill_type: props.data.bill_type
|
||||
const [state, setState] = useSetState<{
|
||||
confirm_application_number: string;
|
||||
confirmed: {
|
||||
[key: number]: ConfirmedBillDetail[]
|
||||
}
|
||||
}>({
|
||||
confirm_application_number: '', confirmed: {}
|
||||
})
|
||||
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 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 <div className="confirm-item align-center space-between"
|
||||
style={{marginBottom: 20}}>
|
||||
<div style={{lineHeight:1.1}}>
|
||||
<div>{it.bill_type}</div>
|
||||
<div style={{fontSize:12}}>
|
||||
<MoneyFormat money={it.amount}/>
|
||||
</div>
|
||||
|
||||
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-item-btn">
|
||||
<Space spacing={15}>
|
||||
{it.confirm_status != 'CONFIRMED' && <Select onChange={v=>setState({bill_type:String(v)})} defaultValue={it.bill_type} style={{width:180}} placeholder={t('manual.bill_type')}>
|
||||
{
|
||||
BillTypes.map((it, idx) => (
|
||||
<Select.Option key={idx} value={it.label}>{it.label}</Select.Option>))
|
||||
}
|
||||
</Select>}
|
||||
{
|
||||
it.confirm_status == 'CONFIRMED' ? <Tag size={'large'} color='light-blue'>{state.bill_type}</Tag> : <>
|
||||
<Popconfirm
|
||||
title={'Notice'} onConfirm={() => onConfirmBill()}
|
||||
position={'topRight'}
|
||||
content={`${t('bill.confirm_bill_type')}?`}
|
||||
><Button style={{width:80}} loading={state.loading} theme={'solid'}>{t('base.confirm')}</Button></Popconfirm>
|
||||
</>
|
||||
}
|
||||
</Space>
|
||||
<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>
|
||||
</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>)
|
||||
}
|
@ -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>
|
||||
|
@ -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,44 +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'}/>
|
||||
}
|
||||
{
|
||||
state.confirmBill.details.map((it, idx) => (<div key={idx}>
|
||||
<Divider margin='12px'/>
|
||||
<BillTypeConfirm data={it}/>
|
||||
</div>))
|
||||
}
|
||||
</div>
|
||||
</>}
|
||||
</Modal>
|
||||
{state.confirmBill && <BillTypeConfirmModal onClose={onBillConfirm} bill={state.confirmBill}/>}
|
||||
</div>)
|
||||
}
|
||||
export default BillQuery
|
@ -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;
|
||||
|
@ -21,6 +21,7 @@ import ManualIndex from "@/pages/manual/index.tsx";
|
||||
import BillQuery from "@/pages/bill/query.tsx";
|
||||
import BillReconciliation from "@/pages/bill/reconciliation.tsx";
|
||||
import ExternalCreate from "@/pages/bill/external_create.tsx";
|
||||
import Permission from "@/pages/auth/permission.tsx";
|
||||
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
@ -76,6 +77,10 @@ const routes: RouteObject[] = [
|
||||
path: 'reconciliation',
|
||||
element: <BillReconciliation/>
|
||||
},
|
||||
{
|
||||
path: 'permission',
|
||||
element: <Permission/>
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
@ -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']
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -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[]) {
|
||||
|
@ -11,4 +11,11 @@ export function getUserInfo() {
|
||||
*/
|
||||
export function auth(code:string,state:string){
|
||||
return post<UserProfile>('/auth', {code, state})
|
||||
}
|
||||
|
||||
export function getPermissionList(){
|
||||
return get<PermissionUserList[]>('/roles')
|
||||
}
|
||||
export function savePermissionList(roles:PermissionUserList[]){
|
||||
return post('/roles',{roles})
|
||||
}
|
@ -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}`
|
||||
|
7
src/types/auth.d.ts
vendored
7
src/types/auth.d.ts
vendored
@ -31,4 +31,9 @@ declare type AuthContextType = {
|
||||
mockLogin: () => Promise<void>;
|
||||
login: (code:string,state:string) => Promise<void>;
|
||||
updateUser: (user:Partial<UserProfile>) => Promise<void>;
|
||||
};
|
||||
};
|
||||
|
||||
declare type PermissionUserList = {
|
||||
role_name:string;
|
||||
username_list: string[];
|
||||
}
|
28
src/types/bill.d.ts
vendored
28
src/types/bill.d.ts
vendored
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -109,4 +120,15 @@ type BillUpdateParams = {
|
||||
remark?: string;
|
||||
merchant_ref?: string;
|
||||
payment_amount?: number | string;
|
||||
}
|
||||
|
||||
type BillTypeConfirm = {
|
||||
bill_type: string;
|
||||
amount: number;
|
||||
}
|
||||
type BillConfirmParams = {
|
||||
id:number;
|
||||
confirm_application_number:string;
|
||||
confirm_student_number:string;
|
||||
detail_confirms:ConfirmedBillDetail[]
|
||||
}
|
@ -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/, '')
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user