Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions server/business/bill.business.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const DataLayer = require("../datalayer/mongo");
const model = require("../database").getModel("bill");
const httpError = require("http-errors");
const Utilities = require('../utilities')

module.exports = class BillBusiness {
constructor() {
Expand All @@ -25,13 +26,24 @@ module.exports = class BillBusiness {
}

/**
* Get a bill by ID.
* Get bill by ID.
*/
async getBillById(billId) {
return this.dataLayer
.findByPropertyAndPopulate(billId, [{path: 'journey', populate: {path: 'entryLocation exitLocation'}}, {path: 'driver', select: 'username type email'}])
.catch((error) => {
throw httpError(500, error.message)
})
}

/**
* Pay a bill by ID.
*/
async payBill(id) {
const record = {
paid: true
}
return this.dataLayer.update(id, record)
return this.dataLayer.update(Utilities.convertToObjectId(id), record)
.catch((error) => {throw httpError(404, error.message)
})
}
Expand Down
8 changes: 8 additions & 0 deletions server/controllers/bill.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ exports.getAllBills = async (req, res) => {
.catch((error) => {
res.status(error.status).send({message: error.message})
})
}

/**
* Get bill by ID.
*/
exports.getBillById = async (req, res) => {
billBusiness.getBillById(req.params.id)
.then((data) => {return res.status(200).send(data)})
.catch((error) => {res.status(error.status).send({message: error.message})})
}

/**
Expand Down
9 changes: 9 additions & 0 deletions server/datalayer/mongo.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ class DataLayer {
})
})
}

/**
* Find all records in the database.
*/
async findByPropertyAndPopulate(id, populateFilter) {
return this.model.findById(id)
.orFail(new Error("Record can't be found in the database."))
.populate(JSON.parse(JSON.stringify(populateFilter)))
}

/**
* Find a record by property in the database.
Expand Down
3 changes: 3 additions & 0 deletions server/routes/bill.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const billController = require("../controllers/bill.controller");
// Get All Bills
router.get("/", checkJwtToken, billController.getAllBills);

// Get Bill By Id
router.get("/:id", checkJwtToken, billController.getBillById);

// Pay for bill
router.put("/:id", checkJwtToken, billController.payBill);

Expand Down
9 changes: 9 additions & 0 deletions server/utilities.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
const haversine = require("haversine")
const httpError = require("http-errors")
const objectId = require("mongoose").Types.ObjectId

module.exports = class Utilities {
static calculateCost(journey) {
return (haversine(journey.entryLocation.coordinates, journey.exitLocation.coordinates) * this.costPerMile)
}

static convertToObjectId(id) {
if (objectId.isValid(id)) {
return objectId(id)
}
throw new httpError(400, "ID is not valid.")
}

static costPerMile = 0.01
}
13 changes: 13 additions & 0 deletions ui/src/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ const api = class Api {
});
}

async getBillById(billId) {
return axios
.get(`${this.baseUrl}/bill/${billId}`, {
withCredentials: true,
})
.then((response) => {
return response.data;
})
.catch((error) => {
throw error;
});
}

async payBill(billId) {
return axios
.put(
Expand Down
3 changes: 2 additions & 1 deletion ui/src/components/input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
<label class="w-100 mb-0">
{{ label }}
<ValidationProvider :name="label" :rules="rules" v-slot="{ errors }">
<b-input :placeholder="placeholder" :type="type" v-bind="$attrs" v-on="$listeners" :class="errors.length > 0 ? 'border-danger' : ''"/>
<b-form-datepicker v-if="type === 'date'" :placeholder="placeholder" :date-format-options="{ month: '2-digit', year: '2-digit', day: undefined, weekday: undefined }" v-bind="$attrs" v-on="$listeners" :class="errors.length > 0 ? 'border-danger' : ''"/>
<b-input v-else :placeholder="placeholder" :type="type" v-bind="$attrs" v-on="$listeners" :class="errors.length > 0 ? 'border-danger' : ''"/>
<span class="text-danger">{{ errors[0]}}</span>
</ValidationProvider>
</label>
Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
id="navbar"
v-if="loggedIn"
>
<div class="d-flex align-items-center">
<a class="d-flex align-items-center text-decoration-none" href="/my-bills">
<img
src="@/assets/creditcard.png"
alt="Self Service Portal Logo"
/>
<h4 class="ml-3 mb-0">Self Service Portal</h4>
</div>
</a>
<b-navbar-nav class="ml-auto">
<b-nav-item right class="mr-3" id="support" :to="{ name: 'Help' }">
<img
Expand Down
3 changes: 3 additions & 0 deletions ui/src/router/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const routes = [
path: "/my-bills/:id",
name: "PayBill",
component: PayBill,
meta: {
title: "Pay Bill",
},
beforeEnter: isAuthenticated,
},
{
Expand Down
18 changes: 13 additions & 5 deletions ui/src/styles/style.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
@import '~bootstrap/dist/css/bootstrap.css';
@import '~bootstrap-vue/dist/bootstrap-vue.css';

$Navy-Blue: #3c4253;
$Navy-Blue: #3c4253;
$White: white;

$theme-colors: (
"primary": $Navy-Blue
);

.navbar {
background-color: $Navy-Blue;
}
Expand All @@ -28,6 +29,10 @@ h4 {
color: $White;
}

.black-text {
color: black;
}

.email {
color: dodgerblue;
text-decoration: dodgerblue underline;
Expand All @@ -43,4 +48,7 @@ h4 {
.container {
max-width: 75% !important;
}
}
}

@import '~bootstrap/scss/bootstrap';
@import '~bootstrap-vue/src/index.scss';
20 changes: 16 additions & 4 deletions ui/src/views/MyBills.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<template>
<div class="mt-4">
<h1 class="mb-4">My Bills</h1>
<b-form-group>
<b-form-radio-group buttons button-variant="outline-primary" v-model="page" :options="pages"
@change="getBills" />
</b-form-group>
<b-table id="bills-table" :items="filteredBills" :fields="fields" show-empty empty-text="No bills match the filter." responsive striped>
<template #head(entrylocation)="head" >
{{head.label}}
Expand Down Expand Up @@ -29,8 +33,8 @@
<template #cell(cost)="cell">
{{ formatCost(cell.item.cost) }}
</template>
<template #cell(actions)>
<b-link>Pay Bill</b-link>
<template #cell(actions)="cell">
<b-link :to="{ name: 'PayBill', params: { id: cell.item._id }}" v-if="page !== 'Payment History'">Pay Bill</b-link>
</template>
</b-table>
<div class="d-flex justify-content-between align-items-baseline">
Expand All @@ -49,6 +53,7 @@
import Vue from 'vue'
import api from "@/api/api";
import { formatDate, formatCost } from "@/utilities";
import store from "@/store/store";

export default Vue.extend({
name: 'MyBills', //Sets the name of file.
Expand All @@ -62,7 +67,9 @@ export default Vue.extend({
entryLocation: '', //Stores the entry location filter.
exitLocation: '', //Stores the exit location filter.
carRegistrationNumber: '' //Stores the car registration number filter.
}
},
pages: ['Unpaid Bills', 'Payment History'],
page: ''
}
},
computed: {
Expand Down Expand Up @@ -95,7 +102,11 @@ export default Vue.extend({
* Gets a list of bills from the api and sets the bills and totalcount variables.
*/
async getBills() {
const data = await api.getAllBills({limit: this.limit, offset: parseInt(this.offset - 1)}) //TODO: Filter by DriverId
let paid = true
if(this.page === 'Unpaid Bills'){
paid = false
}
const data = await api.getAllBills({driver: store.getters.user.id, paid: paid, limit: this.limit, offset: parseInt(this.offset - 1)})
this.bills = data.bills
this.totalCount = data.count
}
Expand All @@ -104,6 +115,7 @@ export default Vue.extend({
* Gets a list of bills on create.
*/
async created() {
this.page = 'Unpaid Bills'
await this.getBills()
}
})
Expand Down
101 changes: 98 additions & 3 deletions ui/src/views/PayBill.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,108 @@
<template>
<div>
Pay Bill
<div class="mt-4">
<h1 class="mb-4">Payment</h1>
<b-card class="mb-3">
Your total charge {{ formatCost(bill.cost) }} for the journey on {{ formatDate(bill.journey.journeyDateTime)}}
</b-card>
<div class="d-flex">
<b-card class="mr-3 w-50">
<h4 class="black-text">Payment Details</h4>
<ValidationObserver ref="observer">
<Input label="Cardholder's name" rules="required" placeholder="Cardholder's name" v-model="paymentDetails.cardholderName" />
<Input label="Card number" rules="required" placeholder="Card number" @keypress="onlyNumber" class="mt-2" :maxlength="16" v-model="paymentDetails.cardNumber"/>
<div class="d-flex mt-2">
<Input label="CVV" rules="required" placeholder="CVV" class="mr-3" @keypress="onlyNumber" :maxlength="3" v-model="paymentDetails.cvv"/>
<Input label="Expiry Date" type="date" rules="required" placeholder="Expiry Date" class="w-50" v-model="paymentDetails.expiryDate"/>
</div>
</ValidationObserver>
</b-card>
<div class="w-50">
<b-card class="mb-2">
<h4 class="black-text pb-2">Journey Details</h4>
<p>Entry location: {{ bill.journey.entryLocation.name }}</p>
<p>Exit location: {{ bill.journey.exitLocation.name }}</p>
<p>Registration Number: {{ bill.journey.regNumber }}</p>
<p class="mb-0">Journey date: {{ formatDate(bill.journey.journeyDateTime) }}</p>
</b-card>
<div class="d-flex mt-2">
<b-button class="mr-2 w-100" variant="primary" @click="payBill">Pay</b-button>
<b-button class="w-25" variant="primary" @click="payBillViaPayPal">PayPal</b-button>
</div>
</div>
</div>
</div>
</template>

<script lang="js">
import Vue from 'vue';
import api from '../api/api'
import { formatCost, formatDate } from "@/utilities";
import Input from '../components/input'
import { ValidationObserver } from 'vee-validate'

export default Vue.extend({
name: "PayBill" //Sets the name of file.
name: "PayBill", //Sets the name of file.
components: { Input, ValidationObserver }, //Import the custom input field
data() {
return {
bill: {
journey: {
journeyDateTime: '', //Completed Journey Date and Time.
entryLocation: {
name: '' //Name of the entry location.
},
exitLocation: {
name: '' //Name of the exit location.
}
}
}, //Stores the bill.
paymentDetails: {
cardholderName: '', //Cards Cardholder name.
cardNumber: '', //Cards card number.
cvv: '', //Cards CVV number.
expiryDate: '' //Cards expiry date.
}
}
},
methods: {
formatDate, //Import the format date helper function to be used in the template.
formatCost, //Import the format cost helper function to be used in the template.
async payBillViaPayPal() {
api.payBill(this.bill._id).catch((error) => {
this.$bvToast.toast(error.message, { //Create red toast to show error.
title: 'Payment Failed',
variant: 'danger',
solid: true
})
});

await this.$router.push({name: 'MyBills'})
},
async payBill() {
const valid = await this.$refs.observer.validate() //Check if the payment details are valid.
if (!valid) {
return
}

api.payBill(this.bill._id).catch((error) => {
this.$bvToast.toast(error.message, { //Create red toast to show error.
title: 'Payment Failed',
variant: 'danger',
solid: true
})
});

await this.$router.push({name: 'MyBills'}) //Return user to the my bills page.
},
onlyNumber($event) {
let keyCode = ($event.keyCode ? $event.keyCode : $event.which); //Get inputted keycode
if ((keyCode < 48 || keyCode > 57)) { // checks if the value is a letter or .
$event.preventDefault();
}
}
},
async created() {
this.bill = await api.getBillById(this.$route.params.id) //Gets the selected bill by
}
})
</script>