ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 7 React – Create And List Invoice)

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.

Advertisements

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
Advertisements

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);
Advertisements

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);
}
Advertisements

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
Advertisements

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>
    );
  }
}
Advertisements

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>
    );
  }
}
Advertisements

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.

Advertisements

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 2 Auditing in EF Core with CreatedBy and LastModifiedBy)

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

ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 4 AutoMapper – Map object properties to another object)

ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 5 NSwag – Setting up Swagger and Auto generate API client code)

ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 6 React – How To Convert ReactJs To Typescript)

ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 7 React – Create And List Invoice)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s