In this part we will use the API that we created in the previous tutorials and the generated client code to communicate to our API.
Now we will create our user interface for Create invoice. We have a date input on it, so we will need a date picker and I decided to use react-dates for it. To install react-dates execute this command.
npm install react-dates @types/react-dates moment
Now we can start creating our Create Invoice component. Create a new folder inside the components folder and call it invoices. Inside the invoices folder create a new file called CreateInvoice.tsx
import React, { useState } from 'react'; import 'react-dates/initialize'; import 'react-dates/lib/css/_datepicker.css'; import { SingleDatePicker } from 'react-dates'; import { Card, CardBody, Row, Col, Input, FormGroup, Label, Table, Button, CardFooter, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap' import { CreateInvoiceCommand, DiscountType, TaxType, InvoiceItemVm, InvoicesClient } from '../../utils/api'; import { getSubtotal, getTotal, getBalance } from '../../utils/invoiceUtils'; import moment from 'moment'; import { RouteComponentProps, withRouter } from 'react-router-dom'; interface ICreateInvoice extends RouteComponentProps { } const CreateInvoice: React.FC<ICreateInvoice> = ({ history }) => { const [selectedDateFocus, setSelectedDateFocus] = useState<boolean | null>(false); const [selectedDueDateFocus, setSelectedDueDateFocus] = useState<boolean | null>(false); const initValue = new CreateInvoiceCommand({ invoiceNumber: '', logo: '', from: '', to: '', date: undefined, paymentTerms: '', dueDate: undefined, discount: undefined, discountType: DiscountType.Percentage, tax: undefined, taxType: TaxType.Percentage, amountPaid: undefined, invoiceItems: [new InvoiceItemVm({ id: 0, item: '', quantity: undefined, rate: undefined, amount: 0 })] }); const [invoiceData, setInvoiceData] = useState<CreateInvoiceCommand>(initValue); const [modal, setModal] = useState(false); const [title, setTile] = useState(''); const [message, setMessage] = useState(''); const toggle = () => setModal(!modal); const client = new InvoicesClient(); const updateInvoiceItem = (property: 'item' | 'quantity' | 'rate', index: number, value: any) => { if (invoiceData && invoiceData.invoiceItems) { const items = [...invoiceData.invoiceItems]; items[index][property] = value; setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, invoiceItems: [...items] })); } } const addInvoiceItem = () => { if (invoiceData && invoiceData.invoiceItems) { const items = [...invoiceData.invoiceItems]; items.push(new InvoiceItemVm({ id: 0, item: '', quantity: undefined, rate: undefined, amount: 0 })); setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, invoiceItems: [...items] })); } } const removeItem = (index:number) => { if (invoiceData && invoiceData.invoiceItems) { const items = [...invoiceData.invoiceItems]; items.splice(index, 1); setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, invoiceItems: [...items] })); } } return ( <div className="col-md-12"> <Card> <CardBody> <Row> <Col md={6}> <Input type="text" placeholder="Logo image url" style={{ display: 'block', marginBottom: '20px' }} value={invoiceData.logo} onChange={(evt: any) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, logo: evt.target.value }))} /> <Input type="textarea" placeholder="Who is this invoice from? (required)" style={{ display: 'block', resize: 'none', marginBottom: '20px' }} value={invoiceData.from} onChange={(evt: any) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, from: evt.target.value }))} /> <Input type="textarea" placeholder="Who is this invoice to? (required)" style={{ display: 'block', resize: 'none', marginBottom: '20px' }} value={invoiceData.to} onChange={(evt: any) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, to: evt.target.value }))} /> </Col> <Col md={6}> <FormGroup row> <Label md={4}>Invoice #</Label> <Col md={8}> <Input type="text" placeholder="Invoice number" value={invoiceData.invoiceNumber} onChange={(evt: any) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, invoiceNumber: evt.target.value }))} /> </Col> </FormGroup> <FormGroup row> <Label md={4}>Date</Label> <Col md={8}> <SingleDatePicker placeholder="Date" isOutsideRange={() => false} id="date-picker" small={true} block={true} numberOfMonths={1} date={invoiceData.date ? moment(invoiceData.date) : null} onDateChange={(date) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, date: date ? date.toDate() : undefined }))} focused={selectedDateFocus} onFocusChange={({ focused }) => setSelectedDateFocus(focused)} hideKeyboardShortcutsPanel={true} /> </Col> </FormGroup> <FormGroup row> <Label md={4}>Payment Terms</Label> <Col md={8}> <Input type="text" placeholder="Payment terms" value={invoiceData.paymentTerms} onChange={(evt: any) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, paymentTerms: evt.target.value }))} /> </Col> </FormGroup> <FormGroup row> <Label md={4}>Due Date</Label> <Col md={8}> <SingleDatePicker placeholder="Due date" isOutsideRange={() => false} id="due-date-picker" small={true} block={true} numberOfMonths={1} date={invoiceData.dueDate ? moment(invoiceData.dueDate) : null} onDateChange={(date) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, dueDate: date ? date.toDate() : undefined }))} focused={selectedDueDateFocus} onFocusChange={({ focused }) => setSelectedDueDateFocus(focused)} hideKeyboardShortcutsPanel={true} /> </Col> </FormGroup> <Row> <Label md={4} style={{ fontWeight: 'bold' }}>Balance</Label> <Label md={8} style={{ fontWeight: 'bold' }}>{getBalance(invoiceData)}</Label> </Row> </Col> </Row> <Row> <Col md={12}> <Table striped> <thead> <tr> <th style={{ width: '60%' }}>Item</th> <th>Quantity</th> <th>Rate</th> <th>Amount</th> <th></th> </tr> </thead> <tbody> {invoiceData.invoiceItems && invoiceData.invoiceItems.map((invoiceItem: InvoiceItemVm, index: number) => <tr key={`item-${index}`}> <td> <Input type="text" placeholder="Item description" value={invoiceItem.item} onChange={(evt: any) => updateInvoiceItem('item', index, evt.target.value)} /> </td> <td> <Input type="number" placeholder="0" value={invoiceItem.quantity || ''} onChange={(evt: any) => updateInvoiceItem('quantity', index, evt.target.value ? parseInt(evt.target.value) : undefined)} /> </td> <td> <Input type="number" placeholder="0" value={invoiceItem.rate || ''} onChange={(evt: any) => updateInvoiceItem('rate', index, evt.target.value ? parseInt(evt.target.value) : undefined)} /> </td> <td> {invoiceItem.quantity && invoiceItem.rate && invoiceItem.quantity * invoiceItem.rate} </td> <td><Button color="danger" onClick={() => removeItem(index)}>X</Button></td> </tr> )} </tbody> </Table> <Button className="btn btn-primary" onClick={() => addInvoiceItem()}> Add Item </Button> </Col> </Row> <Row> <Col md={6}></Col> <Col md={6}> <FormGroup row> <Label md={4}>Subtotal</Label> <Col md={8}> {getSubtotal(invoiceData.invoiceItems)} </Col> </FormGroup> <FormGroup row> <Label md={4}>Discount</Label> <Col md={4}> <Input type="number" placeholder="0" value={invoiceData.discount || ''} onChange={(evt: any) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, discount: evt.target.value ? parseInt(evt.target.value) : undefined }))} /> </Col> <Col md={4}> <Input type="select" value={invoiceData.discountType} onChange={(evt: any) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, discountType: evt.target.value }))}> <option value={DiscountType.Flat}>Flat rate</option> <option value={DiscountType.Percentage}>Percentage</option> </Input> </Col> </FormGroup> <FormGroup row> <Label md={4}>Tax</Label> <Col md={4}> <Input type="number" placeholder="0" value={invoiceData.tax || ''} onChange={(evt: any) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, tax: evt.target.value ? parseInt(evt.target.value) : undefined }))} /> </Col> <Col md={4}> <Input type="select" value={invoiceData.taxType} onChange={(evt: any) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, taxType: evt.target.value }))}> <option value={TaxType.Flat}>Flat rate</option> <option value={TaxType.Percentage}>Percentage</option> </Input> </Col> </FormGroup> <FormGroup row> <Label md={4}>Total</Label> <Col md={8}> {getTotal(invoiceData)} </Col> </FormGroup> <FormGroup row> <Label md={4}>Amount Paid</Label> <Col md={8}> <Input type="number" placeholder="0" value={invoiceData.amountPaid || ''} onChange={(evt: any) => setInvoiceData(new CreateInvoiceCommand({ ...invoiceData, amountPaid: evt.target.value ? parseInt(evt.target.value) : undefined }))} /> </Col> </FormGroup> </Col> </Row> </CardBody> <CardFooter> <Button className="btn btn-primary" onClick={() => { client.create(invoiceData) .then(() => history.push('/invoices')) .catch(err => { setTile('Error'); setMessage(err.message); setModal(true); }) }} >Save</Button> </CardFooter> </Card> <Modal isOpen={modal} toggle={toggle}> <ModalHeader toggle={toggle}>{title}</ModalHeader> <ModalBody> {message} </ModalBody> <ModalFooter> <Button color="primary" onClick={toggle}>Ok</Button> </ModalFooter> </Modal> </div> ) } export default withRouter(CreateInvoice);
Create a new folder inside the utils folder then add a file called invoiceUtils.ts
import { InvoiceItemVm, DiscountType, TaxType, CreateInvoiceCommand } from './api'; export const getSubtotal = (invoiceItems?: InvoiceItemVm[]) => { let amount = 0; if(invoiceItems) { invoiceItems.forEach(invoiceItem => { if(invoiceItem.quantity && invoiceItem.rate) { amount += invoiceItem.quantity * invoiceItem.rate; } }) } return amount; } export const getDiscount = (amount: number, type: DiscountType, discount?: number) => { if(discount) return type == DiscountType.Flat ? discount : (amount * (discount / 100)); return 0; } export const getTax = (amount: number, type: TaxType, tax?: number) => { if(tax) return type == TaxType.Flat ? tax : (amount * (tax / 100)); return 0; } export const getTotal = (invoiceData: CreateInvoiceCommand) => { const subTotal = getSubtotal(invoiceData.invoiceItems); const discountPrice = subTotal - getDiscount(subTotal, invoiceData.discountType as DiscountType, invoiceData.discount); return discountPrice + getTax(subTotal, invoiceData.taxType as TaxType, invoiceData.tax) } export const getBalance = (invoiceData: CreateInvoiceCommand) => { if(invoiceData && invoiceData.amountPaid) return getTotal(invoiceData) - invoiceData.amountPaid; return getTotal(invoiceData); }
Then now we will just a create a simple ui to list our created invoices. Create a new file inside the invoices folder and call it Invoices.tsx
import React, { useState, useEffect } from 'react'; import { InvoiceVm, InvoicesClient } from '../../utils/api'; import { Table } from 'reactstrap'; import {getBalance} from '../../utils/invoiceUtils'; import moment from 'moment'; interface IInvoices { } const Invoices: React.FC<IInvoices> = ({ }) => { const [invoices, setInvoices] = useState<InvoiceVm[]>([]); const client = new InvoicesClient(); useEffect(() => { client.get().then(res => setInvoices(res)) .catch(err => console.log(err)); }, []) return ( <div> <Table striped> <thead> <tr> <th>Invoice #</th> <th>Date</th> <th>Balance</th> </tr> </thead> <tbody> {invoices && invoices.map(invoice => <tr key={`invoice-${invoice.id}`}> <td>{invoice.invoiceNumber}</td> <td>{invoice.date && moment(invoice.date).format('MMMM D YYYY')}</td> <td>{getBalance(invoice)}</td> </tr>)} </tbody> </Table> </div> ) } export default Invoices
Update the App.tsx to include routes for Create and List Invoices.
import React, { Component } from 'react'; import { Route } from 'react-router'; import { Layout } from './components/Layout'; import { Home } from './components/Home'; import { FetchData } from './components/FetchData'; import { Counter } from './components/Counter'; import AuthorizeRoute from './components/api-authorization/AuthorizeRoute'; import ApiAuthorizationRoutes from './components/api-authorization/ApiAuthorizationRoutes'; import { ApplicationPaths } from './components/api-authorization/ApiAuthorizationConstants'; import CreateInvoice from './components/invoices/CreateInvoice'; import Invoices from './components/invoices/Invoices'; import './custom.css' export default class App extends Component { static displayName = App.name; render () { return ( <Layout> <Route exact path='/' component={Home} /> <Route path='/counter' component={Counter} /> <AuthorizeRoute path='/create' component={CreateInvoice} /> <AuthorizeRoute path='/invoices' component={Invoices} /> <AuthorizeRoute path='/fetch-data' component={FetchData} /> <Route path={ApplicationPaths.ApiAuthorizationPrefix} component={ApiAuthorizationRoutes} /> </Layout> ); } }
Then update the NavMenu.tsx to include a Create and List of Invoices in nav menu.
import React, { Component } from 'react'; import { Collapse, Container, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'; import { Link } from 'react-router-dom'; import { LoginMenu } from './api-authorization/LoginMenu'; import './NavMenu.css'; interface IProps { } interface IState { collapsed: boolean } export class NavMenu extends Component<IProps, IState> { static displayName = NavMenu.name; constructor (props: IProps) { super(props); this.toggleNavbar = this.toggleNavbar.bind(this); this.state = { collapsed: true }; } toggleNavbar () { this.setState({ collapsed: !this.state.collapsed }); } render () { return ( <header> <Navbar className="navbar-expand-sm navbar-toggleable-sm ng-white border-bottom box-shadow mb-3" light> <Container> <NavbarBrand tag={Link} to="/">InvoiceManagementApp.Api</NavbarBrand> <NavbarToggler onClick={this.toggleNavbar} className="mr-2" /> <Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={!this.state.collapsed} navbar> <ul className="navbar-nav flex-grow"> <NavItem> <NavLink tag={Link} className="text-dark" to="/">Home</NavLink> </NavItem> <NavItem> <NavLink tag={Link} className="text-dark" to="/counter">Counter</NavLink> </NavItem> <NavItem> <NavLink tag={Link} className="text-dark" to="/fetch-data">Fetch data</NavLink> </NavItem> <NavItem> <NavLink tag={Link} className="text-dark" to="/invoices">Invoices</NavLink> </NavItem> <NavItem> <NavLink tag={Link} className="text-dark" to="/create">Create Invoice</NavLink> </NavItem> <LoginMenu> </LoginMenu> </ul> </Collapse> </Container> </Navbar> </header> ); } }
Run the application and go to Create invoices you should now be able to create a new invoice.

Then go to Invoices to view the created Invoices.

RELATED ARTICLES:
ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 1)
ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 3 MediatR and FluentValidation)