Adding discount code support (#109)
Added a discount code module. Lots of work went into this. Please report any problems as issues to be fixedmaster
parent
83dc6f76ee
commit
799ed301f2
7
app.js
7
app.js
|
@ -20,7 +20,7 @@ const crypto = require('crypto');
|
|||
const common = require('./lib/common');
|
||||
const { runIndexing } = require('./lib/indexing');
|
||||
const { addSchemas } = require('./lib/schema');
|
||||
const { initDb } = require('./lib/db');
|
||||
const { initDb, getDbUri } = require('./lib/db');
|
||||
let handlebars = require('express-handlebars');
|
||||
const i18n = require('i18n');
|
||||
|
||||
|
@ -237,6 +237,9 @@ handlebars = handlebars.create({
|
|||
formatDate: (date, format) => {
|
||||
return moment(date).format(format);
|
||||
},
|
||||
discountExpiry: (start, end) => {
|
||||
return moment().isBetween(moment(start), moment(end));
|
||||
},
|
||||
ifCond: (v1, operator, v2, options) => {
|
||||
switch(operator){
|
||||
case'==':
|
||||
|
@ -317,7 +320,7 @@ handlebars = handlebars.create({
|
|||
|
||||
// session store
|
||||
const store = new MongoStore({
|
||||
uri: config.databaseConnectionString,
|
||||
uri: getDbUri(config.databaseConnectionString),
|
||||
collection: 'sessions'
|
||||
});
|
||||
|
||||
|
|
|
@ -246,6 +246,36 @@
|
|||
"orderProducts": []
|
||||
}
|
||||
],
|
||||
"discounts": [
|
||||
{
|
||||
"code": "valid_10_amount_code",
|
||||
"type": "amount",
|
||||
"value": 10,
|
||||
"start": "",
|
||||
"end": ""
|
||||
},
|
||||
{
|
||||
"code": "valid_10_percent_code",
|
||||
"type": "percent",
|
||||
"value": 10,
|
||||
"start": "",
|
||||
"end": ""
|
||||
},
|
||||
{
|
||||
"code": "expired_10_percent_code",
|
||||
"type": "percent",
|
||||
"value": 10,
|
||||
"start": "",
|
||||
"end": ""
|
||||
},
|
||||
{
|
||||
"code": "future_10_percent_code",
|
||||
"type": "percent",
|
||||
"value": 10,
|
||||
"start": "",
|
||||
"end": ""
|
||||
}
|
||||
],
|
||||
"menu": {
|
||||
"items": [
|
||||
{
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
"maxQuantity": 25,
|
||||
"modules": {
|
||||
"enabled": {
|
||||
"shipping": "shipping-basic"
|
||||
"shipping": "shipping-basic",
|
||||
"discount": "discount-voucher"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ const restrictedRoutes = [
|
|||
{ route: '/admin/product/edit/:id', response: 'redirect' },
|
||||
{ route: '/admin/product/update', response: 'redirect' },
|
||||
{ route: '/admin/product/delete/:id', response: 'redirect' },
|
||||
{ route: '/admin/product/published_state', response: 'json' },
|
||||
{ route: '/admin/product/publishedState', response: 'json' },
|
||||
{ route: '/admin/product/setasmainimage', response: 'json' },
|
||||
{ route: '/admin/product/deleteimage', response: 'json' },
|
||||
{ route: '/admin/product/removeoption', response: 'json' },
|
||||
|
@ -20,7 +20,7 @@ const restrictedRoutes = [
|
|||
{ route: '/admin/settings/menu/new', response: 'json' },
|
||||
{ route: '/admin/settings/menu/update', response: 'json' },
|
||||
{ route: '/admin/settings/menu/delete', response: 'json' },
|
||||
{ route: '/admin/settings/menu/save_order', response: 'json' },
|
||||
{ route: '/admin/settings/menu/saveOrder', response: 'json' },
|
||||
{ route: '/admin/file/upload', response: 'json' }
|
||||
];
|
||||
|
||||
|
|
|
@ -111,8 +111,9 @@ const clearSessionValue = (session, sessionVar) => {
|
|||
return temp;
|
||||
};
|
||||
|
||||
const updateTotalCart = (req, res) => {
|
||||
const updateTotalCart = async (req, res) => {
|
||||
const config = getConfig();
|
||||
const db = req.app.db;
|
||||
|
||||
req.session.totalCartAmount = 0;
|
||||
req.session.totalCartItems = 0;
|
||||
|
@ -131,15 +132,34 @@ const updateTotalCart = (req, res) => {
|
|||
// Update the total items in cart for the badge
|
||||
req.session.totalCartItems = Object.keys(req.session.cart).length;
|
||||
|
||||
// Net cart amount
|
||||
const netCartAmount = req.session.totalCartAmount - req.session.totalCartShipping || 0;
|
||||
// Update the total amount not including shipping/discounts
|
||||
req.session.totalCartNetAmount = req.session.totalCartAmount;
|
||||
|
||||
// Calculate shipping using the loaded module
|
||||
config.modules.loaded.shipping.calculateShipping(
|
||||
netCartAmount,
|
||||
req.session.totalCartNetAmount,
|
||||
config,
|
||||
req
|
||||
);
|
||||
|
||||
// If discount module enabled
|
||||
if(config.modules.loaded.discount){
|
||||
// Recalculate discounts
|
||||
const discount = await db.discounts.findOne({ code: req.session.discountCode });
|
||||
if(discount){
|
||||
config.modules.loaded.discount.calculateDiscount(
|
||||
discount,
|
||||
req
|
||||
);
|
||||
}else{
|
||||
// If discount code is not found, remove it
|
||||
delete req.session.discountCode;
|
||||
req.session.totalCartDiscount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate our total amount removing discount and adding shipping
|
||||
req.session.totalCartAmount = (req.session.totalCartNetAmount - req.session.totalCartDiscount) + req.session.totalCartShipping;
|
||||
};
|
||||
|
||||
const emptyCart = async (req, res, type, customMessage) => {
|
||||
|
@ -150,12 +170,13 @@ const emptyCart = async (req, res, type, customMessage) => {
|
|||
delete req.session.shippingAmount;
|
||||
delete req.session.orderId;
|
||||
delete req.session.cartSubscription;
|
||||
delete req.session.discountCode;
|
||||
|
||||
// Remove cart from DB
|
||||
await db.cart.deleteOne({ sessionId: req.session.id });
|
||||
|
||||
// update total cart
|
||||
updateTotalCart(req, res);
|
||||
await updateTotalCart(req, res);
|
||||
|
||||
// Update checking cart for subscription
|
||||
updateSubscriptionCheck(req, res);
|
||||
|
|
26
lib/db.js
26
lib/db.js
|
@ -14,15 +14,14 @@ function initDb(dbUrl, callback){ // eslint-disable-line
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
// Set the DB url
|
||||
dbUrl = getDbUri(dbUrl);
|
||||
|
||||
// select DB
|
||||
const dbUriObj = mongodbUri.parse(dbUrl);
|
||||
let db;
|
||||
// if in testing, set the testing DB
|
||||
if(process.env.NODE_ENV === 'test'){
|
||||
db = client.db('testingdb');
|
||||
}else{
|
||||
db = client.db(dbUriObj.database);
|
||||
}
|
||||
|
||||
// Set the DB depending on ENV
|
||||
const db = client.db(dbUriObj.database);
|
||||
|
||||
// setup the collections
|
||||
db.users = db.collection('users');
|
||||
|
@ -33,17 +32,28 @@ function initDb(dbUrl, callback){ // eslint-disable-line
|
|||
db.customers = db.collection('customers');
|
||||
db.cart = db.collection('cart');
|
||||
db.sessions = db.collection('sessions');
|
||||
db.discounts = db.collection('discounts');
|
||||
|
||||
_db = db;
|
||||
return callback(null, _db);
|
||||
}
|
||||
};
|
||||
|
||||
function getDbUri(dbUrl){
|
||||
const dbUriObj = mongodbUri.parse(dbUrl);
|
||||
// if in testing, set the testing DB
|
||||
if(process.env.NODE_ENV === 'test'){
|
||||
dbUriObj.database = 'expresscart-test';
|
||||
}
|
||||
return mongodbUri.format(dbUriObj);
|
||||
}
|
||||
|
||||
function getDb(){
|
||||
return _db;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getDb,
|
||||
initDb
|
||||
initDb,
|
||||
getDbUri
|
||||
};
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
const calculateDiscount = (discount, req) => {
|
||||
let discountAmount = 0;
|
||||
if(req.session.discountCode){
|
||||
if(discount.type === 'amount'){
|
||||
discountAmount = discount.value;
|
||||
}
|
||||
if(discount.type === 'percent'){
|
||||
// Apply the discount on the net cart amount (eg: minus shipping)
|
||||
discountAmount = (discount.value / 100) * req.session.totalCartNetAmount;
|
||||
}
|
||||
}
|
||||
|
||||
req.session.totalCartDiscount = discountAmount;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
calculateDiscount
|
||||
};
|
|
@ -24,7 +24,7 @@ const calculateShipping = (amount, config, req) => {
|
|||
if(!req.session.customerCountry){
|
||||
req.session.shippingMessage = 'Estimated shipping';
|
||||
req.session.totalCartShipping = domesticShippingAmount;
|
||||
req.session.totalCartAmount = req.session.totalCartAmount + domesticShippingAmount;
|
||||
req.session.totalCartAmount = amount + domesticShippingAmount;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -32,14 +32,14 @@ const calculateShipping = (amount, config, req) => {
|
|||
if(req.session.customerCountry.toLowerCase() !== shippingFromCountry.toLowerCase()){
|
||||
req.session.shippingMessage = 'International shipping';
|
||||
req.session.totalCartShipping = internationalShippingAmount;
|
||||
req.session.totalCartAmount = req.session.totalCartAmount + internationalShippingAmount;
|
||||
req.session.totalCartAmount = amount + internationalShippingAmount;
|
||||
return;
|
||||
}
|
||||
|
||||
// Domestic shipping
|
||||
req.session.shippingMessage = 'Domestic shipping';
|
||||
req.session.totalCartShipping = domesticShippingAmount;
|
||||
req.session.totalCartAmount = req.session.totalCartAmount + domesticShippingAmount;
|
||||
req.session.totalCartAmount = amount + domesticShippingAmount;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const moment = require('moment');
|
||||
const glob = require('glob');
|
||||
const Ajv = require('ajv');
|
||||
const ajv = new Ajv();
|
||||
|
@ -19,6 +20,13 @@ const addSchemas = () => {
|
|||
const amountRegex = /^\d+\.\d\d$/;
|
||||
ajv.addFormat('amount', amountRegex);
|
||||
|
||||
// Datetime format
|
||||
ajv.addFormat('datetime', {
|
||||
validate: (dateTimeString) => {
|
||||
return moment(dateTimeString, 'DD/MM/YYYY HH:mm').isValid();
|
||||
}
|
||||
});
|
||||
|
||||
ajv.addKeyword('isNotEmpty', {
|
||||
type: 'string',
|
||||
validate: (schema, data) => {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$id": "editDiscount",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"amount",
|
||||
"percent"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"type": "number"
|
||||
},
|
||||
"start": {
|
||||
"type": "object",
|
||||
"format" : "datetime"
|
||||
},
|
||||
"end": {
|
||||
"type": "object",
|
||||
"format" : "datetime"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"discountId"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"$id": "newDiscount",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"amount",
|
||||
"percent"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"type": "number"
|
||||
},
|
||||
"start": {
|
||||
"type": "object",
|
||||
"format" : "datetime"
|
||||
},
|
||||
"end": {
|
||||
"type": "object",
|
||||
"format" : "datetime"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code",
|
||||
"type",
|
||||
"value",
|
||||
"start",
|
||||
"end"
|
||||
]
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
const { getConfig } = require('./common');
|
||||
const { initDb } = require('./db');
|
||||
const { fixProductDates } = require('../test/helper');
|
||||
const { fixProductDates, fixDiscountDates } = require('../test/helper');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
@ -15,6 +15,7 @@ initDb(config.databaseConnectionString, (err, db) => {
|
|||
db.users.deleteMany({}, {}),
|
||||
db.customers.deleteMany({}, {}),
|
||||
db.products.deleteMany({}, {}),
|
||||
db.discounts.deleteMany({}, {}),
|
||||
db.menu.deleteMany({}, {})
|
||||
])
|
||||
.then(() => {
|
||||
|
@ -22,6 +23,7 @@ initDb(config.databaseConnectionString, (err, db) => {
|
|||
db.users.insertMany(jsonData.users),
|
||||
db.customers.insertMany(jsonData.customers),
|
||||
db.products.insertMany(fixProductDates(jsonData.products)),
|
||||
db.discounts.insertMany(fixDiscountDates(jsonData.discounts)),
|
||||
db.menu.insertOne(jsonData.menu)
|
||||
])
|
||||
.then(() => {
|
||||
|
|
|
@ -172,5 +172,24 @@
|
|||
"Dashboard": "Dashboard",
|
||||
"Create order": "Create order",
|
||||
"Order shipping amount": "Order shipping amount",
|
||||
"Order net amount": "Order net amount"
|
||||
"Order net amount": "Order net amount",
|
||||
"Discount code": "Discount code",
|
||||
"Apply": "Apply",
|
||||
"Discount codes": "Discount codes",
|
||||
"Code": "Code",
|
||||
"Expiry": "Expiry",
|
||||
"There are currently no discount codes setup.": "There are currently no discount codes setup.",
|
||||
"New discount": "New discount",
|
||||
"Amount": "Amount",
|
||||
"Percent": "Percent",
|
||||
"Discount type": "Discount type",
|
||||
"Discount value": "Discount value",
|
||||
"Discount": "Discount",
|
||||
"Edit discount": "Edit discount",
|
||||
"New Discount": "New Discount",
|
||||
"Discount start": "Discount start",
|
||||
"Discount end": "Discount end",
|
||||
"Start": "Start",
|
||||
"Running": "Running",
|
||||
"Not running": "Not running"
|
||||
}
|
|
@ -9255,71 +9255,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"superagent": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
|
||||
"integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"component-emitter": "^1.2.0",
|
||||
"cookiejar": "^2.1.0",
|
||||
"debug": "^3.1.0",
|
||||
"extend": "^3.0.0",
|
||||
"form-data": "^2.3.1",
|
||||
"formidable": "^1.2.0",
|
||||
"methods": "^1.1.1",
|
||||
"mime": "^1.4.1",
|
||||
"qs": "^6.5.1",
|
||||
"readable-stream": "^2.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
|
||||
"dev": true
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"supertap": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supertap/-/supertap-1.0.0.tgz",
|
||||
|
@ -9363,22 +9298,78 @@
|
|||
}
|
||||
},
|
||||
"supertest": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/supertest/-/supertest-3.4.2.tgz",
|
||||
"integrity": "sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz",
|
||||
"integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"methods": "^1.1.2",
|
||||
"superagent": "^3.8.3"
|
||||
}
|
||||
},
|
||||
"supertest-session": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supertest-session/-/supertest-session-4.0.0.tgz",
|
||||
"integrity": "sha512-9d7KAL+K9hnnicov7USv/Nu1tSl40qSrOsB8zZHOEtfEzHaAoID6tbl1NeCVUg7SJreJMlNn+KJ88V7FW8gD6Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"object-assign": "^4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"superagent": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
|
||||
"integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"component-emitter": "^1.2.0",
|
||||
"cookiejar": "^2.1.0",
|
||||
"debug": "^3.1.0",
|
||||
"extend": "^3.0.0",
|
||||
"form-data": "^2.3.1",
|
||||
"formidable": "^1.2.0",
|
||||
"methods": "^1.1.1",
|
||||
"mime": "^1.4.1",
|
||||
"qs": "^6.5.1",
|
||||
"readable-stream": "^2.3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
|
|
|
@ -84,8 +84,7 @@
|
|||
"gulp-minify": "^3.1.0",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"less": "^3.10.3",
|
||||
"supertest": "^3.4.2",
|
||||
"supertest-session": "^4.0.0"
|
||||
"supertest": "^4.0.2"
|
||||
},
|
||||
"main": "app.js",
|
||||
"keywords": [
|
||||
|
|
|
@ -284,11 +284,11 @@ $(document).ready(function (){
|
|||
});
|
||||
|
||||
// Call to API to check if a permalink is available
|
||||
$(document).on('click', '#validate_permalink', function(e){
|
||||
$(document).on('click', '#validatePermalink', function(e){
|
||||
if($('#productPermalink').val() !== ''){
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/admin/api/validate_permalink',
|
||||
url: '/admin/validatePermalink',
|
||||
data: { permalink: $('#productPermalink').val(), docId: $('#productId').val() }
|
||||
})
|
||||
.done(function(msg){
|
||||
|
@ -404,10 +404,10 @@ $(document).ready(function (){
|
|||
});
|
||||
|
||||
// Call to API for a change to the published state of a product
|
||||
$('input[class="published_state"]').change(function(){
|
||||
$('input[class="publishedState"]').change(function(){
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/admin/product/published_state',
|
||||
url: '/admin/product/publishedState',
|
||||
data: { id: this.id, state: this.checked }
|
||||
})
|
||||
.done(function(msg){
|
||||
|
@ -544,6 +544,86 @@ $(document).ready(function (){
|
|||
}
|
||||
});
|
||||
|
||||
$('#discountNewForm').validator().on('submit', function(e){
|
||||
if(!e.isDefaultPrevented()){
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/admin/settings/discount/create',
|
||||
data: {
|
||||
code: $('#discountCode').val(),
|
||||
type: $('#discountType').val(),
|
||||
value: $('#discountValue').val(),
|
||||
start: $('#discountStart').val(),
|
||||
end: $('#discountEnd').val()
|
||||
}
|
||||
})
|
||||
.done(function(msg){
|
||||
showNotification(msg.message, 'success', false, '/admin/settings/discount/edit/' + msg.discountId);
|
||||
})
|
||||
.fail(function(msg){
|
||||
showNotification(msg.responseJSON.message, 'danger');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#discountEditForm').validator().on('submit', function(e){
|
||||
if(!e.isDefaultPrevented()){
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/admin/settings/discount/update',
|
||||
data: {
|
||||
discountId: $('#discountId').val(),
|
||||
code: $('#discountCode').val(),
|
||||
type: $('#discountType').val(),
|
||||
value: $('#discountValue').val(),
|
||||
start: $('#discountStart').val(),
|
||||
end: $('#discountEnd').val()
|
||||
}
|
||||
})
|
||||
.done(function(msg){
|
||||
showNotification(msg.message, 'success');
|
||||
})
|
||||
.fail(function(msg){
|
||||
showNotification(msg.responseJSON.message, 'danger');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#discountStart').datetimepicker({
|
||||
uiLibrary: 'bootstrap4',
|
||||
footer: true,
|
||||
modal: true,
|
||||
format: 'dd/mm/yyyy HH:MM',
|
||||
showOtherMonths: true
|
||||
});
|
||||
$('#discountEnd').datetimepicker({
|
||||
uiLibrary: 'bootstrap4',
|
||||
footer: true,
|
||||
modal: true,
|
||||
format: 'dd/mm/yyyy HH:MM'
|
||||
});
|
||||
|
||||
$(document).on('click', '#btnDiscountDelete', function(e){
|
||||
e.preventDefault();
|
||||
if(confirm('Are you sure?')){
|
||||
$.ajax({
|
||||
method: 'DELETE',
|
||||
url: '/admin/settings/discount/delete',
|
||||
data: {
|
||||
discountId: $(this).attr('data-id')
|
||||
}
|
||||
})
|
||||
.done(function(msg){
|
||||
showNotification(msg.message, 'success', true);
|
||||
})
|
||||
.fail(function(msg){
|
||||
showNotification(msg.message, 'danger', true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '#settings-menu-new', function(e){
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
|
@ -613,7 +693,7 @@ $(document).ready(function (){
|
|||
$.ajax({
|
||||
data: { order: menuOrder },
|
||||
type: 'POST',
|
||||
url: '/admin/settings/menu/save_order'
|
||||
url: '/admin/settings/menu/saveOrder'
|
||||
})
|
||||
.done(function(){
|
||||
showNotification('Menu order saved', 'success', true);
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -183,6 +183,38 @@ $(document).ready(function (){
|
|||
}
|
||||
});
|
||||
|
||||
$('#addDiscountCode').on('click', function(e){
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/checkout/adddiscountcode',
|
||||
data: {
|
||||
discountCode: $('#discountCode').val()
|
||||
}
|
||||
})
|
||||
.done(function(msg){
|
||||
showNotification(msg.message, 'success', true);
|
||||
})
|
||||
.fail(function(msg){
|
||||
showNotification(msg.responseJSON.message, 'danger');
|
||||
});
|
||||
});
|
||||
|
||||
$('#removeDiscountCode').on('click', function(e){
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/checkout/removediscountcode',
|
||||
data: {}
|
||||
})
|
||||
.done(function(msg){
|
||||
showNotification(msg.message, 'success', true);
|
||||
})
|
||||
.fail(function(msg){
|
||||
showNotification(msg.responseJSON.message, 'danger');
|
||||
});
|
||||
});
|
||||
|
||||
$('#loginForm').on('click', function(e){
|
||||
if(!e.isDefaultPrevented()){
|
||||
e.preventDefault();
|
||||
|
@ -513,6 +545,15 @@ function updateCartDiv(){
|
|||
shippingTotal = `<span id="shipping-amount">${session.shippingMessage}</span>`;
|
||||
}
|
||||
|
||||
var discountTotalAmt = numeral(session.totalCartDiscount).format('0.00');
|
||||
var discountTotal = '';
|
||||
if(session.totalCartDiscount > 0){
|
||||
discountTotal = `
|
||||
<div class="text-right">
|
||||
Discount: <strong id="discount-amount">${result.currencySymbol}${discountTotalAmt}</strong>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// If the cart has contents
|
||||
if(cart){
|
||||
$('#cart-empty').empty();
|
||||
|
@ -553,16 +594,16 @@ function updateCartDiv(){
|
|||
<div class="col-12 col-md-6 no-pad-left mb-2">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<button class="btn btn-outline-primary btn-qty-minus" type="button">-</button>
|
||||
<button class="btn btn-primary btn-qty-minus" type="button">-</button>
|
||||
</div>
|
||||
<input type="number" class="form-control cart-product-quantity text-center" id="${productId}-qty" data-id="${productId}" maxlength="2" value="${item.quantity}">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-primary btn-qty-add" type="button">+</button>
|
||||
<button class="btn btn-primary btn-qty-add" type="button">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 col-md-2 no-pad-left">
|
||||
<button class="btn btn-outline-danger btn-delete-from-cart" data-id="${productId}" type="button"><i class="far fa-trash-alt" data-id="${productId}" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-danger btn-delete-from-cart" data-id="${productId}" type="button"><i class="far fa-trash-alt" data-id="${productId}" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="col-8 col-md-4 align-self-center text-right">
|
||||
<strong class="my-auto">${result.currencySymbol}${productTotalAmount}</strong>
|
||||
|
@ -588,6 +629,7 @@ function updateCartDiv(){
|
|||
<div class="text-right">
|
||||
${shippingTotal}
|
||||
</div>
|
||||
${discountTotal}
|
||||
<div class="text-right">
|
||||
Total:
|
||||
<strong id="total-cart-amount">${result.currencySymbol}${totalAmount}</strong>
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -4,11 +4,6 @@
|
|||
background-color: #000000;
|
||||
border-color: #000000;
|
||||
}
|
||||
.btn-outline-danger {
|
||||
color: #ffffff !important;
|
||||
background-color: #cc4135;
|
||||
border-color: #cc4135;
|
||||
}
|
||||
.has-error input,
|
||||
.has-error textarea,
|
||||
.has-error div {
|
||||
|
|
|
@ -1 +1 @@
|
|||
.btn-outline-primary,.btn-warning{color:#fff!important;background-color:#000;border-color:#000}.btn-outline-danger{color:#fff!important;background-color:#cc4135;border-color:#cc4135}.has-error div,.has-error input,.has-error textarea{border-color:#cc4135}#frm_search,.search-bar-input,.search-bar-input .btn{padding-top:10px;height:45px}.productsWrapper{padding-right:10px;padding-left:10px}.searchBarWrapper{padding-right:0;padding-left:0}.footer{padding-top:20px}.product-price{padding-bottom:0}.navbarMenuWrapper{background-color:#f5f5f5}.navbarMenu>ul>li>a:hover{color:#cc4135!important}.navbarMenu{padding-right:0;padding-left:0}.product-wrapper>a:hover{color:#cc4135!important}#navbar,#navbar>.navbar-nav,#navbar>.navbar-nav>li>a,.navbar-header,.navbar-static-top{margin-bottom:0;height:100px!important}#navbar>.navbar-nav>li>a{padding-top:35px}.pagination>li>a{background-color:#cc4135!important}body .popover{display:none!important}.navbar-brand{color:#cc4135!important;letter-spacing:4px;padding-left:20px!important;padding-top:0!important;height:80px!important;font-size:55px!important}.navbar-brand,.navbar-brand-image{height:80px;padding-left:10px;padding-top:10px}.navbar-default .badge{background-color:#cc4135}#empty-cart:active,#empty-cart:active:hover,#empty-cart:focus,#empty-cart:hover,.pushy-link:active,.pushy-link:active:hover,.pushy-link:focus,.pushy-link:hover{border-color:#cc4135;background-color:#cc4135}.navActive>a{margin-bottom:0;padding-top:15px;border-bottom:5px solid #cc4135}#navbar,#navbar>.navbar-nav,#navbar>.navbar-nav>li>a,.navbar-header,.navbar-static-top{background-color:#fff}.navbar-default .navbar-nav>li>a{color:#838b8f;font-size:20px}.global-result-type{color:#8d8d8d}.global-result:hover{background-color:#007bff}.global-result:hover .global-result-detail,.global-result:hover .global-result-type,.global-result:hover .global-result-type .fal{color:#fff!important}.global-result a{text-decoration:none!important}.global-result:hover{cursor:pointer}.global-result:first-child{border-top-left-radius:0;border-top-right-radius:0}.global-result{border-left:0;border-right:0}.global-search-modal-content,.global-search-modal-header{background-color:transparent;border:none}#global-search-results{padding-right:0;border-bottom-left-radius:.3rem;border-bottom-right-radius:.3rem}.global-search-form{margin-bottom:0}#global-search-value{border-bottom-right-radius:0}.search-input-addon{border-bottom-left-radius:0}@media only screen and (max-width:768px){.navbar-default .navbar-brand{padding-top:10px}.navbar-default .navbar-nav>li>a{font-size:16px}.searchBarWrapper{padding-top:10px}.navbarMenuWrapper{padding-left:0;padding-right:0}.navbarMenuOuter{padding-left:0;padding-right:0}.navActive>a{color:#fff!important}.navbarMenu{padding-right:7.5px;padding-left:7.5px}.navActive>a{color:#fff!important;background-color:#cc4135;border-bottom:none}.footer{padding-top:10px}}
|
||||
.btn-outline-primary,.btn-warning{color:#fff!important;background-color:#000;border-color:#000}.has-error div,.has-error input,.has-error textarea{border-color:#cc4135}#frm_search,.search-bar-input,.search-bar-input .btn{padding-top:10px;height:45px}.productsWrapper{padding-right:10px;padding-left:10px}.searchBarWrapper{padding-right:0;padding-left:0}.footer{padding-top:20px}.product-price{padding-bottom:0}.navbarMenuWrapper{background-color:#f5f5f5}.navbarMenu>ul>li>a:hover{color:#cc4135!important}.navbarMenu{padding-right:0;padding-left:0}.product-wrapper>a:hover{color:#cc4135!important}#navbar,#navbar>.navbar-nav,#navbar>.navbar-nav>li>a,.navbar-header,.navbar-static-top{margin-bottom:0;height:100px!important}#navbar>.navbar-nav>li>a{padding-top:35px}.pagination>li>a{background-color:#cc4135!important}body .popover{display:none!important}.navbar-brand{color:#cc4135!important;letter-spacing:4px;padding-left:20px!important;padding-top:0!important;height:80px!important;font-size:55px!important}.navbar-brand,.navbar-brand-image{height:80px;padding-left:10px;padding-top:10px}.navbar-default .badge{background-color:#cc4135}#empty-cart:active,#empty-cart:active:hover,#empty-cart:focus,#empty-cart:hover,.pushy-link:active,.pushy-link:active:hover,.pushy-link:focus,.pushy-link:hover{border-color:#cc4135;background-color:#cc4135}.navActive>a{margin-bottom:0;padding-top:15px;border-bottom:5px solid #cc4135}#navbar,#navbar>.navbar-nav,#navbar>.navbar-nav>li>a,.navbar-header,.navbar-static-top{background-color:#fff}.navbar-default .navbar-nav>li>a{color:#838b8f;font-size:20px}.global-result-type{color:#8d8d8d}.global-result:hover{background-color:#007bff}.global-result:hover .global-result-detail,.global-result:hover .global-result-type,.global-result:hover .global-result-type .fal{color:#fff!important}.global-result a{text-decoration:none!important}.global-result:hover{cursor:pointer}.global-result:first-child{border-top-left-radius:0;border-top-right-radius:0}.global-result{border-left:0;border-right:0}.global-search-modal-content,.global-search-modal-header{background-color:transparent;border:none}#global-search-results{padding-right:0;border-bottom-left-radius:.3rem;border-bottom-right-radius:.3rem}.global-search-form{margin-bottom:0}#global-search-value{border-bottom-right-radius:0}.search-input-addon{border-bottom-left-radius:0}@media only screen and (max-width:768px){.navbar-default .navbar-brand{padding-top:10px}.navbar-default .navbar-nav>li>a{font-size:16px}.searchBarWrapper{padding-top:10px}.navbarMenuWrapper{padding-left:0;padding-right:0}.navbarMenuOuter{padding-left:0;padding-right:0}.navActive>a{color:#fff!important}.navbarMenu{padding-right:7.5px;padding-left:7.5px}.navActive>a{color:#fff!important;background-color:#cc4135;border-bottom:none}.footer{padding-top:10px}}
|
|
@ -10,12 +10,6 @@
|
|||
border-color: @btn-danger-color-border;
|
||||
}
|
||||
|
||||
.btn-outline-danger {
|
||||
color: @btn-danger-color-txt !important;
|
||||
background-color: @accent-color;
|
||||
border-color: @accent-color;
|
||||
}
|
||||
|
||||
.has-error input, .has-error textarea, .has-error div {
|
||||
border-color: @accent-color;
|
||||
}
|
||||
|
|
|
@ -490,26 +490,29 @@ input.form-control.customerDetails{
|
|||
color: @primary-btn-color;
|
||||
}
|
||||
|
||||
.btn-warning, .btn-outline-primary {
|
||||
.btn-warning, .btn-primary {
|
||||
color: #ffffff;
|
||||
background-color: @primary-btn-color;
|
||||
border-color: @primary-btn-color;
|
||||
}
|
||||
|
||||
.btn-warning:hover, .btn-outline-primary:hover {
|
||||
.btn-primary:hover,
|
||||
.btn-primary:active,
|
||||
.btn-primary.focus,
|
||||
.btn-primary:focus {
|
||||
color: #ffffff !important;
|
||||
background-color: @primary-btn-color !important;
|
||||
border-color: @primary-btn-color !important;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.btn-outline-danger{
|
||||
.btn-danger{
|
||||
color: #ffffff;
|
||||
background-color: @secondary-btn-color;
|
||||
border-color: @secondary-btn-color;
|
||||
}
|
||||
|
||||
.btn-outline-danger:hover{
|
||||
.btn-danger:hover{
|
||||
color: #ffffff !important;
|
||||
background-color: @secondary-btn-color !important;
|
||||
border-color: @secondary-btn-color !important;
|
||||
|
|
|
@ -391,24 +391,26 @@ input.form-control.customerDetails {
|
|||
color: #000000;
|
||||
}
|
||||
.btn-warning,
|
||||
.btn-outline-primary {
|
||||
.btn-primary {
|
||||
color: #ffffff;
|
||||
background-color: #000000;
|
||||
border-color: #000000;
|
||||
}
|
||||
.btn-warning:hover,
|
||||
.btn-outline-primary:hover {
|
||||
.btn-primary:hover,
|
||||
.btn-primary:active,
|
||||
.btn-primary.focus,
|
||||
.btn-primary:focus {
|
||||
color: #ffffff !important;
|
||||
background-color: #000000 !important;
|
||||
border-color: #000000 !important;
|
||||
opacity: 0.65;
|
||||
}
|
||||
.btn-outline-danger {
|
||||
.btn-danger {
|
||||
color: #ffffff;
|
||||
background-color: #cc3a2c;
|
||||
border-color: #cc3a2c;
|
||||
}
|
||||
.btn-outline-danger:hover {
|
||||
.btn-danger:hover {
|
||||
color: #ffffff !important;
|
||||
background-color: #cc3a2c !important;
|
||||
border-color: #cc3a2c !important;
|
||||
|
|
File diff suppressed because one or more lines are too long
168
routes/admin.js
168
routes/admin.js
|
@ -4,10 +4,12 @@ const { restrict, checkAccess } = require('../lib/auth');
|
|||
const escape = require('html-entities').AllHtmlEntities;
|
||||
const colors = require('colors');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const moment = require('moment');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const multer = require('multer');
|
||||
const mime = require('mime-type/with-db');
|
||||
const { validateJson } = require('../lib/schema');
|
||||
const ObjectId = require('mongodb').ObjectID;
|
||||
const router = express.Router();
|
||||
|
||||
|
@ -401,7 +403,7 @@ router.post('/admin/settings/menu/delete', restrict, checkAccess, (req, res) =>
|
|||
});
|
||||
|
||||
// We call this via a Ajax call to save the order from the sortable list
|
||||
router.post('/admin/settings/menu/save_order', restrict, checkAccess, (req, res) => {
|
||||
router.post('/admin/settings/menu/saveOrder', restrict, checkAccess, (req, res) => {
|
||||
const result = common.orderMenu(req, res);
|
||||
if(result === false){
|
||||
res.status(400).json({ message: 'Failed saving menu order' });
|
||||
|
@ -411,7 +413,7 @@ router.post('/admin/settings/menu/save_order', restrict, checkAccess, (req, res)
|
|||
});
|
||||
|
||||
// validate the permalink
|
||||
router.post('/admin/api/validate_permalink', async (req, res) => {
|
||||
router.post('/admin/validatePermalink', async (req, res) => {
|
||||
// if doc id is provided it checks for permalink in any products other that one provided,
|
||||
// else it just checks for any products with that permalink
|
||||
const db = req.app.db;
|
||||
|
@ -431,6 +433,168 @@ router.post('/admin/api/validate_permalink', async (req, res) => {
|
|||
res.status(200).json({ message: 'Permalink validated successfully' });
|
||||
});
|
||||
|
||||
// Discount codes
|
||||
router.get('/admin/settings/discounts', restrict, checkAccess, async (req, res) => {
|
||||
const db = req.app.db;
|
||||
|
||||
const discounts = await db.discounts.find({}).toArray();
|
||||
|
||||
res.render('settings-discounts', {
|
||||
title: 'Discount code',
|
||||
config: req.app.config,
|
||||
session: req.session,
|
||||
discounts,
|
||||
admin: true,
|
||||
message: common.clearSessionValue(req.session, 'message'),
|
||||
messageType: common.clearSessionValue(req.session, 'messageType'),
|
||||
helpers: req.handlebars.helpers
|
||||
});
|
||||
});
|
||||
|
||||
// Edit a discount code
|
||||
router.get('/admin/settings/discount/edit/:id', restrict, checkAccess, async (req, res) => {
|
||||
const db = req.app.db;
|
||||
|
||||
const discount = await db.discounts.findOne({ _id: common.getId(req.params.id) });
|
||||
|
||||
res.render('settings-discount-edit', {
|
||||
title: 'Discount code edit',
|
||||
session: req.session,
|
||||
admin: true,
|
||||
discount,
|
||||
message: common.clearSessionValue(req.session, 'message'),
|
||||
messageType: common.clearSessionValue(req.session, 'messageType'),
|
||||
helpers: req.handlebars.helpers,
|
||||
config: req.app.config
|
||||
});
|
||||
});
|
||||
|
||||
// Update discount code
|
||||
router.post('/admin/settings/discount/update', restrict, checkAccess, async (req, res) => {
|
||||
const db = req.app.db;
|
||||
|
||||
// Doc to insert
|
||||
const discountDoc = {
|
||||
discountId: req.body.discountId,
|
||||
code: req.body.code,
|
||||
type: req.body.type,
|
||||
value: parseInt(req.body.value),
|
||||
start: moment(req.body.start, 'DD/MM/YYYY HH:mm').toDate(),
|
||||
end: moment(req.body.end, 'DD/MM/YYYY HH:mm').toDate()
|
||||
};
|
||||
|
||||
// Validate the body again schema
|
||||
const schemaValidate = validateJson('editDiscount', discountDoc);
|
||||
if(!schemaValidate.result){
|
||||
res.status(400).json(schemaValidate.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check start is after today
|
||||
if(moment(discountDoc.start).isBefore(moment())){
|
||||
res.status(400).json({ message: 'Discount start date needs to be after today' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check end is after the start
|
||||
if(!moment(discountDoc.end).isAfter(moment(discountDoc.start))){
|
||||
res.status(400).json({ message: 'Discount end date needs to be after start date' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if code exists
|
||||
const checkCode = await db.discounts.countDocuments({
|
||||
code: discountDoc.code,
|
||||
_id: { $ne: common.getId(discountDoc.discountId) }
|
||||
});
|
||||
if(checkCode){
|
||||
res.status(400).json({ message: 'Discount code already exists' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove discountID
|
||||
delete discountDoc.discountId;
|
||||
|
||||
try{
|
||||
await db.discounts.updateOne({ _id: common.getId(req.body.discountId) }, { $set: discountDoc }, {});
|
||||
res.status(200).json({ message: 'Successfully saved', discount: discountDoc });
|
||||
}catch(ex){
|
||||
res.status(400).json({ message: 'Failed to save. Please try again' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create a discount code
|
||||
router.get('/admin/settings/discount/new', restrict, checkAccess, async (req, res) => {
|
||||
res.render('settings-discount-new', {
|
||||
title: 'Discount code create',
|
||||
session: req.session,
|
||||
admin: true,
|
||||
message: common.clearSessionValue(req.session, 'message'),
|
||||
messageType: common.clearSessionValue(req.session, 'messageType'),
|
||||
helpers: req.handlebars.helpers,
|
||||
config: req.app.config
|
||||
});
|
||||
});
|
||||
|
||||
// Create a discount code
|
||||
router.post('/admin/settings/discount/create', restrict, checkAccess, async (req, res) => {
|
||||
const db = req.app.db;
|
||||
|
||||
// Doc to insert
|
||||
const discountDoc = {
|
||||
code: req.body.code,
|
||||
type: req.body.type,
|
||||
value: parseInt(req.body.value),
|
||||
start: moment(req.body.start, 'DD/MM/YYYY HH:mm').toDate(),
|
||||
end: moment(req.body.end, 'DD/MM/YYYY HH:mm').toDate()
|
||||
};
|
||||
|
||||
// Validate the body again schema
|
||||
const schemaValidate = validateJson('newDiscount', discountDoc);
|
||||
if(!schemaValidate.result){
|
||||
res.status(400).json(schemaValidate.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if code exists
|
||||
const checkCode = await db.discounts.countDocuments({
|
||||
code: discountDoc.code
|
||||
});
|
||||
if(checkCode){
|
||||
res.status(400).json({ message: 'Discount code already exists' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check start is after today
|
||||
if(moment(discountDoc.start).isBefore(moment())){
|
||||
res.status(400).json({ message: 'Discount start date needs to be after today' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check end is after the start
|
||||
if(!moment(discountDoc.end).isAfter(moment(discountDoc.start))){
|
||||
res.status(400).json({ message: 'Discount end date needs to be after start date' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert discount code
|
||||
const discount = await db.discounts.insertOne(discountDoc);
|
||||
res.status(200).json({ message: 'Discount code created successfully', discountId: discount.insertedId });
|
||||
});
|
||||
|
||||
// Delete discount code
|
||||
router.delete('/admin/settings/discount/delete', restrict, checkAccess, async (req, res) => {
|
||||
const db = req.app.db;
|
||||
|
||||
try{
|
||||
await db.discounts.deleteOne({ _id: common.getId(req.body.discountId) }, {});
|
||||
res.status(200).json({ message: 'Discount code successfully deleted' });
|
||||
return;
|
||||
}catch(ex){
|
||||
res.status(400).json({ message: 'Error deleting discount code. Please try again.' });
|
||||
}
|
||||
});
|
||||
|
||||
// upload the file
|
||||
const upload = multer({ dest: 'public/uploads/' });
|
||||
router.post('/admin/file/upload', restrict, checkAccess, upload.single('uploadFile'), async (req, res) => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const colors = require('colors');
|
||||
const moment = require('moment');
|
||||
const _ = require('lodash');
|
||||
const {
|
||||
getId,
|
||||
|
@ -173,7 +174,7 @@ router.get('/checkout/cartdata', (req, res) => {
|
|||
});
|
||||
});
|
||||
|
||||
router.get('/checkout/payment', (req, res) => {
|
||||
router.get('/checkout/payment', async (req, res) => {
|
||||
const config = req.app.config;
|
||||
|
||||
// if there is no items in the cart then render a failure
|
||||
|
@ -189,6 +190,9 @@ router.get('/checkout/payment', (req, res) => {
|
|||
paymentType = '_subscription';
|
||||
}
|
||||
|
||||
// update total cart amount one last time before payment
|
||||
await updateTotalCart(req, res);
|
||||
|
||||
res.render(`${config.themeViews}checkout-payment`, {
|
||||
title: 'Checkout',
|
||||
config: req.app.config,
|
||||
|
@ -207,6 +211,84 @@ router.get('/checkout/payment', (req, res) => {
|
|||
});
|
||||
});
|
||||
|
||||
router.post('/checkout/adddiscountcode', async (req, res) => {
|
||||
const config = req.app.config;
|
||||
const db = req.app.db;
|
||||
|
||||
// if there is no items in the cart return a failure
|
||||
if(!req.session.cart){
|
||||
res.status(400).json({
|
||||
message: 'The are no items in your cart.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the discount module is loaded
|
||||
if(!config.modules.loaded.discount){
|
||||
res.status(400).json({
|
||||
message: 'Access denied.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check defined or null
|
||||
if(!req.body.discountCode || req.body.discountCode === ''){
|
||||
res.status(400).json({
|
||||
message: 'Discount code is invalid or expired'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate discount code
|
||||
const discount = await db.discounts.findOne({ code: req.body.discountCode });
|
||||
if(!discount){
|
||||
res.status(400).json({
|
||||
message: 'Discount code is invalid or expired'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate date validity
|
||||
if(!moment().isBetween(moment(discount.start), moment(discount.end))){
|
||||
res.status(400).json({
|
||||
message: 'Discount is expired'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the discount code
|
||||
req.session.discountCode = discount.code;
|
||||
|
||||
// Update the cart amount
|
||||
await updateTotalCart(req, res);
|
||||
|
||||
// Return the message
|
||||
res.status(200).json({
|
||||
message: 'Discount code applied'
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/checkout/removediscountcode', async (req, res) => {
|
||||
// if there is no items in the cart return a failure
|
||||
if(!req.session.cart){
|
||||
res.status(400).json({
|
||||
message: 'The are no items in your cart.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the discount code
|
||||
delete req.session.discountCode;
|
||||
|
||||
// update total cart amount
|
||||
await updateTotalCart(req, res);
|
||||
|
||||
// Return the message
|
||||
res.status(200).json({
|
||||
message: 'Discount code removed'
|
||||
});
|
||||
});
|
||||
|
||||
// show an individual product
|
||||
router.get('/product/:id', async (req, res) => {
|
||||
const db = req.app.db;
|
||||
|
@ -310,7 +392,7 @@ router.post('/product/updatecart', async (req, res, next) => {
|
|||
req.session.cart[cartItem.productId].totalItemPrice = productPrice * productQuantity;
|
||||
|
||||
// update total cart amount
|
||||
updateTotalCart(req, res);
|
||||
await updateTotalCart(req, res);
|
||||
|
||||
// Update checking cart for subscription
|
||||
updateSubscriptionCheck(req, res);
|
||||
|
@ -345,7 +427,7 @@ router.post('/product/removefromcart', async (req, res, next) => {
|
|||
$set: { cart: req.session.cart }
|
||||
});
|
||||
// update total cart
|
||||
updateTotalCart(req, res);
|
||||
await updateTotalCart(req, res);
|
||||
|
||||
// Update checking cart for subscription
|
||||
updateSubscriptionCheck(req, res);
|
||||
|
@ -489,7 +571,7 @@ router.post('/product/addtocart', async (req, res, next) => {
|
|||
}, { upsert: true });
|
||||
|
||||
// update total cart amount
|
||||
updateTotalCart(req, res);
|
||||
await updateTotalCart(req, res);
|
||||
|
||||
// Update checking cart for subscription
|
||||
updateSubscriptionCheck(req, res);
|
||||
|
|
|
@ -305,7 +305,7 @@ router.post('/admin/product/delete', restrict, checkAccess, async (req, res) =>
|
|||
});
|
||||
|
||||
// update the published state based on an ajax call from the frontend
|
||||
router.post('/admin/product/published_state', restrict, checkAccess, async (req, res) => {
|
||||
router.post('/admin/product/publishedState', restrict, checkAccess, async (req, res) => {
|
||||
const db = req.app.db;
|
||||
|
||||
try{
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const fs = require('fs');
|
||||
const _ = require('lodash');
|
||||
const session = require('supertest-session');
|
||||
const moment = require('moment');
|
||||
const supertest = require('supertest');
|
||||
const app = require('../app.js');
|
||||
const { newId } = require('../lib/common');
|
||||
const { runIndexing } = require('../lib/indexing');
|
||||
|
@ -14,6 +15,7 @@ const g = {
|
|||
db: {},
|
||||
config: {},
|
||||
products: {},
|
||||
discounts: {},
|
||||
customers: {},
|
||||
users: {},
|
||||
request: null,
|
||||
|
@ -26,20 +28,23 @@ const setup = (db) => {
|
|||
db.users.deleteMany({}, {}),
|
||||
db.customers.deleteMany({}, {}),
|
||||
db.products.deleteMany({}, {}),
|
||||
db.orders.deleteMany({}, {})
|
||||
db.discounts.deleteMany({}, {}),
|
||||
db.orders.deleteMany({}, {}),
|
||||
db.sessions.deleteMany({}, {})
|
||||
])
|
||||
.then(() => {
|
||||
return Promise.all([
|
||||
db.users.insertMany(addApiKey(jsonData.users)),
|
||||
db.customers.insertMany(jsonData.customers),
|
||||
db.products.insertMany(fixProductDates(jsonData.products))
|
||||
db.products.insertMany(fixProductDates(jsonData.products)),
|
||||
db.discounts.insertMany(fixDiscountDates(jsonData.discounts))
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
const runBefore = async () => {
|
||||
// Create a session
|
||||
g.request = session(app);
|
||||
g.request = supertest.agent(app);
|
||||
await new Promise(resolve => {
|
||||
app.on('appStarted', async () => {
|
||||
// Set some stuff now we have the app started
|
||||
|
@ -51,6 +56,7 @@ const runBefore = async () => {
|
|||
// Get some data from DB to use in compares
|
||||
g.products = await g.db.products.find({}).toArray();
|
||||
g.customers = await g.db.customers.find({}).toArray();
|
||||
g.discounts = await g.db.discounts.find({}).toArray();
|
||||
g.users = await g.db.users.find({}).toArray();
|
||||
|
||||
// Insert orders using product ID's
|
||||
|
@ -87,6 +93,36 @@ const fixProductDates = (products) => {
|
|||
return products;
|
||||
};
|
||||
|
||||
const fixDiscountDates = (discounts) => {
|
||||
let index = 0;
|
||||
discounts.forEach(() => {
|
||||
let startDate = moment().subtract(1, 'days').toDate();
|
||||
let endDate = moment().add(7, 'days').toDate();
|
||||
const expiredStart = moment().subtract(14, 'days').toDate();
|
||||
const expiredEnd = moment().subtract(7, 'days').toDate();
|
||||
const futureStart = moment().add(7, 'days').toDate();
|
||||
const futureEnd = moment().add(14, 'days').toDate();
|
||||
|
||||
// If code is expired, make sure the dates are correct
|
||||
if(discounts[index].code.substring(0, 7) === 'expired'){
|
||||
startDate = expiredStart;
|
||||
endDate = expiredEnd;
|
||||
}
|
||||
|
||||
// If code is future, make sure the dates are correct
|
||||
if(discounts[index].code.substring(0, 6) === 'future'){
|
||||
startDate = futureStart;
|
||||
endDate = futureEnd;
|
||||
}
|
||||
|
||||
// Set the expiry dates
|
||||
discounts[index].start = startDate;
|
||||
discounts[index].end = endDate;
|
||||
index++;
|
||||
});
|
||||
return discounts;
|
||||
};
|
||||
|
||||
const addApiKey = (users) => {
|
||||
let index = 0;
|
||||
users.forEach(() => {
|
||||
|
@ -100,5 +136,6 @@ module.exports = {
|
|||
runBefore,
|
||||
setup,
|
||||
g,
|
||||
fixProductDates
|
||||
fixProductDates,
|
||||
fixDiscountDates
|
||||
};
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
import{ serial as test }from'ava';
|
||||
const {
|
||||
runBefore,
|
||||
g
|
||||
} = require('../helper');
|
||||
const moment = require('moment');
|
||||
|
||||
test.before(async () => {
|
||||
await runBefore();
|
||||
});
|
||||
|
||||
test('[Success] Add valid amount discount', async t => {
|
||||
// Remove any sessions
|
||||
await g.db.sessions.deleteMany({}, {});
|
||||
|
||||
await g.request
|
||||
.post('/product/addtocart')
|
||||
.send({
|
||||
productId: g.products[0]._id,
|
||||
productQuantity: 1,
|
||||
productOptions: JSON.stringify(g.products[0].productOptions)
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const res = await g.request
|
||||
.post('/checkout/adddiscountcode')
|
||||
.send({
|
||||
discountCode: g.discounts[0].code
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
t.deepEqual(res.body.message, 'Discount code applied');
|
||||
|
||||
// Get our session
|
||||
const sessions = await g.db.sessions.find({}).toArray();
|
||||
if(!sessions || sessions.length === 0){
|
||||
t.fail();
|
||||
}
|
||||
|
||||
// Calculate what we expect
|
||||
const totalCartAmount = g.products[0].productPrice * 1;
|
||||
|
||||
const session = sessions[0].session;
|
||||
t.deepEqual(session.discountCode, g.discounts[0].code);
|
||||
t.deepEqual(session.totalCartDiscount, g.discounts[0].value);
|
||||
t.deepEqual(session.totalCartAmount, totalCartAmount - g.discounts[0].value);
|
||||
});
|
||||
|
||||
test('[Success] Add valid percent discount', async t => {
|
||||
// Remove any sessions
|
||||
await g.db.sessions.deleteMany({}, {});
|
||||
|
||||
await g.request
|
||||
.post('/product/addtocart')
|
||||
.send({
|
||||
productId: g.products[0]._id,
|
||||
productQuantity: 1,
|
||||
productOptions: JSON.stringify(g.products[0].productOptions)
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const res = await g.request
|
||||
.post('/checkout/adddiscountcode')
|
||||
.send({
|
||||
discountCode: g.discounts[1].code
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
t.deepEqual(res.body.message, 'Discount code applied');
|
||||
|
||||
// Get our session
|
||||
const sessions = await g.db.sessions.find({}).toArray();
|
||||
if(!sessions || sessions.length === 0){
|
||||
t.fail();
|
||||
}
|
||||
|
||||
// Calculate what we expect - percent
|
||||
const totalCartAmount = g.products[0].productPrice * 1;
|
||||
const expectedDiscount = (g.discounts[1].value / 100) * totalCartAmount;
|
||||
|
||||
const session = sessions[0].session;
|
||||
t.deepEqual(session.discountCode, g.discounts[1].code);
|
||||
t.deepEqual(session.totalCartAmount, totalCartAmount - expectedDiscount);
|
||||
t.deepEqual(session.totalCartDiscount, expectedDiscount);
|
||||
});
|
||||
|
||||
test('[Fail] Add an expired discount code', async t => {
|
||||
await g.request
|
||||
.post('/product/addtocart')
|
||||
.send({
|
||||
productId: g.products[0]._id,
|
||||
productQuantity: 1,
|
||||
productOptions: JSON.stringify(g.products[0].productOptions)
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const res = await g.request
|
||||
.post('/checkout/adddiscountcode')
|
||||
.send({
|
||||
discountCode: g.discounts[2].code
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
t.deepEqual(res.body.message, 'Discount is expired');
|
||||
});
|
||||
|
||||
test('[Fail] Add a future discount code', async t => {
|
||||
await g.request
|
||||
.post('/product/addtocart')
|
||||
.send({
|
||||
productId: g.products[0]._id,
|
||||
productQuantity: 1,
|
||||
productOptions: JSON.stringify(g.products[0].productOptions)
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const res = await g.request
|
||||
.post('/checkout/adddiscountcode')
|
||||
.send({
|
||||
discountCode: g.discounts[3].code
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
t.deepEqual(res.body.message, 'Discount is expired');
|
||||
});
|
||||
|
||||
test('[Fail] Add a bogus code', async t => {
|
||||
await g.request
|
||||
.post('/product/addtocart')
|
||||
.send({
|
||||
productId: g.products[0]._id,
|
||||
productQuantity: 1,
|
||||
productOptions: JSON.stringify(g.products[0].productOptions)
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const res = await g.request
|
||||
.post('/checkout/adddiscountcode')
|
||||
.send({
|
||||
discountCode: 'some_bogus_code'
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
t.deepEqual(res.body.message, 'Discount code is invalid or expired');
|
||||
});
|
||||
|
||||
test('[Success] Create a new discount code', async t => {
|
||||
// Add a discount code
|
||||
const res = await g.request
|
||||
.post('/admin/settings/discount/create')
|
||||
.send({
|
||||
code: 'TEST_CODE_5',
|
||||
type: 'amount',
|
||||
value: 10,
|
||||
start: moment().add(1, 'days').format('DD/MM/YYYY HH:mm'),
|
||||
end: moment().add(7, 'days').format('DD/MM/YYYY HH:mm')
|
||||
})
|
||||
.set('apiKey', g.users[0].apiKey)
|
||||
.expect(200);
|
||||
|
||||
t.deepEqual(res.body.message, 'Discount code created successfully');
|
||||
});
|
||||
|
||||
test('[Fail] Create a new discount code with invalid type', async t => {
|
||||
// Add a discount code
|
||||
const res = await g.request
|
||||
.post('/admin/settings/discount/create')
|
||||
.send({
|
||||
code: 'TEST_CODE_1',
|
||||
type: 'bogus_type',
|
||||
value: 10,
|
||||
start: moment().add(1, 'days').format('DD/MM/YYYY HH:mm'),
|
||||
end: moment().add(7, 'days').format('DD/MM/YYYY HH:mm')
|
||||
})
|
||||
.set('apiKey', g.users[0].apiKey)
|
||||
.expect(400);
|
||||
|
||||
t.deepEqual(res.body[0].message, 'should be equal to one of the allowed values');
|
||||
});
|
||||
|
||||
test('[Fail] Create a new discount code with existing code', async t => {
|
||||
// Add a discount code
|
||||
const res = await g.request
|
||||
.post('/admin/settings/discount/create')
|
||||
.send({
|
||||
code: 'valid_10_amount_code',
|
||||
type: 'amount',
|
||||
value: 10,
|
||||
start: moment().add(1, 'days').format('DD/MM/YYYY HH:mm'),
|
||||
end: moment().add(7, 'days').format('DD/MM/YYYY HH:mm')
|
||||
})
|
||||
.set('apiKey', g.users[0].apiKey)
|
||||
.expect(400);
|
||||
|
||||
t.deepEqual(res.body.message, 'Discount code already exists');
|
||||
});
|
||||
|
||||
test('[Success] Update a discount code', async t => {
|
||||
// Add a discount code
|
||||
const res = await g.request
|
||||
.post('/admin/settings/discount/update')
|
||||
.send({
|
||||
discountId: g.discounts[0]._id,
|
||||
code: 'TEST_CODE_99',
|
||||
type: 'amount',
|
||||
value: 20,
|
||||
start: moment().add(1, 'days').format('DD/MM/YYYY HH:mm'),
|
||||
end: moment().add(7, 'days').format('DD/MM/YYYY HH:mm')
|
||||
})
|
||||
.set('apiKey', g.users[0].apiKey)
|
||||
.expect(200);
|
||||
|
||||
t.deepEqual(res.body.discount.value, 20);
|
||||
t.deepEqual(res.body.message, 'Successfully saved');
|
||||
});
|
||||
|
||||
test('[Fail] Update a discount with same code as existing', async t => {
|
||||
// Add a discount code
|
||||
const res = await g.request
|
||||
.post('/admin/settings/discount/update')
|
||||
.send({
|
||||
discountId: g.discounts[1]._id,
|
||||
code: 'TEST_CODE_99',
|
||||
type: 'amount',
|
||||
value: 20,
|
||||
start: moment().add(1, 'days').format('DD/MM/YYYY HH:mm'),
|
||||
end: moment().add(7, 'days').format('DD/MM/YYYY HH:mm')
|
||||
})
|
||||
.set('apiKey', g.users[0].apiKey)
|
||||
.expect(400);
|
||||
|
||||
t.deepEqual(res.body.message, 'Discount code already exists');
|
||||
});
|
|
@ -22,7 +22,8 @@
|
|||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha256-L/W5Wfqfa0sdBNIKN9cG6QA5F2qx4qICmU2VgLruv9Y=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="/stylesheets/pushy{{config.env}}.css">
|
||||
{{#if admin}}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.20.2/codemirror.min.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/gijgo/1.9.13/combined/css/gijgo.min.css" integrity="sha256-bH0WSMuCFoG/dxeox/5aOWmaZl729yDg4ylckwSRTfU=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.20.2/codemirror.min.css" integrity="sha256-MdzaXfGXzZdeHw/XEV2LNNycipsLk4uZ0FYzO3hbuvI=" crossorigin="anonymous" />
|
||||
{{/if}}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css" integrity="sha256-+N4/V/SbAFiW1MPBCXnfnP9QSN3+Keu+NlB+0ev/YKQ=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-tokenfield/0.12.0/css/bootstrap-tokenfield.min.css" integrity="sha256-4qBzeX420hElp9/FzsuqUNqVobcClz1BjnXoxUDSYQ0=" crossorigin="anonymous" />
|
||||
|
@ -36,6 +37,8 @@
|
|||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js" integrity="sha256-x3YZWtRjM8bJqf48dFAv/qmgL68SI4jqNWeSLMZaMGA=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha256-WqU1JavFxSAMcLP2WIOI+GB2zWmShMI82mTpLDcqFUg=" crossorigin="anonymous"></script>
|
||||
{{#if admin}}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js" integrity="sha256-4iQZ6BVL4qNKlQ27TExEhBN1HFPvAvAMbFavKKosSWQ=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gijgo/1.9.13/combined/js/gijgo.min.js" integrity="sha256-YZhUu69bCn9uTqQyKwwQ3GyRypS7eaxp/wmVS282sDI=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.20.2/codemirror.min.js" integrity="sha256-K1exjHe1X4MP24jRizgBaSbUDUrNhFDRSwGoEYGmtJE=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.20.2/mode/css/css.min.js" integrity="sha256-D5oJ11cOmRhXSYWELwG2U/XYH3YveZJr9taRYLZ2DSM=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.20.2/mode/xml/xml.min.js" integrity="sha256-ERFGS58tayDq5kkyNwd/89iZZ+HglMH7eYXxG1hxTvA=" crossorigin="anonymous"></script>
|
||||
|
@ -100,7 +103,7 @@
|
|||
{{#unless admin}}
|
||||
{{#ifCond @root.config.enableLanguages "!=" false}}
|
||||
<div class="dropdown d-none d-sm-block">
|
||||
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas fa-globe-americas"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
|
@ -134,7 +137,7 @@
|
|||
<div id="cart" class="col-md-12 pad-left-12 top-pad-10 pushy pushy-right">
|
||||
<div class="row {{checkout}}">
|
||||
<div class="col-sm-12 text-right">
|
||||
<button class="pushy-link btn btn-outline-primary" type="button">X</button>
|
||||
<button class="pushy-link btn btn-primary" type="button">X</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
@ -142,8 +145,8 @@
|
|||
{{> (getTheme 'cart')}}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 {{showCartButtons @root.session.cart}} cart-buttons">
|
||||
<button class="btn btn-outline-danger float-left" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
|
||||
<a href="/checkout/information" class="btn btn-outline-primary float-right">Checkout</a>
|
||||
<button class="btn btn-danger float-left" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
|
||||
<a href="/checkout/information" class="btn btn-primary float-right">Checkout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -22,5 +22,8 @@
|
|||
<li class="list-group-item"><i class="fas fa-cog fa-icon"></i> <a href="/admin/settings">{{ @root.__ "General settings" }}</a></li>
|
||||
<li class="list-group-item"><i class="fas fa-bars fa-icon"></i> <a href="/admin/settings/menu">Menu</a></li>
|
||||
<li class="list-group-item"><i class="far fa-file fa-icon"></i> <a href="/admin/settings/pages">{{ @root.__ "Static pages" }}</a></li>
|
||||
{{#ifCond session.isAdmin '===' true}}
|
||||
<li class="list-group-item"><i class="fas fa-tag"></i> <a href="/admin/settings/discounts">{{ @root.__ "Discount codes" }}</a></li>
|
||||
{{/ifCond}}
|
||||
</ul>
|
||||
</div>
|
|
@ -56,7 +56,7 @@
|
|||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="productPermalink" placeholder="Permalink for the article" value={{result.productPermalink}}>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-success" id="validate_permalink" type="button">{{ @root.__ "Validate" }}</button>
|
||||
<button class="btn btn-outline-success" id="validatePermalink" type="button">{{ @root.__ "Validate" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">{{ @root.__ "This sets a readable URL for the product" }}</p>
|
||||
|
|
|
@ -1,139 +1,138 @@
|
|||
{{> partials/menu}}
|
||||
<form class="form-horizontal" id="productNewForm" data-toggle="validator">
|
||||
<div class="col-sm-12">
|
||||
<div class="page-header">
|
||||
<div class="float-right">
|
||||
<button id="frm_edit_product_save" class="btn btn-outline-success" type="submit">Add product <i class="far fa-plus-square"></i></button>
|
||||
<div class="col-sm-12">
|
||||
<div class="page-header">
|
||||
<div class="float-right">
|
||||
<button id="frm_edit_product_save" class="btn btn-outline-success" type="submit">Add product <i class="far fa-plus-square"></i></button>
|
||||
</div>
|
||||
<h2>{{ @root.__ "New product" }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="productTitle" class="control-label">{{ @root.__ "Product title" }} *</label>
|
||||
<input type="text" id="productTitle" class="form-control" minlength="5" maxlength="200" value="{{productTitle}}" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="productPrice" class="control-label">{{ @root.__ "Product price" }} *</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label class="input-group-text">{{currencySymbol config.currencySymbol}}</label>
|
||||
</div>
|
||||
<h2>{{ @root.__ "New product" }}</h2>
|
||||
<input type="number" id="productPrice" class="form-control" step="any" value="{{productPrice}}" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="productTitle" class="control-label">{{ @root.__ "Product title" }} *</label>
|
||||
<input type="text" id="productTitle" class="form-control" minlength="5" maxlength="200" value="{{productTitle}}" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="productPublished" class="control-label">{{ @root.__ "Status" }}</label>
|
||||
<select class="form-control" id="productPublished">
|
||||
<option value="true" selected>{{ @root.__ "Published" }}</option>
|
||||
<option value="false">{{ @root.__ "Draft" }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="productPrice" class="control-label">{{ @root.__ "Product price" }} *</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label class="input-group-text">{{currencySymbol config.currencySymbol}}</label>
|
||||
</div>
|
||||
{{#if config.trackStock}}
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="productStock" class="control-label">{{ @root.__ "Stock level" }}</label>
|
||||
<input type="number" id="productStock" class="form-control" value="{{productStock}}" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group" id="editor-wrapper">
|
||||
<label for="editor" class="control-label">{{ @root.__ "Product description" }} *</label>
|
||||
<textarea minlength="5" rows="10" id="productDescription" class="form-control" required>{{productDescription}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Permalink</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="productPermalink" placeholder="Permalink for the article" value={{productPermalink}}>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-success" id="validatePermalink" type="button">Validate</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">{{ @root.__ "This sets a readable URL for the product" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ @root.__ "Product options" }}</label>
|
||||
<input type="hidden" id="productOptions" value="{{result.productOptions}}" />
|
||||
<ul class="list-group" id="product_opt_wrapper">
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
<strong>{{ @root.__ "Name" }}:</strong>
|
||||
<input type="text" id="product_optName" class="form-control" placeholder="Size" />
|
||||
</div>
|
||||
<input type="number" id="productPrice" class="form-control" step="any" value="{{productPrice}}" required/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="productPublished" class="control-label">{{ @root.__ "Status" }}</label>
|
||||
<select class="form-control" id="productPublished">
|
||||
<option value="true" selected>{{ @root.__ "Published" }}</option>
|
||||
<option value="false">{{ @root.__ "Draft" }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{{#if config.trackStock}}
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="productStock" class="control-label">{{ @root.__ "Stock level" }}</label>
|
||||
<input type="number" id="productStock" class="form-control" value="{{productStock}}" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group" id="editor-wrapper">
|
||||
<label for="editor" class="control-label">{{ @root.__ "Product description" }} *</label>
|
||||
<textarea minlength="5" rows="10" id="productDescription" class="form-control" required>{{productDescription}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Permalink</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="productPermalink" placeholder="Permalink for the article" value={{productPermalink}}>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-success" id="validate_permalink" type="button">Validate</button>
|
||||
<div class="col-sm-2">
|
||||
<strong>{{ @root.__ "Label" }}:</strong>
|
||||
<input type="text" id="product_optLabel" class="form-control" placeholder="Select size"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">{{ @root.__ "This sets a readable URL for the product" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ @root.__ "Product options" }}</label>
|
||||
<input type="hidden" id="productOptions" value="{{result.productOptions}}" />
|
||||
<ul class="list-group" id="product_opt_wrapper">
|
||||
<div class="col-sm-2">
|
||||
<strong>{{ @root.__ "Type" }}:</strong>
|
||||
<select id="product_optType" class="form-control">
|
||||
<option value="select">Select</option>
|
||||
<option value="radio">Radio</option>
|
||||
<option value="checkbox">Checkbox</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<strong>{{ @root.__ "Options" }}:</strong>
|
||||
<input type="text" id="product_optOptions" class="form-control" placeholder="comma, seporated, list"/>
|
||||
</div>
|
||||
<div class="col-sm-2 text-right"></br>
|
||||
<button id="product_opt_add" class="btn btn-outline-success">{{ @root.__ "Add" }}</button>
|
||||
</div></div>
|
||||
</li>
|
||||
{{#each options}}
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
<strong>{{ @root.__ "Name" }}:</strong>
|
||||
<input type="text" id="product_optName" class="form-control" placeholder="Size" />
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<strong>{{ @root.__ "Label" }}:</strong>
|
||||
<input type="text" id="product_optLabel" class="form-control" placeholder="Select size"/>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<strong>{{ @root.__ "Type" }}:</strong>
|
||||
<select id="product_optType" class="form-control">
|
||||
<option value="select">Select</option>
|
||||
<option value="radio">Radio</option>
|
||||
<option value="checkbox">Checkbox</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<strong>{{ @root.__ "Options" }}:</strong>
|
||||
<input type="text" id="product_optOptions" class="form-control" placeholder="comma, seporated, list"/>
|
||||
</div>
|
||||
<div class="col-sm-2 text-right"></br>
|
||||
<button id="product_opt_add" class="btn btn-outline-success">{{ @root.__ "Add" }}</button>
|
||||
</div></div>
|
||||
</li>
|
||||
{{#each options}}
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class='col-sm-2 opt-name'>{{this.optName}}</div>
|
||||
<div class='col-sm-2'>{{this.optLabel}}</div>
|
||||
<div class='col-sm-2'>{{this.optType}}</div>
|
||||
<div class='col-sm-4'>{{{this.optOptions}}}</div>
|
||||
<div class='col-sm-2 text-right'>
|
||||
<button class='product_opt_remove btn btn-outline-danger'>{{ @root.__ "Remove" }}</button>
|
||||
</div>
|
||||
<div class='col-sm-2 opt-name'>{{this.optName}}</div>
|
||||
<div class='col-sm-2'>{{this.optLabel}}</div>
|
||||
<div class='col-sm-2'>{{this.optType}}</div>
|
||||
<div class='col-sm-4'>{{{this.optOptions}}}</div>
|
||||
<div class='col-sm-2 text-right'>
|
||||
<button class='product_opt_remove btn btn-outline-danger'>{{ @root.__ "Remove" }}</button>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<p class="help-block">{{ @root.__ "Here you can set options for your product. Eg: Size, color, style" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<p class="help-block">{{ @root.__ "Here you can set options for your product. Eg: Size, color, style" }}</p>
|
||||
</div>
|
||||
{{#ifCond config.paymentGateway '==' 'stripe'}}
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Subscription plan</label>
|
||||
<input type="text" class="form-control" id="productSubscription" placeholder="plan_XXXXXXXXXXXXXX">
|
||||
<p class="help-block">First setup the plan in <strong>Stripe</strong> dashboard and enter the Plan ID. Format: plan_XXXXXXXXXXXXXX</p>
|
||||
</div>
|
||||
</div>
|
||||
{{#ifCond config.paymentGateway '==' 'stripe'}}
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Subscription plan</label>
|
||||
<input type="text" class="form-control" id="productSubscription" placeholder="plan_XXXXXXXXXXXXXX">
|
||||
<p class="help-block">First setup the plan in <strong>Stripe</strong> dashboard and enter the Plan ID. Format: plan_XXXXXXXXXXXXXX</p>
|
||||
</div>
|
||||
{{/ifCond}}
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="productComment" class="control-label">{{ @root.__ "Allow comment" }}</label>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input class="productComment" type="checkbox" {{checkedState result.productComment}} id="productComment">
|
||||
</label>
|
||||
</div>
|
||||
<p class="help-block">{{ @root.__ "Allow free form comments when adding products to cart" }}</p>
|
||||
</div>
|
||||
{{/ifCond}}
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="productComment" class="control-label">{{ @root.__ "Allow comment" }}</label>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input class="productComment" type="checkbox" {{checkedState result.productComment}} id="productComment">
|
||||
</label>
|
||||
</div>
|
||||
<p class="help-block">{{ @root.__ "Allow free form comments when adding products to cart" }}</p>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="productTags" class="control-label">Product tag words</label>
|
||||
<input type="text" class="form-control" id="productTags">
|
||||
<p class="help-block">{{ @root.__ "Tag words used to indexed products, making them easier to find and filter." }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="productTags" class="control-label">Product tag words</label>
|
||||
<input type="text" class="form-control" id="productTags">
|
||||
<p class="help-block">{{ @root.__ "Tag words used to indexed products, making them easier to find and filter." }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
{{#each results}}
|
||||
<li class="list-group-item">
|
||||
<button class="float-right btn text-danger btn-delete-product" data-id="{{this._id}}"> <i class="far fa-trash-alt"></i></button>
|
||||
<h4 class="float-right"><input id="{{this._id}}" class="published_state" type="checkbox" {{checkedState this.productPublished}}></h4>
|
||||
<h4 class="float-right"><input id="{{this._id}}" class="publishedState" type="checkbox" {{checkedState this.productPublished}}></h4>
|
||||
<div class="top-pad-8"><a href="/admin/product/edit/{{this._id}}">{{this.productTitle}}</a></div>
|
||||
</li>
|
||||
{{/each}}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
{{> partials/menu}}
|
||||
<form class="form-horizontal col-sm-7" id="discountEditForm" data-toggle="validator">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="page-header">
|
||||
<div class="float-right">
|
||||
<button id="" class="btn btn-outline-success" type="submit">Update discount</button>
|
||||
</div>
|
||||
<h2>{{ @root.__ "Edit discount" }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="form-group">
|
||||
<label for="discountCode" class="control-label">{{ @root.__ "Discount code" }} *</label>
|
||||
<input type="text" id="discountCode" class="form-control" minlength="1" maxlength="50" value="{{discount.code}}" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="discountType" class="control-label">{{ @root.__ "Discount type" }} *</label>
|
||||
<select class="form-control" id="discountType">
|
||||
<option value="amount" {{selectState discount.type "amount"}}>{{ @root.__ "Amount" }}</option>
|
||||
<option value="percent" {{selectState discount.type "percent"}}>{{ @root.__ "Percent" }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="discountValue" class="control-label">{{ @root.__ "Discount value" }} *</label>
|
||||
<input type="number" id="discountValue" class="form-control" minlength="1" maxlength="50" value="{{discount.value}}" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="discountStart" class="control-label">{{ @root.__ "Discount start" }} *</label>
|
||||
<input id="discountStart" value="{{formatDate discount.start 'DD/MM/YYYY kk:mm'}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="discountEnd" class="control-label">{{ @root.__ "Discount end" }} *</label>
|
||||
<input id="discountEnd" value="{{formatDate discount.end 'DD/MM/YYYY kk:mm'}}" />
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="discountId" value="{{discount._id}}">
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,46 @@
|
|||
{{> partials/menu}}
|
||||
<form class="form-horizontal col-sm-7" id="discountNewForm" data-toggle="validator">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="page-header">
|
||||
<div class="float-right">
|
||||
<button id="" class="btn btn-outline-success" type="submit">Add discount</button>
|
||||
</div>
|
||||
<h2>{{ @root.__ "New discount" }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="form-group">
|
||||
<label for="discountCode" class="control-label">{{ @root.__ "Discount code" }} *</label>
|
||||
<input type="text" id="discountCode" class="form-control" minlength="1" maxlength="50" placeholder="CODE20" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="discountType" class="control-label">{{ @root.__ "Discount type" }} *</label>
|
||||
<select class="form-control" id="discountType">
|
||||
<option value="amount" selected>{{ @root.__ "Amount" }}</option>
|
||||
<option value="percent">{{ @root.__ "Percent" }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="discountValue" class="control-label">{{ @root.__ "Discount value" }} *</label>
|
||||
<input type="number" id="discountValue" class="form-control" minlength="1" maxlength="50" placeholder="20" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="discountStart" class="control-label">{{ @root.__ "Discount start" }} *</label>
|
||||
<input id="discountStart" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-group">
|
||||
<label for="discountEnd" class="control-label">{{ @root.__ "Discount end" }} *</label>
|
||||
<input id="discountEnd" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,39 @@
|
|||
{{> partials/menu}}
|
||||
<div class="col-sm-9">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2 class="clearfix">{{ @root.__ "Discount codes" }} <div class="float-right"><a href="/admin/settings/discount/new" class="btn btn-outline-success">{{ @root.__ "New Discount" }}</a></div></h2>
|
||||
{{#if discounts}}
|
||||
<ul class="list-group">
|
||||
{{#each discounts}}
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-4 mt-2">
|
||||
<span><strong>{{ @root.__ "Code" }}:</strong> {{this.code}}</span>
|
||||
</div>
|
||||
<div class="col-2 mt-2">
|
||||
<span><strong>{{ @root.__ "Type" }}:</strong> {{this.type}}</span>
|
||||
</div>
|
||||
<div class="col-3 mt-2">
|
||||
<span><strong>{{ @root.__ "Status" }}:</strong>
|
||||
{{#ifCond (discountExpiry this.start this.end) '===' true}}
|
||||
<span class="text-success">{{ @root.__ "Running" }}</span>
|
||||
{{else}}
|
||||
<span class="text-danger">{{ @root.__ "Not running" }}</span>
|
||||
{{/ifCond}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-3 text-right">
|
||||
<a class="btn btn-outline-success" href="/admin/settings/discount/edit/{{this._id}}">{{ @root.__ "Edit" }}</a>
|
||||
<button class="btn btn-outline-danger" id="btnDiscountDelete" data-id="{{this._id}}">{{ @root.__ "Delete" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<h4 class="text-warning text-center">{{ @root.__ "There are currently no discount codes setup." }}</h4>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -29,16 +29,16 @@
|
|||
<div class="col-12 col-md-6 no-pad-left mb-2">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<button class="btn btn-outline-primary btn-qty-minus" type="button">-</button>
|
||||
<button class="btn btn-primary btn-qty-minus" type="button">-</button>
|
||||
</div>
|
||||
<input type="number" class="form-control cart-product-quantity text-center" id="{{../this.productId}}-qty" data-id="{{../this.productId}}" maxlength="2" value="{{../this.quantity}}">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-primary btn-qty-add" type="button">+</button>
|
||||
<button class="btn btn-primary btn-qty-add" type="button">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 col-md-2 no-pad-left">
|
||||
<button class="btn btn-outline-danger btn-delete-from-cart" data-id="{{../this.productId}}" type="button"><i class="far fa-trash-alt" data-id="{{../this.productId}}" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-danger btn-delete-from-cart" data-id="{{../this.productId}}" type="button"><i class="far fa-trash-alt" data-id="{{../this.productId}}" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="col-12 col-md-8 no-pad-left mb-2"></div>
|
||||
|
@ -66,6 +66,11 @@
|
|||
<span id="shipping-amount">{{@root.session.shippingMessage}}</span>
|
||||
</div>
|
||||
{{/ifCond}}
|
||||
{{#ifCond @root.session.totalCartDiscount '>' 0}}
|
||||
<div class="text-right">
|
||||
Discount: <strong id="discount-amount">{{currencySymbol @root.config.currencySymbol}}{{formatAmount @root.session.totalCartDiscount}}</strong>
|
||||
</div>
|
||||
{{/ifCond}}
|
||||
<div class="text-right">
|
||||
Total:
|
||||
<strong id="total-cart-amount">{{currencySymbol @root.config.currencySymbol}}{{formatAmount @root.session.totalCartAmount}}</strong>
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
{{> (getTheme 'cart')}}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 {{showCartButtons @root.session.cart}} cart-buttons">
|
||||
<button class="btn btn-outline-danger float-left" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
|
||||
<a href="/checkout/information" class="btn btn-outline-danger float-right">Checkout</a>
|
||||
<button class="btn btn-danger float-left" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
|
||||
<a href="/checkout/information" class="btn btn-danger float-right">Checkout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -36,10 +36,10 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<a href="/customer/forgotten"
|
||||
class="btn btn-outline-primary float-left">{{ @root.__ "Forgotten" }}</a>
|
||||
class="btn btn-primary float-left">{{ @root.__ "Forgotten" }}</a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button id="customerLogin" class="btn btn-outline-primary float-right"
|
||||
<button id="customerLogin" class="btn btn-primary float-right"
|
||||
type="submit">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -49,7 +49,7 @@
|
|||
<div class="row bottom-marg-15">
|
||||
<div class="col-sm-12">
|
||||
<button id="customerLogout"
|
||||
class="btn btn-outline-primary float-right">{{ @root.__ "Change customer" }}</button>
|
||||
class="btn btn-primary float-right">{{ @root.__ "Change customer" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -76,8 +76,8 @@
|
|||
</div>
|
||||
{{/unless}}
|
||||
<div class="col-sm-12">
|
||||
<a href="/checkout/cart" class="btn btn-outline-primary float-left">Return to cart</a>
|
||||
<a href="/checkout/shipping" id="checkoutInformation" class="btn btn-outline-primary float-right">Continue to shipping</a>
|
||||
<a href="/checkout/cart" class="btn btn-primary float-left">Return to cart</a>
|
||||
<a href="/checkout/shipping" id="checkoutInformation" class="btn btn-primary float-right">Continue to shipping</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -89,7 +89,7 @@
|
|||
{{#if @root.session.cart}}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 {{showCartButtons @root.session.cart}} cart-buttons">
|
||||
<button class="btn btn-outline-danger float-right" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
|
||||
<button class="btn btn-danger float-right" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -39,6 +39,17 @@
|
|||
<li class="list-group-item">FREE shipping <span class="float-right"><a href="/checkout/shipping">Change</a></span></li>
|
||||
{{/ifCond}}
|
||||
</ul>
|
||||
{{#if @root.config.modules.loaded.discount}}
|
||||
<div class="input-group bottom-pad-15">
|
||||
<input class="form-control" id="discountCode" type="search" placeholder="{{ @root.__ "Discount code" }}" value="{{@root.session.discountCode}}">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-success" id="addDiscountCode">{{ @root.__ "Apply" }}</button>
|
||||
</div>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-danger" id="removeDiscountCode"><i class="fa fa-times" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<form id="shipping-form" class="shipping-form" action="/{{config.paymentGateway}}/checkout_action{{@root.paymentType}}" method="post" role="form" data-toggle="validator" novalidate="false">
|
||||
{{#if session.customerPresent}}
|
||||
{{#ifCond config.paymentGateway '==' 'paypal'}}
|
||||
|
|
|
@ -23,14 +23,14 @@
|
|||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
<a href="/checkout/information" class="btn btn-outline-primary float-left">{{ @root.__ "Return to information" }}</a>
|
||||
<a href="/checkout/payment" class="btn btn-outline-primary float-right">{{ @root.__ "Proceed to payment" }}</a>
|
||||
<a href="/checkout/information" class="btn btn-primary float-left">{{ @root.__ "Return to information" }}</a>
|
||||
<a href="/checkout/payment" class="btn btn-primary float-right">{{ @root.__ "Proceed to payment" }}</a>
|
||||
</div>
|
||||
<div id="cart" class="col-md-7 d-none d-sm-block">
|
||||
{{> (getTheme 'cart')}}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 {{showCartButtons @root.session.cart}} cart-buttons">
|
||||
<button class="btn btn-outline-danger float-right" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
|
||||
<button class="btn btn-danger float-right" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
{{currencySymbol ../config.currencySymbol}}{{formatAmount productPrice}}
|
||||
</h3>
|
||||
<p class="text-center">
|
||||
<a class="btn btn-outline-primary add-to-cart" data-id="{{this._id}}" data-link="{{this.productPermalink}}" data-has-options="{{checkProductOptions this.productOptions}}" role="button">{{ @root.__ "Add to cart" }}</a>
|
||||
<a class="btn btn-primary add-to-cart" data-id="{{this._id}}" data-link="{{this.productPermalink}}" data-has-options="{{checkProductOptions this.productOptions}}" role="button">{{ @root.__ "Add to cart" }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -74,11 +74,11 @@
|
|||
<p class="product-option-text">{{ @root.__ "Quantity" }}</p>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<button class="btn btn-outline-primary qty-btn-minus" type="button">-</button>
|
||||
<button class="btn btn-primary qty-btn-minus" type="button">-</button>
|
||||
</div>
|
||||
<input type="number" class="form-control add-color text-center" id="product_quantity" maxlength="3" value="1">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-primary qty-btn-plus" type="button">+</button>
|
||||
<button class="btn btn-primary qty-btn-plus" type="button">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -89,7 +89,7 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
<div class="col-md-10 btnAddToCart">
|
||||
<button class="btn btn-outline-primary btn-block product-add-to-cart" type="button">{{ @root.__ "Add to cart" }}</button>
|
||||
<button class="btn btn-primary btn-block product-add-to-cart" type="button">{{ @root.__ "Add to cart" }}</button>
|
||||
</div>
|
||||
<div class="col-md-10 body_text">
|
||||
{{{productDescription}}}
|
||||
|
|
Loading…
Reference in New Issue