初步完成发票管理

main
wuyize 2023-01-02 22:49:16 +08:00
parent 3f64451add
commit 9079f39014
6 changed files with 471 additions and 64 deletions

View File

@ -12,6 +12,7 @@ export interface Staff {
staffDepartments: Department[];
staffName: string;
staffBase: string;
staffId: string|null|undefined;
}
export interface Department {

View File

@ -27,6 +27,7 @@ const tokenSlice = createSlice({
const staffSlice = createSlice({
name: 'staff',
initialState: {
staffId: "",
staffName: "",
managingDepartment: null as Department|null,
staffDepartments: [{ departmentId: 0,

View File

@ -6,12 +6,13 @@ import {
invoiceItemsMap, invoiceTypeExtraItemsMap,
invoiceTypeItemsMap,
invoiceTypeNameMap, invoiceTypeShowItemsMap
} from "../../../models/Invoice";
import axiosInstance from "../../../utils/axiosInstance";
} from "../../models/Invoice";
import axiosInstance from "../../utils/axiosInstance";
import TextArea from "antd/es/input/TextArea";
import dayjs, {Dayjs} from "dayjs";
import qs from "qs";
const {Text, Paragraph } = Typography;
const {Text, Paragraph} = Typography;
function InvoiceDetailModal(props: any) {
//const [open, setOpen] = useState(false)
@ -77,28 +78,47 @@ function InvoiceDetailModal(props: any) {
refreshFormItems(value)
}
const withDraw = () => {
setLoading(true)
axiosInstance({
url: 'common/invoice/' + props.invoiceDetail.invoiceId,
method: 'delete',
}).then(response => {
console.log(response.data)
setLoading(false)
props.onClose()
props.needRefresh()
}).catch(function (error) {
console.log(error)
setLoading(false)
})
}
return (
<Modal
open={props.open}
title="发票详情"
onCancel={handleCancel}
footer={null}
footer={props.showFooter ? <Button key="next" type="primary" danger loading={loading} onClick={withDraw}
>
</Button> : null}
>
{
<Form layout="horizontal" labelCol={{span: 4}}
<Form layout="horizontal" labelCol={{span: 4}}
>
<Form.Item
name={`invoiceKind`}
label={`发票类型`}
>
<Text>{invoiceTypeNameMap.get(props.invoiceDetail?.invoiceKind) }</Text>
<Text>{invoiceTypeNameMap.get(props.invoiceDetail?.invoiceKind)}</Text>
</Form.Item>
{formItems}
<Form.Item
name={`invoiceNote`}
label={`备注`}
>
<Paragraph style={{marginTop:5}} >{props.invoiceDetail?.invoiceNote }</Paragraph >
<Paragraph style={{marginTop: 5}}>{props.invoiceDetail?.invoiceNote}</Paragraph>
</Form.Item>
</Form>
}

View File

@ -1,6 +1,374 @@
function InvoiceManagement() {
return(
<div></div>
import {
Card,
Dropdown,
List,
Row,
Input,
Button,
Radio,
Col,
Divider,
DatePicker,
Form,
TimePicker,
Select,
Checkbox,
Pagination, theme, Tag, Badge
} from "antd";
import type {MenuProps, PaginationProps} from 'antd';
import {DownOutlined, UploadOutlined} from '@ant-design/icons';
import React, {
ReactElement,
JSXElementConstructor,
ReactFragment,
ReactPortal,
useState,
useEffect,
ReactNode
} from "react";
import {InvoiceDetail, InvoiceSearchOption} from "../../../models/Invoice"
import {Space, Typography} from 'antd';
import {SizeType} from "antd/es/config-provider/SizeContext";
import axios from "axios";
import axiosInstance, {baseUrl} from "../../../utils/axiosInstance";
import {Simulate} from "react-dom/test-utils";
import change = Simulate.change;
import {useNavigate, useSearchParams} from "react-router-dom";
import {useAppDispatch} from "../../../models/hooks";
import {
Invoice,
invoiceItemsMap,
invoiceTypeExtraItemsMap,
invoiceTypeItemsMap,
invoiceTypeNameMap
} from "../../../models/Invoice";
import {RadioButtonProps} from "antd/es/radio/radioButton";
import qs from "qs";
import InvoiceDetailModal from "../InvoiceDetailModal";
import dayjs from "dayjs";
const {Text, Paragraph} = Typography;
const {Meta} = Card;
const {Search} = Input;
const {Option} = Select
let invoices: Array<Invoice>
const {RangePicker} = DatePicker;
const config = {
rules: [{type: 'object' as const, required: false, message: 'Please select time!'}],
};
const rangeConfig = {
rules: [{type: 'array' as const, required: false, message: 'Please select time!'}],
};
function InvoiceSearch(props: { handleSearchData: any; }) {
const {
token: {colorBgContainer, colorPrimary},
} = theme.useToken();
const [activatedOption, setActivatedOption] = useState('发票代码')
const [complexEnabled, setComplexEnabled] = useState(false)
const [invoiceSearchOption, setInvoiceSearchOption] = useState(new InvoiceSearchOption())
const items: MenuProps['items'] = [
{
key: '1',
label: (<a className="simpleSearchOption" onClick={() => {
invoiceSearchOption.clear();
setActivatedOption("发票代码")
}}>
</a>)
},
{
key: '2',
label: (<a className="simpleSearchOption" onClick={() => {
invoiceSearchOption.clear();
setActivatedOption("发票编号")
}}>
</a>)
}
];
const onValuesChange = (changedValues: any, allValues: any) => {
invoiceSearchOption.clear()
console.log(allValues)
if (allValues['upload-time-picker'] !== null && allValues['upload-time-picker'] !== undefined) {
invoiceSearchOption.invoiceUploadTimeStart = allValues['upload-time-picker'][0].format('YYYY-MM-DDtHH:mm:ss')
invoiceSearchOption.invoiceUploadTimeEnd = allValues['upload-time-picker'][1].format('YYYY-MM-DDtHH:mm:ss')
}
if (allValues['invoice-time-picker'] !== null && allValues['invoice-time-picker'] !== undefined) {
invoiceSearchOption.invoiceDateStart = allValues['invoice-time-picker'][0].format('YYYY-MM-DD')
invoiceSearchOption.invoiceDateEnd = allValues['invoice-time-picker'][1].format('YYYY-MM-DD')
}
if (allValues['invoice-state'] !== "全部") {
invoiceSearchOption.invoiceStates = [allValues['invoice-state']]
}
if (allValues['invoice-kind'] !== "全部") {
invoiceSearchOption.invoiceKinds = [allValues['invoice-kind']]
}
if (allValues['invoice-uploader'] !== null && allValues['invoice-uploader'] !== undefined && allValues['invoice-uploader'].trim() !== "") {
invoiceSearchOption.invoiceUploaderId = allValues['invoice-uploader'].trim()
}
props.handleSearchData(invoiceSearchOption)
}
const onSearch = (value: string) => {
if (!complexEnabled)
invoiceSearchOption.clear()
console.log(invoiceSearchOption)
if (activatedOption === "发票代码") {
invoiceSearchOption.invoiceCode = value;
invoiceSearchOption.invoiceNo = null;
} else {
invoiceSearchOption.invoiceNo = value;
invoiceSearchOption.invoiceCode = null;
}
props.handleSearchData(invoiceSearchOption)
}
const getInvoiceKindsRadioButtons: any = () => {
let options: any[] = []
invoiceTypeNameMap.forEach(function (value, key, map) {
options.push(<Radio.Button style={{marginBottom: 10}} value={key}>{value}</Radio.Button>)
})
return options
}
return (
<div className="headBar">
<div className="simpleSearchBar" style={{
height: '72px',
padding: '30px',
display: "flex",
alignItems: 'center',
justifyContent: "space-between",
backgroundColor: colorBgContainer
}}>
<Search className="simpleSearch"
addonBefore={<Dropdown
menu={{
items,
selectable: true,
defaultSelectedKeys: ['1'],
}}
>
<Typography.Link>
<Space>
{activatedOption}
<DownOutlined/>
</Space>
</Typography.Link>
</Dropdown>}
placeholder={'请在此输入'}
allowClear
onSearch={(value) => onSearch(value)}
style={{width: 404}}
enterButton
/>
<Checkbox onChange={(e) => {
setComplexEnabled(e.target.checked)
}}></Checkbox>
</div>
{complexEnabled &&
<div className="complexSearchBar" style={{
margin: '20px',
padding: '20px 20px 0px 20px',
display: "flex",
backgroundColor: colorBgContainer,
borderRadius: '20px'
}}>
<Form name="complexOption" onValuesChange={onValuesChange} style={{width: '100%'}}>
<Form.Item
name={`invoice-kind`}
label={`发票类型`}
>
<Radio.Group defaultValue="全部">
<Radio.Button value="全部"></Radio.Button>
{getInvoiceKindsRadioButtons()}
</Radio.Group>
</Form.Item>
<div style={{marginTop: -10, display: "flex", flexDirection: "row", flexWrap: "wrap"}}>
<Form.Item name="upload-time-picker" label="上传时间" {...rangeConfig}
style={{marginRight: 100}}>
<RangePicker/>
</Form.Item>
<Form.Item name="invoice-time-picker" label="开票日期" {...rangeConfig}>
<RangePicker/>
</Form.Item>
</div>
<Form.Item
name={`invoice-state`}
label={`发票状态`}
>
<Radio.Group defaultValue="全部">
<Radio.Button value="全部"></Radio.Button>
<Radio.Button value="0">使</Radio.Button>
<Radio.Button value="1"></Radio.Button>
<Radio.Button value="2"></Radio.Button>
</Radio.Group>
</Form.Item>
</Form>
</div>}
</div>
)
}
function InvoiceItem(props: { invoice: Invoice, onClick(invoice: Invoice): void }) {
const {
token: {colorBgContainer, colorPrimary, colorSuccess, colorWarning},
} = theme.useToken();
const onClick = () => {
props.onClick(props.invoice)
}
return (
<div style={{
margin: 30
}}>
<Badge.Ribbon color={['#00000000', colorPrimary, colorSuccess][props.invoice.invoiceState]} text={['', '报销中', '已报销'][props.invoice.invoiceState]}>
<Card
onClick={onClick}
hoverable
style={{
width: 250,
height: 300,
}}
cover={
<img alt="thumbnail"
src={baseUrl + props.invoice.invoiceThumbnailUri}
width="220" height="180"/>
}
>
<div style={{marginTop: -20}}>
<div style={{height: 32,display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: 'center'}}>
<Text style={{
fontWeight: "bold",
fontSize: 20
}}>¥{(props.invoice.invoiceAmount / 100.0).toFixed(2)}</Text>
<Text style={{
fontWeight: "bold",
fontSize: 16
}}>{props.invoice.invoiceUploader.staffName }</Text>
</div>
<div style={{height: 22,display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: 'center'}}>
<Text>{invoiceTypeNameMap.get(props.invoice.invoiceKind)}</Text>
<Text>{props.invoice.invoiceUploader.staffId }</Text>
</div>
<li>{props.invoice.invoiceNo}</li>
<li>{props.invoice.invoiceDate}</li>
</div>
</Card>
</Badge.Ribbon>
</div>
)
}
function InvoiceManagement(props: {}) {
const [totalNum, setTotalNum] = useState(0)
const [invoices, setInvoices] = useState([] as any[])
const [search, setSearch] = useSearchParams()
const [invoiceSearchOption, setInvoiceSearchOption] = useState(new InvoiceSearchOption())
const [detailModalOpen, setDetailModalOpen] = useState(false)
const [invoiceDetail, setInvoiceDetail] = useState(null as InvoiceDetail | null)
const navigate = useNavigate()
const handleInvoiceSearchInfo = (invoiceSearch: InvoiceSearchOption) => {
//setInvoiceSearchOption(invoiceSearch)
searchInvoiceContent(invoiceSearch)
}
const searchInvoiceContent = (invoiceSearch: InvoiceSearchOption) => {
setInvoices([])
let params: any = invoiceSearch
Object.keys(params).forEach(key => {
if (params[key] != null && params[key] !== undefined && params[key] === "") {
params[key] = null
}
})
if (search.get('currentPage'))
params.pageNum = Number(search.get('currentPage')) - 1
else
params.pageNum = 0
if (search.get('pageSize'))
params.pageSize = Number(search.get('pageSize'))
else
params.pageSize = 10
axiosInstance({
url: 'common/invoice?' + qs.stringify(params, {skipNulls: true, arrayFormat: 'indices'}),
method: 'get',
}).then(response => {
console.log(response.data)
setTotalNum(response.data.total)
setInvoices([...response.data.records])
}).catch(function (error) {
console.log(error)
})
}
const onChange = (pageCurrentNum: Number, pageCurrentSize: Number) => {
console.log(pageCurrentNum, pageCurrentSize)
navigate('/invoice/management?currentPage=' + pageCurrentNum + '&pageSize=' + pageCurrentSize)
}
const onItemClick = (invoice: Invoice) => {
console.log(invoice)
axiosInstance({
url: 'common/invoice/' + invoice.invoiceId,
method: 'get',
}).then(response => {
console.log(response.data)
let detail: InvoiceDetail = response.data
detail.invoiceAmount /= 100.
setInvoiceDetail(detail)
setDetailModalOpen(true)
}).catch(function (error) {
console.log(error)
})
}
useEffect(() => {
console.log(search.get('currentPage'))
console.log(search.get('pageSize'))
searchInvoiceContent(invoiceSearchOption)
}, [search]);
console.log(invoices)
return (
<div style={{}}>
<InvoiceSearch handleSearchData={handleInvoiceSearchInfo}/>
<div style={{padding: 26, display: "flex", flexDirection: "column", alignItems: "flex-end"}}>
<div style={{display: "flex", flexWrap: "wrap", justifyContent: 'center', width: '100%'}}>
{invoices.map((item: Invoice, index: number) =>
<InvoiceItem onClick={onItemClick} invoice={item} key={index}/>
)}
</div>
<Pagination showSizeChanger
showQuickJumper
showTotal={(total) => `${total}`}
current={Number(search.get('currentPage') ? search.get('currentPage') : 1)}
pageSize={Number(search.get('pageSize') ? search.get('pageSize') : 10)}
total={totalNum} onChange={onChange}/>
</div>
<InvoiceDetailModal needRefresh={()=>{ searchInvoiceContent(invoiceSearchOption)}} invoiceDetail={invoiceDetail} open={detailModalOpen} onClose={() => {
setDetailModalOpen(false)
}} showFooter={true}/>
</div>
)
}
export default InvoiceManagement

View File

@ -13,7 +13,7 @@ import {
TimePicker,
Select,
Checkbox,
Pagination, theme
Pagination, theme, Tag, Badge
} from "antd";
import type {MenuProps, PaginationProps} from 'antd';
import {DownOutlined, UploadOutlined} from '@ant-design/icons';
@ -45,8 +45,9 @@ import {
} from "../../../models/Invoice";
import {RadioButtonProps} from "antd/es/radio/radioButton";
import qs from "qs";
import InvoiceDetailModal from "./InvoiceDetailModal";
import InvoiceDetailModal from "../InvoiceDetailModal";
import dayjs from "dayjs";
import {store} from "../../../models/store";
const {Meta} = Card;
@ -132,10 +133,10 @@ function InvoiceSearch(props: { handleSearchData: any; }) {
}
const getInvoiceKindsRadioButtons: any = () =>{
const getInvoiceKindsRadioButtons: any = () => {
let options: any[] = []
invoiceTypeNameMap.forEach(function (value, key, map) {
options.push( <Radio.Button style={{marginBottom: 10}} value={key}>{value}</Radio.Button>)
options.push(<Radio.Button style={{marginBottom: 10}} value={key}>{value}</Radio.Button>)
})
return options
}
@ -174,10 +175,16 @@ function InvoiceSearch(props: { handleSearchData: any; }) {
<Checkbox onChange={(e) => {
setComplexEnabled(e.target.checked)
}}></Checkbox>
<InvoiceUploadView/>
<InvoiceUploadView needRefresh={()=>{props.handleSearchData(invoiceSearchOption)}} />
</div>
{complexEnabled &&
<div className="complexSearchBar" style={{margin: '20px',padding: '20px 20px 0px 20px',display: "flex",backgroundColor: colorBgContainer, borderRadius: '20px'}}>
<div className="complexSearchBar" style={{
margin: '20px',
padding: '20px 20px 0px 20px',
display: "flex",
backgroundColor: colorBgContainer,
borderRadius: '20px'
}}>
<Form name="complexOption" onValuesChange={onValuesChange} style={{width: '100%'}}>
<Form.Item
@ -190,8 +197,9 @@ function InvoiceSearch(props: { handleSearchData: any; }) {
</Radio.Group>
</Form.Item>
<div style={{marginTop: -10, display:"flex", flexDirection:"row", flexWrap: "wrap"}}>
<Form.Item name="upload-time-picker" label="上传时间" {...rangeConfig} style={{marginRight:100}}>
<div style={{marginTop: -10, display: "flex", flexDirection: "row", flexWrap: "wrap"}}>
<Form.Item name="upload-time-picker" label="上传时间" {...rangeConfig}
style={{marginRight: 100}}>
<RangePicker/>
</Form.Item>
@ -217,46 +225,49 @@ function InvoiceSearch(props: { handleSearchData: any; }) {
)
}
class InvoiceItem extends React.Component<any, any> {
constructor(props: { invoice: Invoice }) {
super(props);
this.state = {
invoiceThumbnailUri: baseUrl + props.invoice.invoiceThumbnailUri,
invoiceKind: props.invoice.invoiceKind,
invoiceAmount: props.invoice.invoiceAmount,
invoiceDate: props.invoice.invoiceDate,
invoiceNo: props.invoice.invoiceNo,
}
console.log(this.state.invoiceThumbnailUri)
function InvoiceItem(props: { invoice: Invoice, onClick(invoice: Invoice): void }) {
const {
token: {colorBgContainer, colorPrimary, colorSuccess, colorWarning},
} = theme.useToken();
const onClick = () => {
props.onClick(props.invoice)
}
return (
<div style={{
margin: 30
}}>
<Badge.Ribbon color={['#00000000', colorPrimary, colorSuccess][props.invoice.invoiceState]} text={['', '报销中', '已报销'][props.invoice.invoiceState]}>
<Card
onClick={onClick}
hoverable
style={{
width: 250,
height: 300,
}}
cover={
<img alt="thumbnail"
src={baseUrl + props.invoice.invoiceThumbnailUri}
width="220" height="180"/>
}
>
<div style={{marginTop: -20}}>
<li style={{
fontWeight: "bold",
fontSize: 20
}}>¥{(props.invoice.invoiceAmount / 100.0).toFixed(2)}</li>
<li>{invoiceTypeNameMap.get(props.invoice.invoiceKind)}</li>
<li>{props.invoice.invoiceNo}</li>
<li>{props.invoice.invoiceDate}</li>
</div>
</Card>
</Badge.Ribbon>
</div>
onClick = () => {
this.props.onClick(this.props.invoice)
}
render() {
return (
<Card
onClick={this.onClick}
hoverable
style={{
width: 250,
height: 300,
margin: 30
}}
cover={<img alt="thumbnail"
src={this.state.invoiceThumbnailUri}
width="220" height="180"/>}
>
<div style={{marginTop: -20}}>
<li style={{fontWeight: "bold", fontSize: 20}}>¥{(this.state.invoiceAmount / 100.0).toFixed(2)}</li>
<li>{invoiceTypeNameMap.get(this.state.invoiceKind)}</li>
<li>{this.state.invoiceNo}</li>
<li>{this.state.invoiceDate}</li>
</div>
</Card>
)
}
)
}
@ -266,7 +277,7 @@ function InvoiceListView(props: {}) {
const [search, setSearch] = useSearchParams()
const [invoiceSearchOption, setInvoiceSearchOption] = useState(new InvoiceSearchOption())
const [detailModalOpen, setDetailModalOpen] = useState(false)
const [invoiceDetail, setInvoiceDetail] = useState(null as InvoiceDetail|null)
const [invoiceDetail, setInvoiceDetail] = useState(null as InvoiceDetail | null)
const navigate = useNavigate()
const handleInvoiceSearchInfo = (invoiceSearch: InvoiceSearchOption) => {
@ -289,8 +300,11 @@ function InvoiceListView(props: {}) {
params.pageSize = Number(search.get('pageSize'))
else
params.pageSize = 10
params.invoiceUploaderId = store.getState().token.staffId
axiosInstance({
url: 'common/invoice?'+qs.stringify( params,{skipNulls:true, arrayFormat: 'indices'}),
url: 'common/invoice?' + qs.stringify(params, {skipNulls: true, arrayFormat: 'indices'}),
method: 'get',
}).then(response => {
console.log(response.data)
@ -305,15 +319,15 @@ function InvoiceListView(props: {}) {
navigate('/invoice/mine?currentPage=' + pageCurrentNum + '&pageSize=' + pageCurrentSize)
}
const onItemClick = (invoice: Invoice) =>{
const onItemClick = (invoice: Invoice) => {
console.log(invoice)
axiosInstance({
url: 'common/invoice/'+invoice.invoiceId,
url: 'common/invoice/' + invoice.invoiceId,
method: 'get',
}).then(response => {
console.log(response.data)
let detail:InvoiceDetail = response.data
detail.invoiceAmount/=100.
let detail: InvoiceDetail = response.data
detail.invoiceAmount /= 100.
setInvoiceDetail(detail)
setDetailModalOpen(true)
}).catch(function (error) {
@ -345,7 +359,9 @@ function InvoiceListView(props: {}) {
pageSize={Number(search.get('pageSize') ? search.get('pageSize') : 10)}
total={totalNum} onChange={onChange}/>
</div>
<InvoiceDetailModal invoiceDetail={invoiceDetail} open={detailModalOpen} onClose={()=>{setDetailModalOpen(false)}}/>
<InvoiceDetailModal invoiceDetail={invoiceDetail} open={detailModalOpen} onClose={() => {
setDetailModalOpen(false)
}}/>
</div>
)

View File

@ -132,6 +132,7 @@ function FormModal(props: any) {
console.log(response.data)
setLoading(false)
props.onClose()
props.needRefresh()
//setOpen(false)
}).catch(function (error) {
console.log(error)
@ -240,7 +241,7 @@ function FormModal(props: any) {
class InvoiceUploadView extends React.Component<any, any> {
constructor(props: {}) {
constructor(props: any) {
super(props);
this.state = {
uploadModalOpen: false,
@ -281,7 +282,7 @@ class InvoiceUploadView extends React.Component<any, any> {
</Button>
<UpLoadModal open={this.state.uploadModalOpen} nextStep={this.handleNextStep} onClose={this.handleUploadModalClose}/>
<FormModal open={this.state.formModalOpen} onClose={this.handleClose}
<FormModal needRefresh={()=>{ this.props.needRefresh()}} open={this.state.formModalOpen} onClose={this.handleClose}
invoiceIdentifyResponse={this.state.invoiceIdentifyResponse}/>
</>);