Added ability to create manual orders via the admin panel

master
Mark Moffat 2020-01-02 16:00:24 +10:30
parent c34aef8939
commit b410d25ccf
7 changed files with 326 additions and 28 deletions

View File

@ -172,5 +172,6 @@
"Proceed to payment": "Proceed to payment", "Proceed to payment": "Proceed to payment",
"Return to information": "Return to information", "Return to information": "Return to information",
"Search shop": "Search shop", "Search shop": "Search shop",
"Dashboard": "Dashboard" "Dashboard": "Dashboard",
"Create order": "Create order"
} }

View File

@ -329,6 +329,61 @@ $(document).ready(function (){
} }
}); });
$(document).on('click', '#lookupCustomer', function(e){
e.preventDefault();
$.ajax({
method: 'POST',
url: '/admin/customer/lookup',
data: {
customerEmail: $('#customerEmail').val()
}
})
.done(function(result){
showNotification(result.message, 'success');
$('#orderFirstName').val(result.customer.firstName);
$('#orderLastName').val(result.customer.lastName);
$('#orderAddress1').val(result.customer.address1);
$('#orderAddress2').val(result.customer.address2);
$('#orderCountry').val(result.customer.country);
$('#orderState').val(result.customer.state);
$('#orderPostcode').val(result.customer.postcode);
$('#orderPhone').val(result.customer.phone);
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger');
});
});
$(document).on('click', '#orderCreate', function(e){
e.preventDefault();
if($('#createOrderForm').validator('validate').has('.has-error').length === 0){
$.ajax({
method: 'POST',
url: '/admin/order/create',
data: {
orderStatus: $('#orderStatus').val(),
email: $('#customerEmail').val(),
firstName: $('#orderFirstName').val(),
lastName: $('#orderLastName').val(),
address1: $('#orderAddress1').val(),
address2: $('#orderAddress2').val(),
country: $('#orderCountry').val(),
state: $('#orderState').val(),
postcode: $('#orderPostcode').val(),
phone: $('#orderPhone').val(),
orderComment: $('#orderComment').val()
}
})
.done(function(result){
showNotification(result.message, 'success');
window.location = `/admin/order/view/${result.orderId}`;
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger');
});
}
});
$('#sendTestEmail').on('click', function(e){ $('#sendTestEmail').on('click', function(e){
e.preventDefault(); e.preventDefault();
$.ajax({ $.ajax({

File diff suppressed because one or more lines are too long

View File

@ -285,6 +285,35 @@ router.get('/admin/customers/filter/:search', restrict, async (req, res, next) =
}); });
}); });
router.post('/admin/customer/lookup', restrict, async (req, res, next) => {
const db = req.app.db;
const customerEmail = req.body.customerEmail;
// Search for a customer
const customer = await db.customers.findOne({ email: customerEmail });
if(customer){
req.session.customerPresent = true;
req.session.customerEmail = customer.email;
req.session.customerFirstname = customer.firstName;
req.session.customerLastname = customer.lastName;
req.session.customerAddress1 = customer.address1;
req.session.customerAddress2 = customer.address2;
req.session.customerCountry = customer.country;
req.session.customerState = customer.state;
req.session.customerPostcode = customer.postcode;
req.session.customerPhone = customer.phone;
return res.status(200).json({
message: 'Customer found',
customer
});
}
return res.status(400).json({
message: 'No customers found'
});
});
// login the customer and check the password // login the customer and check the password
router.post('/customer/login_action', async (req, res) => { router.post('/customer/login_action', async (req, res) => {
const db = req.app.db; const db = req.app.db;
@ -451,18 +480,7 @@ router.post('/customer/reset/:token', async (req, res) => {
// logout the customer // logout the customer
router.post('/customer/logout', (req, res) => { router.post('/customer/logout', (req, res) => {
// Clear our session // Clear our session
req.session.customerPresent = null; common.clearCustomer(req);
req.session.customerEmail = null;
req.session.customerFirstname = null;
req.session.customerLastname = null;
req.session.customerAddress1 = null;
req.session.customerAddress2 = null;
req.session.customerCountry = null;
req.session.customerState = null;
req.session.customerPostcode = null;
req.session.customerPhone = null;
req.session.orderComment = null;
res.status(200).json({}); res.status(200).json({});
}); });

View File

@ -1,5 +1,13 @@
const express = require('express'); const express = require('express');
const common = require('../lib/common'); const {
clearSessionValue,
emptyCart,
getCountryList,
getId,
sendEmail,
getEmailTemplate,
clearCustomer
} = require('../lib/common');
const { restrict, checkAccess } = require('../lib/auth'); const { restrict, checkAccess } = require('../lib/auth');
const { indexOrders } = require('../lib/indexing'); const { indexOrders } = require('../lib/indexing');
const router = express.Router(); const router = express.Router();
@ -25,8 +33,8 @@ router.get('/admin/orders', restrict, async (req, res, next) => {
admin: true, admin: true,
config: req.app.config, config: req.app.config,
session: req.session, session: req.session,
message: common.clearSessionValue(req.session, 'message'), message: clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers helpers: req.handlebars.helpers
}); });
}); });
@ -60,8 +68,8 @@ router.get('/admin/orders/bystatus/:orderstatus', restrict, async (req, res, nex
filteredStatus: req.params.orderstatus, filteredStatus: req.params.orderstatus,
config: req.app.config, config: req.app.config,
session: req.session, session: req.session,
message: common.clearSessionValue(req.session, 'message'), message: clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers helpers: req.handlebars.helpers
}); });
}); });
@ -69,21 +77,120 @@ router.get('/admin/orders/bystatus/:orderstatus', restrict, async (req, res, nex
// render the editor // render the editor
router.get('/admin/order/view/:id', restrict, async (req, res) => { router.get('/admin/order/view/:id', restrict, async (req, res) => {
const db = req.app.db; const db = req.app.db;
const order = await db.orders.findOne({ _id: common.getId(req.params.id) }); const order = await db.orders.findOne({ _id: getId(req.params.id) });
res.render('order', { res.render('order', {
title: 'View order', title: 'View order',
result: order, result: order,
config: req.app.config, config: req.app.config,
session: req.session, session: req.session,
message: common.clearSessionValue(req.session, 'message'), message: clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: clearSessionValue(req.session, 'messageType'),
editor: true, editor: true,
admin: true, admin: true,
helpers: req.handlebars.helpers helpers: req.handlebars.helpers
}); });
}); });
// render the editor
router.get('/admin/order/create', restrict, async (req, res) => {
res.render('order-create', {
title: 'Create order',
config: req.app.config,
session: req.session,
message: clearSessionValue(req.session, 'message'),
messageType: clearSessionValue(req.session, 'messageType'),
countryList: getCountryList(),
editor: true,
admin: true,
helpers: req.handlebars.helpers
});
});
router.post('/admin/order/create', async (req, res, next) => {
const db = req.app.db;
const config = req.app.config;
// Check if cart is empty
if(!req.session.cart){
res.status(400).json({
message: 'The cart is empty. You will need to add items to the cart first.'
});
}
const orderDoc = {
orderPaymentId: getId(),
orderPaymentGateway: 'Instore',
orderPaymentMessage: 'Your payment was successfully completed',
orderTotal: req.session.totalCartAmount,
orderItemCount: req.session.totalCartItems,
orderProductCount: req.session.totalCartProducts,
orderEmail: req.body.email || req.session.customerEmail,
orderFirstname: req.body.firstName || req.session.customerFirstname,
orderLastname: req.body.lastName || req.session.customerLastname,
orderAddr1: req.body.address1 || req.session.customerAddress1,
orderAddr2: req.body.address2 || req.session.customerAddress2,
orderCountry: req.body.country || req.session.customerCountry,
orderState: req.body.state || req.session.customerState,
orderPostcode: req.body.postcode || req.session.customerPostcode,
orderPhoneNumber: req.body.phone || req.session.customerPhone,
orderComment: req.body.orderComment || req.session.orderComment,
orderStatus: req.body.orderStatus,
orderDate: new Date(),
orderProducts: req.session.cart,
orderType: 'Single'
};
// insert order into DB
try{
const newDoc = await db.orders.insertOne(orderDoc);
// get the new ID
const orderId = newDoc.insertedId;
// add to lunr index
indexOrders(req.app)
.then(() => {
// set the results
req.session.messageType = 'success';
req.session.message = 'Your order was successfully placed. Payment for your order will be completed instore.';
req.session.paymentEmailAddr = newDoc.ops[0].orderEmail;
req.session.paymentApproved = true;
req.session.paymentDetails = `<p><strong>Order ID: </strong>${orderId}</p>
<p><strong>Transaction ID: </strong>${orderDoc.orderPaymentId}</p>`;
// set payment results for email
const paymentResults = {
message: req.session.message,
messageType: req.session.messageType,
paymentEmailAddr: req.session.paymentEmailAddr,
paymentApproved: true,
paymentDetails: req.session.paymentDetails
};
// clear the cart
if(req.session.cart){
emptyCart(req, res, 'function');
}
// Clear customer session
clearCustomer(req);
// send the email with the response
// TODO: Should fix this to properly handle result
sendEmail(req.session.paymentEmailAddr, `Your order with ${config.cartTitle}`, getEmailTemplate(paymentResults));
// redirect to outcome
res.status(200).json({
message: 'Order created successfully',
orderId
});
});
}catch(ex){
res.status(400).json({ err: 'Your order declined. Please try again' });
}
});
// Admin section // Admin section
router.get('/admin/orders/filter/:search', restrict, async (req, res, next) => { router.get('/admin/orders/filter/:search', restrict, async (req, res, next) => {
const db = req.app.db; const db = req.app.db;
@ -92,7 +199,7 @@ router.get('/admin/orders/filter/:search', restrict, async (req, res, next) => {
const lunrIdArray = []; const lunrIdArray = [];
ordersIndex.search(searchTerm).forEach((id) => { ordersIndex.search(searchTerm).forEach((id) => {
lunrIdArray.push(common.getId(id.ref)); lunrIdArray.push(getId(id.ref));
}); });
// we search on the lunr indexes // we search on the lunr indexes
@ -113,8 +220,8 @@ router.get('/admin/orders/filter/:search', restrict, async (req, res, next) => {
config: req.app.config, config: req.app.config,
session: req.session, session: req.session,
searchTerm: searchTerm, searchTerm: searchTerm,
message: common.clearSessionValue(req.session, 'message'), message: clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers helpers: req.handlebars.helpers
}); });
}); });
@ -125,7 +232,7 @@ router.get('/admin/order/delete/:id', restrict, async(req, res) => {
// remove the order // remove the order
try{ try{
await db.orders.deleteOne({ _id: common.getId(req.params.id) }); await db.orders.deleteOne({ _id: getId(req.params.id) });
// remove the index // remove the index
indexOrders(req.app) indexOrders(req.app)
@ -163,7 +270,7 @@ router.post('/admin/order/statusupdate', restrict, checkAccess, async (req, res)
const db = req.app.db; const db = req.app.db;
try{ try{
await db.orders.updateOne({ await db.orders.updateOne({
_id: common.getId(req.body.order_id) }, _id: getId(req.body.order_id) },
{ $set: { orderStatus: req.body.status } { $set: { orderStatus: req.body.status }
}, { multi: false }); }, { multi: false });
return res.status(200).json({ message: 'Status successfully updated' }); return res.status(200).json({ message: 'Status successfully updated' });

116
views/order-create.hbs Normal file
View File

@ -0,0 +1,116 @@
{{> partials/menu}}
<div class="col-sm-9">
<div class="col-sm-12">
<h2>Create Order</h2>
</div>
<div class="order-layout col-md-12">
<div class="row">
<div class="col-12 bottom-pad-20">
<button id="orderCreate" class="btn btn-outline-success float-right">{{ @root.__ "Create order" }}</button>
</div>
</div>
<div class="row">
<div class="col-5">
<form id="createOrderForm">
<div class="row">
<div class="col-12">
<div class="form-group">
<label for="exampleInputEmail1">Order Status</label>
<select class="form-control" id="orderStatus">
<option>{{ @root.__ "Completed" }}</option>
<option>{{ @root.__ "Paid" }}</option>
<option selected>{{ @root.__ "Pending" }}</option>
<option>{{ @root.__ "Cancelled" }}</option>
<option>{{ @root.__ "Declined" }}</option>
<option>{{ @root.__ "Shipped" }}</option>
</select>
</div>
</div>
<div class="col-12">
<div class="form-group">
<label for="exampleInputEmail1">Customer email address</label>
<div class="input-group">
<input type="email" class="form-control" id="customerEmail" aria-describedby="emailHelp" placeholder="Customer email address">
<div class="input-group-append">
<button class="btn btn-outline-success" id="lookupCustomer" type="button">Find</button>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label class="control-label">{{ @root.__ "First name" }} *</label>
<input type="text" class="form-control" name="orderFirstName" id="orderFirstName" value="{{session.customerFirstname}}" placeholder="First name" required>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label class="control-label">{{ @root.__ "Last name" }} *</label>
<input type="text" class="form-control" name="orderLastName" id="orderLastName" value="{{session.customerLastname}}" placeholder="Last name" required>
</div>
</div>
<div class="col-12">
<div class="form-group">
<label class="control-label">{{ @root.__ "Address 1" }} *</label>
<input type="text" class="form-control" name="orderAddress1" id="orderAddress1" value="{{session.customerAddress1}}" placeholder="Address line 1" required>
</div>
</div>
<div class="col-12">
<div class="form-group">
<label class="control-label">{{ @root.__ "Address 2" }}</label>
<input type="text" class="form-control" name="orderAddress2" id="orderAddress2" value="{{session.customerAddress2}}" placeholder="Address line 2">
</div>
</div>
<div class="col-12">
<div class="form-group">
<label class="control-label">{{ @root.__ "Country" }} *</label>
<select class="form-control" id="orderCountry" name="orderCountry" required>
<option value="" disabled selected>Select Country</option>
{{#each countryList}}
<option {{selectState this @root.session.customerCountry}} value="{{this}}">{{this}}</option>
{{/each}}
</select>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label class="control-label">{{ @root.__ "State" }} *</label>
<input type="text" class="form-control" name="orderState" id="orderState" value="{{session.customerState}}" placeholder="State" required>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label class="control-label">{{ @root.__ "Postcode" }} *</label>
<input type="text" class="form-control" name="orderPostcode" id="orderPostcode" value="{{session.customerPostcode}}" placeholder="Postcode" required>
</div>
</div>
<div class="col-12">
<div class="form-group">
<label class="control-label">{{ @root.__ "Phone number" }} *</label>
<input type="text" class="form-control" name="orderPhone" id="orderPhone" value="{{session.customerPhone}}" placeholder="Phone number" required>
</div>
</div>
<div class="col-12">
<div class="form-group">
<label class="control-label">{{ @root.__ "Order comment" }}</label>
<div class="form-group">
<textarea class="form-control" placeholder="Order comment" id="orderComment" name="orderComment">{{session.orderComment}}</textarea>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="col-7">
{{#if session.cart}}
{{> (getTheme 'cart')}}
{{else}}
<div class="alert alert-warning" role="alert">
<h5 class="text-center">The cart is empty.</h5><hr>
<h6 class="text-center">Add some products <a target="_blank" href="/">here</a> then come back to complete the order.</h6>
</div>
{{/if}}
</div>
</div>
</div>
</div>

View File

@ -8,7 +8,8 @@
{{/ifCond}} {{/ifCond}}
<li class="list-group-item"><i class="fas fa-list fa-icon"></i> &nbsp; <a href="/admin/products">{{ @root.__ "List" }}</a></li> <li class="list-group-item"><i class="fas fa-list fa-icon"></i> &nbsp; <a href="/admin/products">{{ @root.__ "List" }}</a></li>
<li class="list-group-item"><strong>Orders</strong></li> <li class="list-group-item"><strong>Orders</strong></li>
<li class="list-group-item"><i class="fas fa-cube fa-icon"></i> &nbsp; <a href="/admin/orders">{{ @root.__ "List" }}</a></li> <li class="list-group-item"><i class="fas fa-cubes fa-icon"></i> &nbsp; <a href="/admin/orders">{{ @root.__ "List" }}</a></li>
<li class="list-group-item"><i class="fas fa-cube fa-icon"></i> &nbsp; <a href="/admin/order/create">{{ @root.__ "Create" }}</a></li>
<li class="list-group-item"><strong>Customers</strong></li> <li class="list-group-item"><strong>Customers</strong></li>
<li class="list-group-item"><i class="fas fa-users fa-icon"></i> &nbsp; <a href="/admin/customers">{{ @root.__ "List" }}</a></li> <li class="list-group-item"><i class="fas fa-users fa-icon"></i> &nbsp; <a href="/admin/customers">{{ @root.__ "List" }}</a></li>
<li class="list-group-item"><strong>Users</strong></li> <li class="list-group-item"><strong>Users</strong></li>