To get paid for goods and services provided, companies/freelancers need to send invoices to their customers informing them of the services that they will be charged for. Back then, people had paper invoices which they gave to the customers when they contact them for their services. Right now, with the advent and advancement of technology, people are now able to send electronic invoices to their customers.
To follow through the article adequately, you’ll need the following:
To verify your installation, run the following :
node --version npm --version
If you get version numbers as results then you’re good to go.
Now that we have the requirements all set, the next thing to do is to create the backend server for the application. The backend server will simply:
Now, let’s get to creating the server. Create a folder to house the new project and initialize it as a node project:
mkdir invoicing-app cd invoicing-app && npm init
For the server to function appropriately, there are some node packages that need to be installed, you can install them by running the command :
npm install --save express body-parser connect-multiparty sqlite3 bluebird path umzug bcrypt
bcrypt
to hash user passwordsexpress
to power our web applicationsqlite3
to create and maintain the databasepath
to resolve file paths within our applicationbluebird
to use Promises when writing migrationsumzug
as a task runner to run our database migrationsbody-parser
and connect-multiparty
to handle incoming form requestsNow, go ahead to create a server.js
file that will house the application logic:
touch server.js
In the server.js
, Import the necessary modules and create an express app:
// server.jsconst express = require('express') const bodyParser = require('body-parser'); const sqlite3 = require('sqlite3').verbose(); const PORT = process.env.PORT || 3128; const app = express(); app.use(bodyParser.urlencoded({extended: false})); app.use(bodyParser.json()); [...]
then create a simple /
route to test our server works:
// server.js
[…]app.get('/', function(req,res){ res.send("Welcome to Invoicing App"); }); app.listen(PORT, function(){ console.log(`App running on localhost:${PORT}`); });
app.listen()
tells the server the port to listen to for incoming routes.
To start the server, run the following in your project directory:
node server
Your application begins to listen to incoming requests.
For an invoicing application, a database is needed to store the existing invoices. SQLite
is going to be our database client of choice for this application.
To create a database folder, create a file for your database in the folder:
mkdir database
cd database && touch InvoicingApp.db
In the database directory, run the sqlite3
client and then open InvoicingApp.db database
$ invoicing-app/database/ sqlite3
> .open InvoicingApp.db
Now that the database to be used has been selected, next thing is to create the needed tables.
Database Structure For this application, there are 3 tables needed:
Since the necessary tables have been identified, the next step is to run the queries to create the tables.
Writing Migrations Migrations are used to keep track of changes in a database as the application grows. To do this, create a migrations
folder in the database
directory.
mkdir migrations
This will house all the migration files.
Now, create a 1.0.js
file in the migrations
folder. This naming convention is to keep track of the newest changes.
cd migations && touch 1.0.js
In the 1.0.js
file, you first import the node modules:
// database/migrations 1.0.js
“use strict”;
const Promise = require(“bluebird”);
const sqlite3 = require(“sqlite3”);
const path = require(‘path’);[...]
Then, the idea now is to export an up
function that will be executed when the migration file is run and a down
function to reverse the changes to the database.
// database/migrations/1.0.js[...] module.exports = { up: function() { return new Promise(function(resolve, reject) { /* Here we write our migration function */ let db = new sqlite3.Database('./database/InvoicingApp.db'); // enabling foreign key constraints on sqlite db db.run(`PRAGMA foreign_keys = ON`); [...]
In the up
function, connection is first made to the database and then, foreign keys are enabled on the sqlite
database
In SQLite, foreign keys are disabled by default to allow for backwards compatibility so, the foreign keys have to be enabled on every connection
After successful connection, the queries to create the tables are then specified
// database/migrations/1.0.js
[…]
db.serialize(function() {
db.run(CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT, email TEXT, company_name TEXT, password TEXT )
);db.run(`CREATE TABLE invoices ( id INTEGER PRIMARY KEY, name TEXT, user_id INTEGER, paid NUMERIC, FOREIGN KEY(user_id) REFERENCES users(id) )`); db.run(`CREATE TABLE transactions ( id INTEGER PRIMARY KEY, name TEXT, price INTEGER, invoice_id INTEGER, FOREIGN KEY(invoice_id) REFERENCES invoices(id) )`); }); db.close(); }); }, [...]
The serialize()
is used to specify that the queries should be run sequentially and not simultaneously
Afterwards, the queries to reverse the changes are also specified in the down()
function below
// database/migrations/1.0.js
[…]down: function() { return new Promise(function(resolve, reject) { /* This runs if we decide to rollback. In that case we must revert the `up` function and bring our database to it's initial state */ let db = new sqlite3.Database("./database/InvoicingApp.db"); db.serialize(function() { db.run(`DROP TABLE transactions`); db.run(`DROP TABLE invoices`); db.run(`DROP TABLE users`); }); db.close(); }); } };
Running Migrations Once the migration files have been created, the next step is running them to effect the changes in the database. To do this, create a scripts
folder from the root of your application and then create a file called migrate.js
.
mkdir scripts
cd scripts && touch migrate.js
The migrate.js
file will look like this:
// scripts/migrate.jsconst path = require("path"); const Umzug = require("umzug"); let umzug = new Umzug({ logging: function() { console.log.apply(null, arguments); }, migrations: { path: "./database/migrations", pattern: /\.js$/ }, upName: "up", downName: "down" }); [...]
First, the needed node modules are imported and then, a new umzug
object is created with the following configurations. The path and pattern of the migrations scripts are also specified. To learn more about the configurations, head over here
To also give some verbose feedback, create a function to log events as shown below and then finally execute the up
function to run the database queries specified in the migrations folder.
// scripts/migrate.js
[…]function logUmzugEvent(eventName) { return function(name, migration) { console.log(`${name} ${eventName}`); }; } // using event listeners to log events umzug.on("migrating", logUmzugEvent("migrating")); umzug.on("migrated", logUmzugEvent("migrated")); umzug.on("reverting", logUmzugEvent("reverting")); umzug.on("reverted", logUmzugEvent("reverted")); // this will run your migrations umzug.up().then(console.log("all migrations done"));
Now, to execute the script, go to your terminal and in the root directory of your application, run:
> ~/invoicing-app node scripts/migrate.js up
all migrations done
== 1.0: migrating =======
1.0 migrating
Now that the database is adequately set up, the next thing is to go back to the server.js
file and create the application routes. For this application, the following routes will be made available:
Registering a New User To register a new user, a post request will be made to the /register
route of our server. This route will look like this :
// server.js
[…]
const bcrypt = require(‘bcrypt’)
const saltRounds = 10;
[…]app.post('/register', function(req, res){ // check to make sure none of the fields are empty if( isEmpty(req.body.name) || isEmpty(req.body.email) || isEmpty(req.body.company_name) || isEmpty(req.body.password) ){ return res.json({ 'status' : false, 'message' : 'All fields are required' }); } // any other intendend checks [...]
A check is made to see if any of the fields are empty. If no fields are empty and if the data sent matches all the specification. If an error occurs, an error message is sent to the user as a response. If not, the password is hashed and the data is then stored in the database and a response is sent to the user informing them that they are registered.
// server.js
bcrypt.hash(req.body.password, saltRounds, function(err, hash) {
let db = new sqlite3.Database(“./database/InvoicingApp.db”);
let sql =INSERT INTO users(name,email,company_name,password) VALUES('${ req.body.name }','${req.body.email}','${req.body.company_name}','${hash}')
;
db.run(sql, function(err) {
if (err) {
throw err;
} else {
return res.json({
status: true,
message: “User Created”
});
}
});
db.close();
});
});
When a sample request is made from Postman, the result below is obtained :
Logging in a New User Now, if an existing user tries to log in to the system using the /login
route, they need to provide their email address and password. Once they do that, the route handles the request as follows :
// server.js
[…]app.post("/login", function(req, res) { let db = new sqlite3.Database("./database/InvoicingApp.db"); let sql = `SELECT * from users where email='${req.body.email}'`; db.all(sql, [], (err, rows) => { if (err) { throw err; } db.close(); if (rows.length == 0) { return res.json({ status: false, message: "Sorry, wrong email" }); } [...]
A query is made to the database to fetch the record of the user with a particular email. If the result returns an empty array, then it means that the user doesn’t exist and a response is sent informing the user of the error.
If the database query returned user data, further check is made to see if the password entered matches that password in the database. If it does, then a response is sent with the user data.
// server.js
[…]
let user = rows[0];
let authenticated = bcrypt.compareSync(req.body.password, user.password);
delete user.password;
if (authenticated) {
return res.json({
status: true,
user: user
});
}
return res.json({
status: false,
message: “Wrong Password, please retry”
});
});
});[...]
When the route is tested from Postman, you get this result:
Creating a new Invoice The /invoice
route handles the creation of an invoice. Data passed to the route will include the following:
The server handles the request as follows:
// server.js
[…]
app.post(“/invoice”, multipartMiddleware, function(req, res) {
// validate data
if (isEmpty(req.body.name)) {
return res.json({
status: false,
message: “Invoice needs a name”
});
}
// perform other checks[...]
First, the data sent to the server is validated and then, a connection is made to the database for the subsequent queries.
// server.js
[…]
// create invoice
let db = new sqlite3.Database(“./database/InvoicingApp.db”);
let sql =INSERT INTO invoices(name,user_id,paid) VALUES( '${req.body.name}', '${req.body.user_id}', 0 )
;
[…]
The insert query needed to create the invoice is written and then, it is executed. Afterwards, the singular transactions are inserted into the transactions
table with the invoice_id
as foreign key to reference them.
// server.js
[…]
db.serialize(function() {
db.run(sql, function(err) {
if (err) {
throw err;
}
let invoice_id = this.lastID;
for (let i = 0; i < req.body.txn_names.length; i++) {
let query =INSERT INTO transactions(name,price,invoice_id) VALUES( '${req.body.txn_names[i]}', '${req.body.txn_prices[i]}', '${invoice_id}' )
;
db.run(query);
}
return res.json({
status: true,
message: “Invoice created”
});
});
});
[…]
Once this is executed, the invoice is successfully created:
On checking the SQLite
database, the following result is obtained:
sqlite> select * from invoices;
1|Test Invoice New|2|0
sqlite> select * from transactions;
1|iPhone|600|1
2|Macbook|1700|1
Fetching All Invoices
Now, when a user wants to see all the created invoices, the client will make a GET
request to the /invoice/user/:id
route. The user id is passed as a route parameter. The request is handled as follows:
// index.js
[…]
app.get(“/invoice/user/:user_id”, multipartMiddleware, function(req, res) {
let db = new sqlite3.Database(“./database/InvoicingApp.db”);
let sql =SELECT * FROM invoices LEFT JOIN transactions ON invoices.id=transactions.invoice_id WHERE user_id='${req.params.user_id}'
;
db.all(sql, [], (err, rows) => {
if (err) {
throw err;
}
return res.json({
status: true,
transactions: rows
});
});
});[...]
A query is run to fetch all the invoices and the transactions related to the invoice belonging to a particular user.
Fetching Single Invoice To fetch a specific invoice, the a GET
request is with the user_id
and invoice_id
to the /invoice/user/{user_id}/{invoice_id}
route. The request is handles as follows
// index.js
[…]app.get("/invoice/user/:user_id/:invoice_id", multipartMiddleware, function(req, res) { let db = new sqlite3.Database("./database/InvoicingApp.db"); let sql = `SELECT * FROM invoices LEFT JOIN transactions ON invoices.id=transactions.invoice_id WHERE user_id='${ req.params.user_id }' AND invoice_id='${req.params.invoice_id}'`; db.all(sql, [], (err, rows) => { if (err) { throw err; } return res.json({ status: true, transactions: rows }); }); }); // set application port [...]
A query is run to fetch a single invoice and the transactions related to the invoice belonging to the user.
Running the request on Postman will give you the result below :
In this part of the series, we walked though how to get up your server with all the needed routes for the mini invoicing application. In the next part of this series, we will look at how to Create the Interface for the invoicing application using Vue. Here’s a link to the full Github repository.
In this part, let’s take a look at how to build the part of the application users will interact with, the user interface.
To follow through this article, you’ll need the following:
To build the frontend of this application, we’ll be using Vue. Vue
is a progressive JavaScript framework used for building useful and interactive user interfaces. To install vue, run the following command:
npm install -g vue-cli
To confirm your Vue installation, run the command:
vue --version
If you get the version number as your result then you’re good to go.
To create a new project with Vue, run the following command and then follow the prompt:
vue init webpack invoicing-app-frontend
If you have followed the first part of the series, you can run this command in the same project directory. If not the command will create the project in your existing directory.
This creates a sample Vue
project that we’ll build upon in this article.
Installing Node Modules For the frontend of this invoicing application, a lot of requests are going to be made to the backend server. To do this, we’ll make use of axios. To install axios
, run the command in your project directory:
npm install axios --save
Adding Bootstrap To allow us get some default styling in our application, We’ll make use of Bootstrap
. To add it to your application, add the following to the index.html
file in the project:
// index.html
[…]
<link rel=“stylesheet” href=“https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css” integrity=“sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4” crossorigin=“anonymous”>
[…]
<script src=“https://code.jquery.com/jquery-3.3.1.slim.min.js” integrity=“sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo” crossorigin=“anonymous”></script>
<script src=“https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js” integrity=“sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ” crossorigin=“anonymous”></script>
<script src=“https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js” integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm"crossorigin=“anonymous”></script>
[…]
For this application, we are going to have 2 major routes:
/
- To render the login page/dashboard
- To render the user dashboardTo configure these routes, open the src/router/index.js
and update it to look like this:
// src/router/index.js
import Vue from ‘vue’
import Router from ‘vue-router’
import SignUp from ‘@/components/SignUp’
import Dashboard from ‘@/components/Dashboard’
Vue.use(Router)
export default new Router({
mode: ‘history’,
routes: [
{
path: ‘/’,
name: ‘SignUp’,
component: SignUp
},
{
path: ‘/dashboard’,
name: ‘Dashboard’,
component: Dashboard
},
]
})
This specifies the components that should be displayed to the user when they visit your application.
One of the major selling points of Vue is the component structure. Components allows the frontend of your application to be more modular and reusable. For this application, we’ll have the following components:
Navigation Component This is the sidebar that will house the links of different actions. Create a new component in the /src/components
directory:
touch SideNav.vue
Now, edit the SideNav.vue
like this:
// src/components/SideNav.vue
<script>
export default {
name: “SideNav”,
props: [“name”, “company”],
methods: {
setActive(option) {
this.$parent.$parent.isactive = option;
},
openNav() {
document.getElementById(“leftsidenav”).style.width = “20%”;
},
closeNav() {
document.getElementById(“leftsidenav”).style.width = “0%”;
}
}
};
</script>[…]
The component is created with two props
: first the name of the user and second, the name of the company. The two methods are to add the collapsible functionality to the sidebar. The setActive
method will update the component calling the parent of the SideNav
component, in this case Dashboard
, when a user clicks on a navigation link.
The component has the following template:
// src/components/SideNav.vue
[…]
<template>
<div>
<span style=“font-size:30px;cursor:pointer” v-on:click=“openNav”>☰</span>
<div id=“leftsidenav” class=“sidenav”>
<p style=“font-size:12px;cursor:pointer” v-on:click=“closeNav”><em>Close Nav</em></p>
<p><em>Company: {{ company }} </em></p>
<h3>Welcome, {{ name }}</h3>
<p class=“clickable” v-on:click=“setActive(‘create’)”>Create Invoice</p>
<p class=“clickable” v-on:click=“setActive(‘view’)”>View Invoices</p>
</div>
</div>
</template>
[…]
Styling for the component can be obtained from the Github repository.
Header Component The header component is simple, it displays the name of the application and side bar if a user is signed in. Create a Header.vue
file in the src/components
directory:
touch Header.vue
The component file will look like this:
// src/components/Header.vue
<template>
<nav class=“navbar navbar-light bg-light”>
<template v-if=“user != null”>
<SideNav v-bind:name=“user.name” v-bind:company=“user.company_name”/>
</template>
<span class=“navbar-brand mb-0 h1”>{{title}}</span>
</nav>
</template>
<script>
import SideNav from ‘./SideNav’
export default {
name: “Header”,
props : [“user”],
components: {
SideNav
},
data() {
return {
title: “Invoicing App”,
};
}
};
</script>
The header component has a single prop
called user
. This prop
will be passed by any component that will use the header component. In template for the header, the SideNav
component created earlier is imported and conditional rendering is used to determine if the SideNav
should be displayed or not.
SignUp/SignIn Component This component has one job, to house the signup form and sign in form. Create a new file in /src/components
directory.
touch SignIn.vue
Now, the component to register/login a user is a little more complex than the two earlier components. We need to have the Login and Register forms on the same page. Let’s take a look at how to achieve this.
First, create the component:
// src/components/SignIn.vue
<script>
import Header from “./Header”;
import axios from “axios”;
export default {
name: “SignUp”,
components: {
Header
},
data() {
return {
model: {
name: “”,
email: “”,
password: “”,
c_password: “”,
company_name: “”
},
loading: “”,
status: “”
};
},
[…]
The Header
component is imported and the data properties of the components are also specified. Next, create the methods to handle what happens when data is submitted:
// src/components/SignIn.vue
[…]
methods: {
validate() {
// checks to ensure passwords match
if( this.model.password != this.model.c_password){
return false;
}
return true;
},
[…]
The validate method performs checks to make sure the data sent by the user meets our requirements.
// src/components/SignIn.vue
[…]
register() {
const formData = new FormData();
let valid = this.validate();
if(valid){
formData.append(“name”, this.model.name);
formData.append(“email”, this.model.email);
formData.append(“company_name”, this.model.company_name);
formData.append(“password”, this.model.password);this.loading = “Registering you, please wait”;
// Post to server
axios.post(“http://localhost:3128/register”, formData).then(res => {
// Post a status message
this.loading = “”;
if (res.data.status == true) {
// now send the user to the next route
this.$router.push({
name: “Dashboard”,
params: { user: res.data.user }
});
} else {
this.status = res.data.message;
}
});
}else{
alert(“Passwords do not match”);
}
},
[…]
The register
method of the component handles the action when a user tries to register a new account. First, the data is validated using the validate
method. Then If all criteria are met, the data is prepared for submission using the formData
.
We’ve also defined the loading
property of the component to let the user know when their form is being processed. Finally, a POST
request is sent to the backend server using axios. When a response is received from the server with a status of true
, the user is directed to the dashboard else, an error message is displayed to the user.
// src/components/SignIn.vue
[…]
login() {
const formData = new FormData();
formData.append(“email”, this.model.email);
formData.append(“password”, this.model.password);
this.loading = “Signing in”;
// Post to server
axios.post(“http://localhost:3128/login”, formData).then(res => {
// Post a status message
this.loading = “”;
if (res.data.status == true) {
// now send the user to the next route
this.$router.push({
name: “Dashboard”,
params: { user: res.data.user }
});
} else {
this.status = res.data.message;
}
});
}
}
};
</script>
Just like the register
method, the data is prepared and sent over to the backend server to authenticate the user. If the user exists and the details match, the user is directed to their dashboard.
The Vue router was used here to direct the user to a different view. This will be visited more in-depth as we move on
Now, let’s also take a look at the template for the signup component:
// src/components/SignUp.vue
<template>
[…]
<div class=“tab-pane fade show active” id=“pills-login” role=“tabpanel” aria-labelledby=“pills-login-tab”>
<div class=“row”>
<div class=“col-md-12”>
<form @submit.prevent=“login”>
<div class=“form-group”>
<label for=“”>Email:</label>
<input type=“email” required class=“form-control” placeholder=“eg chris@invoiceapp.com” v-model=“model.email”>
</div><div class="form-group"> <label for="">Password:</label> <input type="password" required class="form-control" placeholder="Enter Password" v-model="model.password"> </div> <div class="form-group"> <button class="btn btn-primary" >Login</button> {{ loading }} {{ status }} </div> </form>
</div>
</div>
</div>
[…]
The login in form is shown above and the input fields are linked to the respective data properties specified when the components were created. When the submit button of the form is clicked, the login method of the component is called.
Usually, when the submit button of a form is clicked, the form is submitted via a GET
or POST
request but instead of using that, we added this when creating the form to override the default behavior and specify that the login function should be called.
<form @submit.prevent=“login”>
The register form also looks like this:
// src/components/SignIn.vue
[…]
<div class=“tab-pane fade” id=“pills-register” role=“tabpanel” aria-labelledby=“pills-register-tab”>
<div class=“row”>
<div class=“col-md-12”>
<form @submit.prevent=“register”>
<div class=“form-group”>
<label for=“”>Name:</label>
<input type=“text” required class=“form-control” placeholder=“eg Chris” v-model=“model.name”>
</div>
<div class=“form-group”>
<label for=“”>Email:</label>
<input type=“email” required class=“form-control” placeholder=“eg chris@invoiceapp.com” v-model=“model.email”>
</div><div class="form-group"> <label for="">Company Name:</label> <input type="text" required class="form-control" placeholder="eg Chris Tech" v-model="model.company_name"> </div> <div class="form-group"> <label for="">Password:</label> <input type="password" required class="form-control" placeholder="Enter Password" v-model="model.password"> </div> <div class="form-group"> <label for="">Confirm Password:</label> <input type="password" required class="form-control" placeholder="Confirm Passowrd" v-model="model.confirm_password"> </div> <div class="form-group"> <button class="btn btn-primary" >Register</button> {{ loading }} {{ status }} </div> </form>
</div>
</div>
[…]
</template>
The @submit.prevent
is also used here to call the register
method when the submit button is clicked.
Now, when you run your development server using the command:
npm run dev
Visit localhost:8080/
on your browser and you get the following result:
Dashboard Component Now, the Dashboard component will be displayed when the user gets routed to the /dashboard
route. It displays the Header
and the CreateInvoice
component by default. Create the Dashboard.vue
file in the src/components
directory
touch Dashboard.vue
Edit the file to look like this:
// src/component/Dashboard.vue
<script>
import Header from “./Header”;
import CreateInvoice from “./CreateInvoice”;
import ViewInvoices from “./ViewInvoices”;
export default {
name: “Dashboard”,
components: {
Header,
CreateInvoice,
ViewInvoices,
},
data() {
return {
isactive: ‘create’,
title: “Invoicing App”,
user : (this.$route.params.user) ? this.$route.params.user : null
};
}
};
</script>
[…]
Above, the necessary components are imported and are rendered based on the template below:
// src/components/Dashboard.vue
[…]
<template>
<div class=“container-fluid” style=“padding: 0px;”>
<Header v-bind:user=“user”/>
<template v-if=“this.isactive == ‘create’”>
<CreateInvoice />
</template>
<template v-else>
<ViewInvoices />
</template>
</div>
</template>
We will look at how to create theCreateInvoice
andViewInvoices
components later in the article.
Create Invoice Component The CreateInvoice
component contains the form needed to create a new invoice. Create a new file in the src/components
directory:
touch CreateInvoice.vue
Edit the CreateInvoice
component to look like this:
// src/components/CreateInvoice.vue<template>
<div>
<div class=“container”>
<div class=“tab-pane fade show active”>
<div class=“row”>
<div class=“col-md-12”>
<h3>Enter Details below to Create Invoice</h3>
<form @submit.prevent=“onSubmit”>
<div class=“form-group”>
<label for=“”>Invoice Name:</label>
<input type=“text” required class=“form-control” placeholder=“eg Seller’s Invoice” v-model=“invoice.name”>
</div>
<div class=“form-group”>
<label for=“”>Invoice Price:</label><span> $ {{ invoice.total_price }}</span>
</div>
[…]
We create a form that accepts the name of the invoice and displays the total price of the invoice. The total price is obtained by summing up the prices of individual transactions for the invoice.
Let’s take a look at how transactions are added to the invoice:
// src/components/CreateInvoice.vue
[…]
<hr />
<h3> Transactions </h3>
<div class=“form-group”>
<label for=“”>Add Transaction:</label>
<button type=“button” class=“btn btn-primary” data-toggle=“modal” data-target=“#transactionModal”>
+
</button>
<!-- Modal -->
<div class=“modal fade” id=“transactionModal” tabindex=“-1” role=“dialog” aria-labelledby=“transactionModalLabel” aria-hidden=“true”>
<div class=“modal-dialog” role=“document”>
<div class=“modal-content”>
<div class=“modal-header”>
<h5 class=“modal-title” id=“exampleModalLabel”>Add Transaction</h5>
<button type=“button” class=“close” data-dismiss=“modal” aria-label=“Close”>
<span aria-hidden=“true”>×</span>
</button>
</div>
<div class=“modal-body”>
<div class=“form-group”>
<label for=“”>Transaction name</label>
<input type=“text” id=“txn_name_modal” class=“form-control”>
</div>
<div class=“form-group”>
<label for=“”>Price ($)</label>
<input type=“numeric” id=“txn_price_modal” class=“form-control”>
</div>
</div>
<div class=“modal-footer”>
<button type=“button” class=“btn btn-secondary” data-dismiss=“modal”>Discard Transaction</button>
<button type=“button” class=“btn btn-primary” data-dismiss=“modal” v-on:click=“saveTransaction()”>Save transaction</button>
</div>
</div>
</div>
</div>
</div>
[…]
A button is displayed for the user to add a new transaction. When the button is clicked, a modal is shown to the user to enter the details of the transaction. When the Save Transaction
button is clicked, a method adds it to the existing transactions.
// src/components/CreateInvoice.vue
[…]
<div class=“col-md-12”>
<table class=“table”>
<thead>
<tr>
<th scope=“col”>#</th>
<th scope=“col”>Transaction Name</th>
<th scope=“col”>Price ($)</th>
<th scope=“col”></th>
</tr>
</thead>
<tbody>
<template v-for=“txn in transactions”>
<tr :key=“txn.id”>
<th>{{ txn.id }}</th>
<td>{{ txn.name }}</td>
<td>{{ txn.price }} </td>
<td><button type=“button” class=“btn btn-danger” v-on:click=“deleteTransaction(txn.id)”>X</button></td>
</tr>
</template>
</tbody>
</table>
</div><div class=“form-group”>
<button class=“btn btn-primary” >Create Invoice</button>
{{ loading }}
{{ status }}
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
[…]
The existing transactions are displayed in a tabular format. When the X
button is clicked, the transaction in question is deleted from the transaction list and the Invoice Price is recalculated. Finally, the Create Invoice
button triggers a function that then prepares the data and sends it to the backend server for creation of the invoice.
Let’s also take a look at the component structure of the Create Invoice
component:
// src/components/CreateInvoice.vue
[…]
<script>
import axios from “axios”;
export default {
name: “CreateInvoice”,
data() {
return {
invoice: {
name: “”,
total_price: 0
},
transactions: [],
nextTxnId: 1,
loading: “”,
status: “”
};
},
[…]
First, we defined the data properties for the component. The component will have an invoice object containing the invoice name
and total_price
. It’ll also have an array of transactions
with an index “nextTxnId
". This will keep track of the transactions and variables to send status updates to the user (loading
, status
)
// src/components/CreateInvoice.vue
[…]
methods: {
saveTransaction() {
// append data to the arrays
let name = document.getElementById(“txn_name_modal”).value;
let price = document.getElementById(“txn_price_modal”).value;if( name.length != 0 && price > 0){ this.transactions.push({ id: this.nextTxnId, name: name, price: price }); this.nextTxnId++; this.calcTotal(); // clear their values document.getElementById("txn_name_modal").value = ""; document.getElementById("txn_price_modal").value = ""; } }, [...]
The methods for the CreateInvoice
component are also defined. The saveTransaction()
method takes the values in the transaction form modal and then adds it to the transaction list. The deleteTransaction()
method deletes an existing transaction object from the list of transactions while the calcTotal()
method recalculates the total invoice price when a new transaction is added or deleted.
// src/components/CreateInvoice.vue
[…]
deleteTransaction(id) {
let newList = this.transactions.filter(function(el) {
return el.id !== id;
});
this.nextTxnId–;
this.transactions = newList;
this.calcTotal();
},
calcTotal(){
let total = 0;
this.transactions.forEach(element => {
total += parseInt(element.price);
});
this.invoice.total_price = total;
},
[…]
Finally, the onSubmit()
method will submit the form to the backend server. In the method, we’ll use formData
and axios
to send the requests. The transactions array containing the transaction objects is split into two different arrays. One array to hold the transaction names and the other to hold the transaction prices. The server then attempts to process the request and send back a response to the user.
onSubmit() {
const formData = new FormData();
// format for request
let txn_names = [];
let txn_prices = [];
this.transactions.forEach(element => {
txn_names.push(element.name);
txn_prices.push(element.price);
});
formData.append(“name”, this.invoice.name);
formData.append(“txn_names”, txn_names);
formData.append(“txn_prices”, txn_prices);
formData.append(“user_id”, this.$route.params.user.id);
this.loading = “Creating Invoice, please wait …”;
// Post to server
axios.post(“http://localhost:3128/invoice”, formData).then(res => {
// Post a status message
this.loading = “”;
if (res.data.status == true) {
this.status = res.data.message;
} else {
this.status = res.data.message;
}
});
}
}
};
</script>
When you go back to the application on localhost:8080
and sign in, you get redirected to a dashboard that looks like this:
View Invoices Component Now that we can create invoices, the next would be to have a visual picture of invoices that have been created and their statuses. To do this, let’s create a ViewInvoices.vue
file in the src/components
directory of the application.
touch ViewInvoices.vue
Edit the file to look like this:
// src/components/ViewInvoices.vue
<template>
<div>
<div class=“container”>
<div class=“tab-pane fade show active”>
<div class=“row”>
<div class=“col-md-12”>
<h3>Here are a list of your Invoices</h3>
<table class=“table”>
<thead>
<tr>
<th scope=“col”>Invoice #</th>
<th scope=“col”>Invoice Name</th>
<th scope=“col”>Status</th>
<th scope=“col”></th>
</tr>
</thead>
<tbody>
<template v-for=“invoice in invoices”>
<tr>
<th scope=“row”>{{ invoice.id }}</th>
<td>{{ invoice.name }}</td>
<td v-if=“invoice.paid == 0 “> Unpaid </td>
<td v-else> Paid </td>
<td ><a href=”#” class=“btn btn-success”>TO INVOICE</a></td> </tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
[…]
The template above contains a table displaying the invoices a user has created. It also has a button that takes the user to a single invoice view page when an invoice is clicked.
// src/components/ViewInvoice.vue
[…]
<script>
import axios from “axios”;
export default {
name: “ViewInvoices”,
data() {
return {
invoices: [],
user: this.$route.params.user
};
},
mounted() {
axios
.get(http://localhost:3128/invoice/user/${this.user.id}
)
.then(res => {
if (res.data.status == true) {
this.invoices = res.data.invoices;
}
});
}
};
</script>
The ViewInvoices
component has its data properties as an array of invoices and the user details. The user details are obtained from the route parameters and when the component is mounted
, a GET
is made to the backend server to fetch the list of invoices created by the user which are then displayed using the template that was shown earlier.
When you go to the Dashboard
, click the View Invoices
option on the SideNav
and you get a view that looks like this:
In this part of the series, we configured the user interface of the invoicing application using concepts from Vue. In the next part of the series, we will take a look at how to add JWT authentication to maintain user sessions, view single invoices and send invoices via email to recipients. Feel free to leave a comment below. Here’s a link to the Github repository.
Thanks for reading ❤
If you liked this post, share it with all of your programming buddies!
Follow us on Facebook | Twitter
☞ Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)
☞ Nuxt.js - Vue.js on Steroids
☞ Build Web Apps with Vue JS 2 & Firebase
☞ The Complete Node.js Developer Course (3rd Edition)
☞ Angular & NodeJS - The MEAN Stack Guide
☞ NodeJS - The Complete Guide (incl. MVC, REST APIs, GraphQL)
☞ Node.js: The Complete Guide to Build RESTful APIs (2018)
☞ The JavaScript Developer’s Guide To Node.JS
☞ Node.js, ExpressJs, MongoDB and Vue.js (MEVN Stack) Application Tutorial
☞ Full Stack Developers: Everything You Need to Know
☞ How to Perform Web-Scraping using Node.js
☞ Google’s Go Essentials For Node.js / JavaScript Developers
☞ Restful API with NodeJS, Express, PostgreSQL, Sequelize, Travis, Mocha, Coveralls and Code Climate
Originally published on https://scotch.io
#vue-js #node-js #javascript