Redesigned the querying for the sale duplicates screen to use aggregation; Finished the styling of the sale duplicate screen; Tested the functionality of sale duplicates; Added a way to show hidden (ignored) duplicates.
This commit is contained in:
44
imports/api/Logs.js
Normal file
44
imports/api/Logs.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Mongo } from 'meteor/mongo';
|
||||
|
||||
// The logging tool is primarily for managing administrative functions such that administrators can view the app logs and issue commands that might generate administrative logging.
|
||||
|
||||
Meteor.log = new Logger();
|
||||
Logs = new Mongo.Collection('Logs');
|
||||
|
||||
let logMongo = new LoggerMongo(Meteor.log, {
|
||||
collection: Logs
|
||||
});
|
||||
logMongo.enable({
|
||||
enable: true,
|
||||
client: false, /* Client calls are not executed on the client. */
|
||||
server: true /* Calls from the client will be executed on the server. */
|
||||
});
|
||||
|
||||
if(Meteor.isServer) {
|
||||
Logs._ensureIndex({'date': 1}, {expireAfterSeconds: 86400});
|
||||
Meteor.publish('logs', function() {
|
||||
return Logs.find({}, {limit: 10000});
|
||||
});
|
||||
Meteor.methods({
|
||||
clearLogs: function() {
|
||||
return Logs.remove({}, function(err) {
|
||||
if(err) Meteor.log.error(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Logs.allow({
|
||||
insert: () => false,
|
||||
update: () => false,
|
||||
remove: () => false
|
||||
});
|
||||
|
||||
Logs.deny({
|
||||
insert: () => true,
|
||||
update: () => true,
|
||||
remove: () => true
|
||||
});
|
||||
|
||||
export default Logs;
|
||||
@@ -3,6 +3,13 @@ import { Mongo } from 'meteor/mongo';
|
||||
import { check } from 'meteor/check';
|
||||
import {SimpleSchema} from 'meteor/aldeed:simple-schema';
|
||||
|
||||
/**
|
||||
* Notes:
|
||||
* The Product object has a prices field which is an object whose fields names are Measure ID's. Each field value (for each Measure ID) is an object that has a 'price', 'effectiveDate', and 'previousPrice'.
|
||||
* The effectiveDate field stores the date as a number in the format YYYYMMDD. Converting this number into a local date is done with moment(sale.date.toString(), "YYYYMMDD").toDate(), and converting it to a number from a date can be accomplished with ~~(moment(date).format("YYYYMMDD")), where the ~~ is a bitwise not and converts a string to a number quickly and reliably.
|
||||
* Because the structure of the Product object is so complicated, the normal checking that is done by the framework cannot be used.
|
||||
*/
|
||||
|
||||
Products = new Mongo.Collection('Products');
|
||||
|
||||
const ProductsSchema = new SimpleSchema({
|
||||
@@ -237,7 +244,7 @@ if(Meteor.isServer) {
|
||||
check(measureId, String);
|
||||
check(price, Number);
|
||||
if(setPrevious) check(setPrevious, Boolean);
|
||||
if(effectiveDate) check(effectiveDate, Date);
|
||||
if(effectiveDate) check(effectiveDate, Number); // TODO: Check that the format is YYYYMMDD
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
let products = Products.find({_id: {$in: productIds}}, {fields: {prices: 1}}).fetch();
|
||||
|
||||
@@ -3,10 +3,15 @@ import { Mongo } from 'meteor/mongo';
|
||||
import { check } from 'meteor/check';
|
||||
import {SimpleSchema} from 'meteor/aldeed:simple-schema';
|
||||
|
||||
/**
|
||||
* Notes:
|
||||
* The Sale object has a date field which stores the date as a number in the format YYYYMMDD. Converting this number into a local date is done with moment(sale.date.toString(), "YYYYMMDD").toDate(), and converting it to a number from a date can be accomplished with ~~(moment(date).format("YYYYMMDD")), where the ~~ is a bitwise not and converts a string to a number quickly and reliably.
|
||||
*/
|
||||
|
||||
Sales = new Mongo.Collection('Sales');
|
||||
let SalesSchema = new SimpleSchema({
|
||||
date: {
|
||||
type: Date,
|
||||
type: Number, // A number in the format of YYYYMMDD to allow for searching using greater and less than, and to prevent timezones from messing everything up.
|
||||
label: "Date",
|
||||
optional: false,
|
||||
index: 1
|
||||
@@ -58,6 +63,19 @@ let SalesSchema = new SimpleSchema({
|
||||
trim: false,
|
||||
optional: true
|
||||
},
|
||||
ignoreDuplicates: {
|
||||
type: Boolean,
|
||||
optional: true
|
||||
},
|
||||
isDuplicateOf: {
|
||||
type: String,
|
||||
trim: false,
|
||||
optional: true
|
||||
},
|
||||
duplicateCount: {
|
||||
type: Number,
|
||||
optional: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
label: "Created On",
|
||||
@@ -71,14 +89,22 @@ if(Meteor.isServer) {
|
||||
let dbQuery = [];
|
||||
|
||||
if(query) {
|
||||
// _.each(_.keys(query), function(key) {
|
||||
// if(_.isObject(query[key])) dbQuery[key] = query[key];
|
||||
// else if(_.isNumber(query[key])) dbQuery[key] = query[key];
|
||||
// else dbQuery[key] = {$regex: RegExp.escape(query[key]), $options: 'i'};
|
||||
// });
|
||||
|
||||
_.each(_.keys(query), function(key) {
|
||||
if(_.isObject(query[key])) dbQuery.push({[key]: query[key]});
|
||||
//if(_.isObject(query[key])) dbQuery.push({[key]: query[key]});
|
||||
if(_.isObject(query[key])) {
|
||||
if(query[key].type === 'dateRange') {
|
||||
if(query[key].start && query[key].end)
|
||||
dbQuery.push({[key]: {$gte: query[key].start, $lte: query[key].end}});
|
||||
else if(query[key].start)
|
||||
dbQuery.push({[key]: {$gte: query[key].start}});
|
||||
else if(query[key].end)
|
||||
dbQuery.push({[key]: {$lte: query[key].end}});
|
||||
// Do nothing if a start and/or end are not provided.
|
||||
}
|
||||
else {
|
||||
dbQuery.push({[key]: query[key]});
|
||||
}
|
||||
}
|
||||
else if(_.isNumber(query[key])) dbQuery.push({[key]: query[key]});
|
||||
else {
|
||||
let searchValue = query[key];
|
||||
@@ -97,6 +123,65 @@ if(Meteor.isServer) {
|
||||
dbQuery = dbQuery.length > 0 ? {$and: dbQuery} : {};
|
||||
return Meteor.collections.Sales.find(dbQuery, {limit: limit, sort, skip: skipCount});
|
||||
});
|
||||
Meteor.publish('duplicateSales', function(query, includeIgnoredDuplicates) {
|
||||
// Start with the duplicate count needing to be greater than zero, and with duplicates marked as ignored not included.
|
||||
let dbQuery = [{duplicateCount: {$gt: 0}}];
|
||||
|
||||
// If we should include ignored duplicates than add it to the query as a requirement.
|
||||
if(!includeIgnoredDuplicates) {
|
||||
dbQuery.push({$or: [{ignoreDuplicates: {$exists: false}}, {ignoreDuplicates: false}]});
|
||||
}
|
||||
|
||||
//if(query) {
|
||||
// // Add each query requirement sent by the client.
|
||||
// _.each(_.keys(query), function(key) {
|
||||
// //if(_.isObject(query[key])) dbQuery.push({[key]: query[key]});
|
||||
// if(_.isObject(query[key])) {
|
||||
// if(query[key].type === 'dateRange') {
|
||||
// if(query[key].start && query[key].end)
|
||||
// dbQuery.push({[key]: {$gte: query[key].start, $lte: query[key].end}});
|
||||
// else if(query[key].start)
|
||||
// dbQuery.push({[key]: {$gte: query[key].start}});
|
||||
// else if(query[key].end)
|
||||
// dbQuery.push({[key]: {$lte: query[key].end}});
|
||||
// // Do nothing if a start and/or end are not provided.
|
||||
// }
|
||||
// else {
|
||||
// dbQuery.push({[key]: query[key]});
|
||||
// }
|
||||
// }
|
||||
// else if(_.isNumber(query[key])) dbQuery.push({[key]: query[key]});
|
||||
// else {
|
||||
// let searchValue = query[key];
|
||||
// let searches = searchValue && searchValue.length > 0 ? searchValue.split(/\s+/) : undefined;
|
||||
//
|
||||
// for(let search of searches) {
|
||||
// dbQuery.push({[key]: {$regex: '\\b' + search, $options: 'i'}});
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
|
||||
// Wrap the array of requirements with an $and, or remove the single requirement from the array (if there is only a single requirement).
|
||||
if(dbQuery.length === 1) dbQuery = dbQuery[0];
|
||||
else dbQuery = {$and: dbQuery};
|
||||
|
||||
// Find all Sale objects marked as having at least one duplicate.
|
||||
//return Meteor.collections.Sales.find(dbQuery);
|
||||
|
||||
let pipeline = [
|
||||
{$match: dbQuery},
|
||||
{$lookup: {from: "Products", localField: "productId", foreignField: "_id", as: "product"}},
|
||||
{$lookup: {from: "Measures", localField: "measureId", foreignField: "_id", as: "measure"}},
|
||||
{$lookup: {from: "Venues", localField: "venueId", foreignField: "_id", as: "venue"}},
|
||||
{$unwind: "$product"},
|
||||
{$unwind: "$measure"},
|
||||
{$unwind: "$venue"},
|
||||
{$project: {_id: 1, date: 1, amount: 1, price: 1, venueId: 1, productId: 1, measureId: 1, duplicateCount: 1, ignoreDuplicates: 1, 'productName': '$product.name', 'measureName': '$measure.name', 'venueName': '$venue.name'}}
|
||||
];
|
||||
|
||||
ReactiveAggregate(this, Sales, pipeline, {clientCollection: 'duplicateSales'});
|
||||
});
|
||||
// time: expects either undefined, 'weekly', or 'monthly'
|
||||
// options: expects either undefined, 'markets', or 'types'
|
||||
Meteor.publish('salesTotals', function(time, options) {
|
||||
@@ -185,7 +270,7 @@ if(Meteor.isServer) {
|
||||
},
|
||||
insertSale: function(sale) {
|
||||
check(sale, {
|
||||
date: Date,
|
||||
date: Number, // TODO: Check that the format is YYYYMMDD
|
||||
amount: Match.Where(function(x) {
|
||||
check(x, Number);
|
||||
return x > 0;
|
||||
@@ -199,7 +284,7 @@ if(Meteor.isServer) {
|
||||
venueId: String,
|
||||
comment: Match.Optional(String)
|
||||
});
|
||||
//TODO: Check the structure of sale. Use: check(sale, {name: String, ...});
|
||||
|
||||
sale.createdAt = new Date();
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
@@ -242,7 +327,7 @@ if(Meteor.isServer) {
|
||||
},
|
||||
updateSale: function(id, date, venueId, price, amount) {
|
||||
check(id, String);
|
||||
check(date, Date);
|
||||
check(date, Number); // TODO: Check that the format is YYYYMMDD
|
||||
check(venueId, String);
|
||||
check(price, Number);
|
||||
check(amount, Number);
|
||||
@@ -252,7 +337,152 @@ if(Meteor.isServer) {
|
||||
if(err) console.log(err);
|
||||
}, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
countSales: function() {
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
return Sales.find({}).count();
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
removeDuplicateSales: function(id, justOne) { // Expects the id of the sale that has duplicates and an optional boolean flag (justOne) indicating whether just one duplicate should be removed, or all of them (default).
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
// Remove either one or all of the duplicates of the Sale with the given ID.
|
||||
if(justOne) {
|
||||
let sale = Sales.findOne({isDuplicateOf: id});
|
||||
|
||||
if(sale) {
|
||||
Sales.remove({_id: sale._id});
|
||||
}
|
||||
}
|
||||
else {
|
||||
Sales.remove({isDuplicateOf: id});
|
||||
}
|
||||
}
|
||||
},
|
||||
ignoreDuplicateSales: function(id) { // Expects the id of the sale that has duplicates. Will mark this sale and all duplicates to be ignored in future duplicate checks.
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
// Mark to ignore duplicates for this Sale (id) and all its duplicates, and clear any duplicate counts and references.
|
||||
//Sales.update({$or: [{_id: id}, {isDuplicateOf: id}]}, {$set: {ignoreDuplicates: true}, $unset: {isDuplicateOf: "", duplicateCount: ""}});
|
||||
|
||||
// Mark to ignore duplicates for this Sale (id). We will leave the duplicate count and references so that the duplicates will show in a query if we want to revisit those marked as ignored.
|
||||
Sales.update({$or: [{_id: id}, {isDuplicateOf: id}]}, {$set: {ignoreDuplicates: true}});
|
||||
}
|
||||
},
|
||||
markDuplicateSales: function() {
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
let sales = Sales.find({}, {sort: {date: 1, venueId: 1, productId: 1, price: 1, amount: 1, measureId: 1, createdAt: 1}}).fetch();
|
||||
|
||||
// Iterate over all the sales looking for sales that have duplicates.
|
||||
// Since the sales are sorted by sale date, venueId, productId, price, amount, and measureId which all must be identical to be considered a possible duplicate sale, we only have to check subsequent sales until a non-duplicate is found.
|
||||
for(let i = 0; i < sales.length;) {
|
||||
let sale = sales[i];
|
||||
|
||||
// If this is marked as a duplicate of another sale, but we got to this point in the loop then the sale it is a duplicate of must have been removed or marked to ignore duplicates.
|
||||
if(sale.isDuplicateOf) {
|
||||
delete sale.isDuplicateOf;
|
||||
Sales.update(sale._id, {$unset: {isDuplicateOf: ""}}, function(err, id) {
|
||||
if(err) console.log(err);
|
||||
}, {bypassCollection2: true});
|
||||
}
|
||||
|
||||
//Skip this one if it is marked to ignore duplicates.
|
||||
//if(sale.ignoreDuplicates) {
|
||||
// i++;
|
||||
//}
|
||||
//else {
|
||||
let keepChecking = true;
|
||||
let duplicateCount = 0;
|
||||
|
||||
// Keep checking subsequent sales until a non-duplicate is found. Ignore anything marked to ignore duplicates. Count the number of duplicates and also mark duplicates to reference the Sale we are currently checking.
|
||||
while(keepChecking) {
|
||||
let checkSale = sales[++i]; // Increment the index to the next Sale object.
|
||||
|
||||
// Since it is possible to exceed the length of the array, we will check for an undefined next sale and set the flag to stop checking if one is found.
|
||||
if(checkSale && sale.productId === checkSale.productId && sale.venueId === checkSale.venueId && sale.price === checkSale.price && sale.amount === checkSale.amount && sale.measureId === checkSale.measureId) {
|
||||
// Mark the next sale as a duplicate of the currently examined sale.
|
||||
checkSale.isDuplicateOf = sale._id;
|
||||
Sales.update(checkSale._id, {$set: {isDuplicateOf: checkSale.isDuplicateOf}}, function(err, id) {
|
||||
if(err) console.log(err);
|
||||
}, {bypassCollection2: true});
|
||||
duplicateCount++;
|
||||
}
|
||||
else {
|
||||
// Stop checking.
|
||||
keepChecking = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the currently checked sale has a proper duplicate count before moving on in the search.
|
||||
if(duplicateCount > 0) {
|
||||
if(sale.duplicateCount !== duplicateCount) {
|
||||
// Update the sale's duplicate count.
|
||||
sale.duplicateCount = duplicateCount;
|
||||
Sales.update(sale._id, {$set: {duplicateCount: sale.duplicateCount}}, function(err, id) {
|
||||
if(err) console.log(err);
|
||||
}, {bypassCollection2: true});
|
||||
}
|
||||
}
|
||||
else if(sale.duplicateCount) {
|
||||
// Remove the duplicate count if it is set.
|
||||
delete sale.duplicateCount;
|
||||
Sales.update(sale._id, {$unset: {duplicateCount: ""}}, function(err, id) {
|
||||
if(err) console.log(err);
|
||||
}, {bypassCollection2: true});
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
}
|
||||
/*
|
||||
countDuplicateSales: function() {
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_MANAGE])) {
|
||||
let sales = Sales.find({}, {sort: {date: 1, venueId: 1, productId: 1, price: 1, amount: 1, measureId: 1}}).fetch();
|
||||
let salesByDate = {};
|
||||
let lastDate = undefined;
|
||||
let lastDateCollection;
|
||||
let duplicates = [];
|
||||
|
||||
//Create a map of Sale arrays by sale date.
|
||||
for(let i = 0; i < sales.length; i++) {
|
||||
let date = sales[i].date;
|
||||
|
||||
if(date) {
|
||||
if(date === lastDate) {
|
||||
lastDateCollection.push(sales[i]);
|
||||
}
|
||||
else {
|
||||
lastDate = date;
|
||||
salesByDate[date] = lastDateCollection = [sales[i]];
|
||||
}
|
||||
}
|
||||
else {
|
||||
Meteor.log.error("Found a sale without a date!!!");
|
||||
}
|
||||
}
|
||||
|
||||
for(let date in salesByDate) {
|
||||
let sales = salesByDate[date];
|
||||
|
||||
for(let i = 0; i < sales.length - 1; i++) {
|
||||
if(sales[i].productId === sales[i+1].productId && sales[i].venueId === sales[i+1].venueId && sales[i].price === sales[i+1].price && sales[i].amount === sales[i+1].amount && sales[i].measureId === sales[i+1].amount) {
|
||||
duplicates.push([sales[i], sales[i+1]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return duplicates;
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
deleteDuplicateSales: function() {
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_MANAGE])) {
|
||||
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
}
|
||||
*/
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import Products from "./Product.js";
|
||||
import ProductTags from "./ProductTag.js";
|
||||
import Sales from "./Sale.js";
|
||||
import SalesSheets from "./SalesSheet.js";
|
||||
import Logs from "./Logs.js";
|
||||
import Users from "./User.js";
|
||||
import UserRoles from "./Roles.js";
|
||||
|
||||
//Save the collections in the Meteor.collections property for easy access without name conflicts.
|
||||
Meteor.collections = {Measures, Venues, Products, ProductTags, Sales, SalesSheets, Users, UserRoles};
|
||||
Meteor.collections = {Measures, Venues, Products, ProductTags, Sales, SalesSheets, Logs, Users, UserRoles};
|
||||
|
||||
//If this is the server then setup the default admin user if none exist.
|
||||
if(Meteor.isServer) {
|
||||
|
||||
@@ -36,6 +36,13 @@ pri.route('/userManagement', {
|
||||
BlazeLayout.render('Body', {content: 'UserManagement'});
|
||||
}
|
||||
});
|
||||
pri.route('/miscManagement', {
|
||||
name: 'MiscManagement',
|
||||
action: function(params, queryParams) {
|
||||
require("/imports/ui/MiscManagement.js");
|
||||
BlazeLayout.render('Body', {content: 'MiscManagement'});
|
||||
}
|
||||
});
|
||||
pri.route('/sales', {
|
||||
name: 'Sales',
|
||||
action: function(params, queryParams) {
|
||||
@@ -44,6 +51,14 @@ pri.route('/sales', {
|
||||
BlazeLayout.render('Body', {content: 'Sales'});
|
||||
}
|
||||
});
|
||||
pri.route('/saleDuplicates', {
|
||||
name: 'SaleDuplicates',
|
||||
action: function(params, queryParams) {
|
||||
require("/imports/ui/SaleDuplicates.js");
|
||||
|
||||
BlazeLayout.render('Body', {content: 'SaleDuplicates'});
|
||||
}
|
||||
});
|
||||
pri.route('/salesSheets', {
|
||||
name: 'SalesSheets',
|
||||
action: function(params, queryParams) {
|
||||
|
||||
@@ -1,37 +1,44 @@
|
||||
<template name="Measures">
|
||||
<div id="measures">
|
||||
<div class="tableControls">
|
||||
<span class="controlLabel">Show Hidden</span>
|
||||
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
|
||||
<label>
|
||||
<input type="checkbox" name="showHidden"><span></span>
|
||||
</label>
|
||||
{{#if Template.subscriptionsReady}}
|
||||
<div class="tableControls">
|
||||
<span class="controlLabel">Show Hidden</span>
|
||||
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
|
||||
<label>
|
||||
<input type="checkbox" name="showHidden"><span></span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="pagination">
|
||||
<span class="prevMeasures noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextMeasures noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="pagination">
|
||||
<span class="prevMeasures noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextMeasures noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="name">Name {{>MeasureSearch columnName='name'}}</th>
|
||||
<th class="postfix">Postfix {{>MeasureSearch columnName='postfix'}}</th>
|
||||
<th class="actions">Actions <span class="newMeasureButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
|
||||
</tr>
|
||||
<!--<button type="button" name="newMeasureButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#if displayNewMeasure}}
|
||||
{{> MeasureEditor isNew=true}}
|
||||
{{/if}}
|
||||
{{#each measures}}
|
||||
{{> Measure}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="listRow">
|
||||
<div class="listCell">
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="name">Name {{>MeasureSearch columnName='name'}}</th>
|
||||
<th class="postfix">Postfix {{>MeasureSearch columnName='postfix'}}</th>
|
||||
<th class="actions">Actions <span class="newMeasureButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
|
||||
</tr>
|
||||
<!--<button type="button" name="newMeasureButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#if displayNewMeasure}}
|
||||
{{> MeasureEditor isNew=true}}
|
||||
{{/if}}
|
||||
{{#each measures}}
|
||||
{{> Measure}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
163
imports/ui/Measures.import.styl
vendored
163
imports/ui/Measures.import.styl
vendored
@@ -1,6 +1,9 @@
|
||||
#measures
|
||||
margin: 20px 20px
|
||||
display: table
|
||||
content-box: border-box
|
||||
padding: 10px 20px
|
||||
height: 100%
|
||||
width: 100%
|
||||
text-align: left
|
||||
|
||||
.tableControls
|
||||
@@ -18,77 +21,89 @@
|
||||
top: -4px
|
||||
display: inline-block
|
||||
|
||||
.tableContainer
|
||||
width: 100%
|
||||
margin-bottom: 20px
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
|
||||
table
|
||||
table-layout: fixed
|
||||
.listRow
|
||||
display: table-row
|
||||
.listCell
|
||||
display: table-cell
|
||||
position: relative
|
||||
height: 100%
|
||||
width: 100%
|
||||
.measureSearch
|
||||
margin: 3px 0 2px 1px
|
||||
.measureEditorTd
|
||||
background: #deeac0
|
||||
input[name="name"], input[name="postfix"]
|
||||
width: 100%
|
||||
.editorDiv
|
||||
margin: 4px 0
|
||||
label
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-size: .9em
|
||||
padding-bottom: 4px
|
||||
select2
|
||||
font-size: .4em
|
||||
> thead
|
||||
> tr
|
||||
> th.name
|
||||
width: auto
|
||||
> th.postfix
|
||||
width: auto
|
||||
> th.actions
|
||||
width: 90px
|
||||
text-align: center
|
||||
.newMeasureButton
|
||||
margin-top: 4px
|
||||
padding: 0px 12px
|
||||
.fa-plus-circle
|
||||
display: inline-block
|
||||
.fa-times-circle
|
||||
display: none
|
||||
.newMeasureButton.active
|
||||
background-color: #fb557b
|
||||
color: black
|
||||
.fa-times-circle
|
||||
display: inline-block
|
||||
.fa-plus-circle
|
||||
display: none
|
||||
> tbody
|
||||
> tr
|
||||
.actionRemove
|
||||
color: #F77
|
||||
.actionEdit
|
||||
color: #44F
|
||||
.editorApply
|
||||
color: green
|
||||
.editorCancel
|
||||
color: red
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
.actionActivate
|
||||
color: #158b18
|
||||
.actionHide
|
||||
color: #6a0707
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
> tr.hidden
|
||||
background-color: #e995ff
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
.actionShow
|
||||
color: #027905
|
||||
> tr.hidden:hover
|
||||
background-color: #ffb5ff
|
||||
.tableContainer
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
width: auto
|
||||
height: auto
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
overflow-y: auto
|
||||
table
|
||||
table-layout: fixed
|
||||
width: 100%
|
||||
.measureSearch
|
||||
margin: 3px 0 2px 1px
|
||||
.measureEditorTd
|
||||
background: #deeac0
|
||||
input[name="name"], input[name="postfix"]
|
||||
width: 100%
|
||||
.editorDiv
|
||||
margin: 4px 0
|
||||
label
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-size: .9em
|
||||
padding-bottom: 4px
|
||||
select2
|
||||
font-size: .4em
|
||||
> thead
|
||||
> tr
|
||||
> th.name
|
||||
width: auto
|
||||
> th.postfix
|
||||
width: auto
|
||||
> th.actions
|
||||
width: 90px
|
||||
text-align: center
|
||||
.newMeasureButton
|
||||
margin-top: 4px
|
||||
padding: 0px 12px
|
||||
.fa-plus-circle
|
||||
display: inline-block
|
||||
.fa-times-circle
|
||||
display: none
|
||||
.newMeasureButton.active
|
||||
background-color: #fb557b
|
||||
color: black
|
||||
.fa-times-circle
|
||||
display: inline-block
|
||||
.fa-plus-circle
|
||||
display: none
|
||||
> tbody
|
||||
> tr
|
||||
.actionRemove
|
||||
color: #F77
|
||||
.actionEdit
|
||||
color: #44F
|
||||
.editorApply
|
||||
color: green
|
||||
.editorCancel
|
||||
color: red
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
.actionActivate
|
||||
color: #158b18
|
||||
.actionHide
|
||||
color: #6a0707
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
> tr.hidden
|
||||
background-color: #e995ff
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
.actionShow
|
||||
color: #027905
|
||||
> tr.hidden:hover
|
||||
background-color: #ffb5ff
|
||||
27
imports/ui/MiscManagement.html
Normal file
27
imports/ui/MiscManagement.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<template name="MiscManagement">
|
||||
<div id="miscManagement">
|
||||
{{#if Template.subscriptionsReady}}
|
||||
<div class="controls">
|
||||
<a href="javascript:" class="cleanDates">Clean Dates (removes time components)</a><br/>
|
||||
<!--<a href="javascript:" class="importMissingSalesData">Import Sales Data (JSON)</a><br/>-->
|
||||
<a href="javascript:" class="clearLogs">Clear Logs</a><br/>
|
||||
<a href="javascript:" class="countDuplicateSales">Count Duplicate Sales</a><br/>
|
||||
<a href="javascript:" class="deleteDuplicateSales">Delete Duplicate Sales</a><br/>
|
||||
<div class="logCount">{{logCount}}</div>
|
||||
</div>
|
||||
<div class="pageContentRow">
|
||||
<div class="pageContentCell">
|
||||
<div class="pageContentContainer">
|
||||
<ul class="logs">
|
||||
{{#each logs}}
|
||||
<li>{{message}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
35
imports/ui/MiscManagement.import.styl
vendored
Normal file
35
imports/ui/MiscManagement.import.styl
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
#miscManagement
|
||||
display: table
|
||||
content-box: border-box
|
||||
padding: 10px 20px
|
||||
height: 100%
|
||||
width: 100%
|
||||
text-align: left
|
||||
|
||||
.controls
|
||||
text-align: right
|
||||
margin-right: 20px
|
||||
|
||||
.pageContentRow
|
||||
display: table-row
|
||||
.pageContentCell
|
||||
display: table-cell
|
||||
position: relative
|
||||
height: 100%
|
||||
width: 100%
|
||||
.pageContentContainer
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
width: auto
|
||||
height: auto
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
overflow-y: auto
|
||||
.logs
|
||||
list-style-type: none
|
||||
height: 100%
|
||||
:hover
|
||||
background: #CCC
|
||||
76
imports/ui/MiscManagement.js
Normal file
76
imports/ui/MiscManagement.js
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
import './MiscManagement.html';
|
||||
import '/imports/util/selectize/selectize.js'
|
||||
|
||||
let PREFIX = "MiscManagement";
|
||||
|
||||
Meteor.subscribe("logs");
|
||||
Meteor.subscribe("products");
|
||||
Meteor.subscribe("venues");
|
||||
Meteor.subscribe("measures");
|
||||
|
||||
Template.MiscManagement.helpers({
|
||||
logs: function() {
|
||||
return Meteor.collections.Logs.find({}, {sort: {date: 1}});
|
||||
},
|
||||
logCount: function() {
|
||||
return Meteor.collections.Logs.find({}).count();
|
||||
}
|
||||
});
|
||||
Template.MiscManagement.events({
|
||||
"click .cleanDates": function(event, template) {
|
||||
Meteor.call("cleanDates");
|
||||
},
|
||||
"click .importMissingSalesData": function(event, template) {
|
||||
console.log("Calling importMissingSales");
|
||||
Meteor.call("importMissingSales");
|
||||
},
|
||||
"click .clearLogs": function(event, template) {
|
||||
Meteor.call("clearLogs");
|
||||
},
|
||||
"click .countDuplicateSales": function(event, template) {
|
||||
Meteor.log.info("Starting to count duplicates...");
|
||||
|
||||
let products = Meteor.collections.Products.find({}).fetch();
|
||||
let venues = Meteor.collections.Venues.find({}).fetch();
|
||||
let measures = Meteor.collections.Measures.find({}).fetch();
|
||||
let productNameMap = {};
|
||||
let venueNameMap = {};
|
||||
let measureNameMap = {};
|
||||
|
||||
for(let i = 0; i < products.length; i++) {
|
||||
productNameMap[products[i]._id] = products[i].name;
|
||||
}
|
||||
for(let i = 0; i < venues.length; i++) {
|
||||
venueNameMap[venues[i]._id] = venues[i].name;
|
||||
}
|
||||
for(let i = 0; i < measures.length; i++) {
|
||||
measureNameMap[measures[i]._id] = measures[i].name;
|
||||
}
|
||||
|
||||
Meteor.call("countSales", function(err, result) {
|
||||
if(err) Meteor.log.error(err);
|
||||
else {
|
||||
let salesCount = result;
|
||||
|
||||
Meteor.call("countDuplicateSales", function(err, result) {
|
||||
if(err) Meteor.log.error(err);
|
||||
else {
|
||||
Meteor.log.info("Duplicate Sales Counted: " + result.length + " out of " + salesCount + " total sales.");
|
||||
|
||||
for(let i = 0; i < result.length; i++) {
|
||||
let sale = result[i][0];
|
||||
Meteor.log.info("\tdate: " + sale.date + " product: " + productNameMap[sale.productId] + " venue: " + venueNameMap[sale.venueId] + " measure" + measureNameMap[sale.measureId] + " price: " + sale.price.toFixed(2) + " amount: " + sale.amount + " id: " + sale._id);
|
||||
sale = result[i][1];
|
||||
Meteor.log.info("\tdate: " + sale.date + " product: " + productNameMap[sale.productId] + " venue: " + venueNameMap[sale.venueId] + " measure" + measureNameMap[sale.measureId] + " price: " + sale.price.toFixed(2) + " amount: " + sale.amount + " id: " + sale._id);
|
||||
Meteor.log.info(" -- ");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
"click .deleteDuplicateSales": function(event, template) {
|
||||
Meteor.call("deleteDuplicateSales");
|
||||
}
|
||||
});
|
||||
@@ -1,54 +1,61 @@
|
||||
<template name="Pricing">
|
||||
<div id="pricing">
|
||||
<div class="controls">
|
||||
<div class="measureGroup" style="vertical-align: bottom">
|
||||
<label class='controlLabel'>Selected Measure: </label>
|
||||
<select name="measures">
|
||||
{{#each measures}}
|
||||
<option value="{{_id}}">{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="controlGroup" style="text-align: center">
|
||||
<label class='controlLabel'>New Price: </label>
|
||||
<input type="number" class="price" name="price" min="0" data-schema-key='currency' value="{{price}}" required>
|
||||
<input type="button" class="btn btn-success applyButton" title="Applies the price to selected products." value="Apply">
|
||||
<input type="button" class="btn btn-danger resetButton" title="Resets this form." value="Reset">
|
||||
<br/>
|
||||
<!--<span class="toggleUpdateHistory toggleButton clickable">Set Prev</span>-->
|
||||
<div class="previousSettings outline">
|
||||
<span class="controlLabel">Set Previous:</span>
|
||||
<div class="toggleUpdateHistory checkbox checkbox-slider--b-flat">
|
||||
<label>
|
||||
<input type="checkbox" name="setPrevious" checked><span></span>
|
||||
</label>
|
||||
{{#if Template.subscriptionsReady}}
|
||||
<div class="controls">
|
||||
<div class="measureGroup" style="vertical-align: bottom">
|
||||
<label class='controlLabel'>Selected Measure: </label>
|
||||
<select name="measures">
|
||||
{{#each measures}}
|
||||
<option value="{{_id}}">{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="controlGroup" style="text-align: center">
|
||||
<label class='controlLabel'>New Price: </label>
|
||||
<input type="number" class="price" name="price" min="0" data-schema-key='currency' value="{{price}}" required>
|
||||
<input type="button" class="btn btn-success applyButton" title="Applies the price to selected products." value="Apply">
|
||||
<input type="button" class="btn btn-danger resetButton" title="Resets this form." value="Reset">
|
||||
<br/>
|
||||
<!--<span class="toggleUpdateHistory toggleButton clickable">Set Prev</span>-->
|
||||
<div class="previousSettings outline">
|
||||
<span class="controlLabel">Set Previous:</span>
|
||||
<div class="toggleUpdateHistory checkbox checkbox-slider--b-flat">
|
||||
<label>
|
||||
<input type="checkbox" name="setPrevious" checked><span></span>
|
||||
</label>
|
||||
</div>
|
||||
<label class='controlLabel' style="margin-left: 10px">Effective: </label>
|
||||
<input type="date" class="form-control" name="date" data-schema-key='date' required>
|
||||
</div>
|
||||
</div>
|
||||
<span class="pagination">
|
||||
<span class="prevProducts noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextProducts noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="listRow">
|
||||
<div class="listCell">
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="name">Name</th>
|
||||
<th class="current">Current</th>
|
||||
<th class="changeDate">Change Date</th>
|
||||
<th class="previous">Previous</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each product}}
|
||||
{{> PricingForProduct}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<label class='controlLabel' style="margin-left: 10px">Effective: </label>
|
||||
<input type="date" class="form-control" name="date" data-schema-key='date' required>
|
||||
</div>
|
||||
</div>
|
||||
<span class="pagination">
|
||||
<span class="prevProducts noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextProducts noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="name">Name</th>
|
||||
<th class="current">Current</th>
|
||||
<th class="changeDate">Change Date</th>
|
||||
<th class="previous">Previous</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each product}}
|
||||
{{> PricingForProduct}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
62
imports/ui/Pricing.import.styl
vendored
62
imports/ui/Pricing.import.styl
vendored
@@ -1,6 +1,9 @@
|
||||
#pricing
|
||||
margin: 20px 20px
|
||||
display: table
|
||||
content-box: border-box
|
||||
padding: 10px 20px
|
||||
height: 100%
|
||||
width: 100%
|
||||
text-align: left
|
||||
|
||||
.controls
|
||||
@@ -60,26 +63,39 @@
|
||||
.resetButton
|
||||
margin-left: 20px
|
||||
|
||||
.tableContainer
|
||||
width: 100%
|
||||
margin-bottom: 20px
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
table
|
||||
table-layout: fixed
|
||||
.listRow
|
||||
display: table-row
|
||||
.listCell
|
||||
display: table-cell
|
||||
position: relative
|
||||
height: 100%
|
||||
width: 100%
|
||||
> thead
|
||||
> tr
|
||||
> th.name
|
||||
width: auto
|
||||
> th.current
|
||||
width: 200px
|
||||
> th.previous
|
||||
width: 200px
|
||||
> th.changeDate
|
||||
width: 200px
|
||||
> tbody
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
.tableContainer
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
width: auto
|
||||
height: auto
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
overflow-y: auto
|
||||
table
|
||||
table-layout: fixed
|
||||
width: 100%
|
||||
> thead
|
||||
> tr
|
||||
> th.name
|
||||
width: auto
|
||||
> th.current
|
||||
width: 200px
|
||||
> th.previous
|
||||
width: 200px
|
||||
> th.changeDate
|
||||
width: 200px
|
||||
> tbody
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
@@ -1,6 +1,13 @@
|
||||
|
||||
import './Pricing.html';
|
||||
|
||||
/**
|
||||
* Notes:
|
||||
* The Product object has a prices field which is an object whose fields names are Measure ID's. Each field value (for each Measure ID) is an object that has a 'price', 'effectiveDate', and 'previousPrice'.
|
||||
* The effectiveDate field stores the date as a number in the format YYYYMMDD. Converting this number into a local date is done with moment(sale.date.toString(), "YYYYMMDD").toDate(), and converting it to a number from a date can be accomplished with ~~(moment(date).format("YYYYMMDD")), where the ~~ is a bitwise not and converts a string to a number quickly and reliably.
|
||||
* Because the structure of the Product object is so complicated, the normal checking that is done by the framework cannot be used.
|
||||
*/
|
||||
|
||||
let QUERY_LIMIT = 20;
|
||||
let PREFIX = "Pricing.";
|
||||
|
||||
@@ -70,7 +77,7 @@ Template.Pricing.events({
|
||||
Meteor.call("clearProductPrice", productIds, measureId)
|
||||
}
|
||||
else {
|
||||
date = moment(date ? date : new Date().toDateInputValue(), "YYYY-MM-DD").toDate();
|
||||
date = ~~(moment(date ? date : new Date().toDateInputValue(), "YYYY-MM-DD").format("YYYYMMDD")); // The ~~ is a bitwise not which converts the string into a number in the format of YYYYMMDD for storage in the database; to avoid timezone issues.
|
||||
setPrevious = setPrevious == true || setPrevious == 'on' || setPrevious == "true" || setPrevious == "yes";
|
||||
|
||||
if(setPrevious == true && !date) {
|
||||
@@ -117,9 +124,8 @@ Template.PricingForProduct.helpers({
|
||||
},
|
||||
priceChangeDate: function() {
|
||||
let measureId = Session.get(PREFIX + "selectedMeasure");
|
||||
let date = this.prices && measureId && this.prices[measureId] && this.prices[measureId].effectiveDate ? this.prices[measureId].effectiveDate : undefined;
|
||||
|
||||
return date ? moment(date).format("MM/DD/YYYY (w)") : "-";
|
||||
return this.prices && measureId && this.prices[measureId] && this.prices[measureId].effectiveDate ? moment(this.prices[measureId].effectiveDate.toString(), "YYYYMMDD").format("MM/DD/YYYY (w)") : "-";
|
||||
},
|
||||
rowClass: function() {
|
||||
return this.deactivated ? "deactivated" : "";
|
||||
|
||||
@@ -26,20 +26,24 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="name">Name {{>ProductTag_ProductSearch columnName='name'}}</th>
|
||||
<th class="tags">Tags {{>ProductTag_ProductSearch columnName='tags' collectionQueryColumnName='name' collection='ProductTags' collectionResultColumnName='_id'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each products}}
|
||||
{{> ProductTag_Product}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="listRow">
|
||||
<div class="listCell">
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="name">Name {{>ProductTag_ProductSearch columnName='name'}}</th>
|
||||
<th class="tags">Tags {{>ProductTag_ProductSearch columnName='tags' collectionQueryColumnName='name' collection='ProductTags' collectionResultColumnName='_id'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each products}}
|
||||
{{> ProductTag_Product}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{/if}}
|
||||
|
||||
54
imports/ui/ProductTags.import.styl
vendored
54
imports/ui/ProductTags.import.styl
vendored
@@ -1,6 +1,9 @@
|
||||
#productTags
|
||||
margin: 20px 20px
|
||||
display: table
|
||||
content-box: border-box
|
||||
padding: 10px 20px
|
||||
height: 100%
|
||||
width: 100%
|
||||
text-align: left
|
||||
|
||||
.tagInfo
|
||||
@@ -107,25 +110,38 @@
|
||||
display: table-cell
|
||||
width: 240px
|
||||
vertical-align: bottom;
|
||||
.tableContainer
|
||||
width: 100%
|
||||
margin-bottom: 20px
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
table
|
||||
table-layout: fixed
|
||||
.listRow
|
||||
display: table-row
|
||||
.listCell
|
||||
display: table-cell
|
||||
position: relative
|
||||
height: 100%
|
||||
width: 100%
|
||||
> thead
|
||||
> tr
|
||||
> th.name
|
||||
width: auto
|
||||
> th.tags
|
||||
width: auto
|
||||
> tbody
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
.tableContainer
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
width: auto
|
||||
height: auto
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
overflow-y: auto
|
||||
table
|
||||
table-layout: fixed
|
||||
width: 100%
|
||||
> thead
|
||||
> tr
|
||||
> th.name
|
||||
width: auto
|
||||
> th.tags
|
||||
width: auto
|
||||
> tbody
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
td.roles
|
||||
.role
|
||||
padding: 4px 4px
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
<template name="Products">
|
||||
<div id="products">
|
||||
<div class="tableControls">
|
||||
<span class="controlLabel">Show Hidden</span>
|
||||
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
|
||||
<label>
|
||||
<input type="checkbox" name="showHidden"><span></span>
|
||||
</label>
|
||||
{{#if Template.subscriptionsReady}}
|
||||
<div class="tableControls">
|
||||
<span class="controlLabel">Show Hidden</span>
|
||||
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
|
||||
<label>
|
||||
<input type="checkbox" name="showHidden"><span></span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="pagination">
|
||||
<span class="prevProducts noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextProducts noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="pagination">
|
||||
<span class="prevProducts noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextProducts noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="name">Name {{>ProductSearch columnName='name'}}</th>
|
||||
<th class="tags">Tags {{>ProductSearch columnName='tags' collectionQueryColumnName='name' collection='ProductTags' collectionResultColumnName='_id'}}</th>
|
||||
<th class="aliases">Aliases {{>ProductSearch columnName='aliases'}}</th>
|
||||
<th class="measures">Measures {{>ProductSearch columnName='measures' collectionQueryColumnName='name' collection='Measures' collectionResultColumnName='_id'}}</th>
|
||||
<th class="actions">Actions <span class="newProductButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
|
||||
</tr>
|
||||
<!--<button type="button" name="newProductButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#if displayNewProduct}}
|
||||
{{> ProductEditor isNew=true}}
|
||||
{{/if}}
|
||||
{{#each products}}
|
||||
{{> Product}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="listRow">
|
||||
<div class="listCell">
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="name">Name {{>ProductSearch columnName='name'}}</th>
|
||||
<th class="tags">Tags {{>ProductSearch columnName='tags' collectionQueryColumnName='name' collection='ProductTags' collectionResultColumnName='_id'}}</th>
|
||||
<th class="aliases">Aliases {{>ProductSearch columnName='aliases'}}</th>
|
||||
<th class="measures">Measures {{>ProductSearch columnName='measures' collectionQueryColumnName='name' collection='Measures' collectionResultColumnName='_id'}}</th>
|
||||
<th class="actions">Actions <span class="newProductButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
|
||||
</tr>
|
||||
<!--<button type="button" name="newProductButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#if displayNewProduct}}
|
||||
{{> ProductEditor isNew=true}}
|
||||
{{/if}}
|
||||
{{#each products}}
|
||||
{{> Product}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
171
imports/ui/Products.import.styl
vendored
171
imports/ui/Products.import.styl
vendored
@@ -1,6 +1,9 @@
|
||||
#products
|
||||
margin: 20px 20px
|
||||
display: table
|
||||
content-box: border-box
|
||||
padding: 10px 20px
|
||||
height: 100%
|
||||
width: 100%
|
||||
text-align: left
|
||||
|
||||
.tableControls
|
||||
@@ -18,81 +21,93 @@
|
||||
top: -4px
|
||||
display: inline-block
|
||||
|
||||
.tableContainer
|
||||
width: 100%
|
||||
margin-bottom: 20px
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
|
||||
table
|
||||
table-layout: fixed
|
||||
.listRow
|
||||
display: table-row
|
||||
.listCell
|
||||
display: table-cell
|
||||
position: relative
|
||||
height: 100%
|
||||
width: 100%
|
||||
.productSearch
|
||||
margin: 3px 0 2px 1px
|
||||
.productEditorTd
|
||||
background: #deeac0
|
||||
input[name="name"], .productTagsEditor, .productAliasesEditor, .productMeasuresEditor
|
||||
width: 100%
|
||||
.editorDiv
|
||||
margin: 4px 0
|
||||
label
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-size: .9em
|
||||
padding-bottom: 4px
|
||||
select2
|
||||
font-size: .4em
|
||||
> thead
|
||||
> tr
|
||||
> th.name
|
||||
width: auto
|
||||
> th.tags
|
||||
width: 220px
|
||||
> th.aliases
|
||||
width: 220px
|
||||
> th.measures
|
||||
width: 220px
|
||||
> th.actions
|
||||
width: 90px
|
||||
text-align: center
|
||||
.newProductButton
|
||||
margin-top: 4px
|
||||
padding: 0px 12px
|
||||
.fa-plus-circle
|
||||
display: inline-block
|
||||
.fa-times-circle
|
||||
display: none
|
||||
.newProductButton.active
|
||||
background-color: #fb557b
|
||||
color: black
|
||||
.fa-times-circle
|
||||
display: inline-block
|
||||
.fa-plus-circle
|
||||
display: none
|
||||
> tbody
|
||||
> tr
|
||||
.actionRemove
|
||||
color: #F77
|
||||
.actionEdit
|
||||
color: #44F
|
||||
.editorApply
|
||||
color: green
|
||||
.editorCancel
|
||||
color: red
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
.actionActivate
|
||||
color: #158b18
|
||||
.actionHide
|
||||
color: #6a0707
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
> tr.hidden
|
||||
background-color: #e995ff
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
.actionShow
|
||||
color: #027905
|
||||
> tr.hidden:hover
|
||||
background-color: #ffb5ff
|
||||
.tableContainer
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
width: auto
|
||||
height: auto
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
overflow-y: auto
|
||||
table
|
||||
table-layout: fixed
|
||||
width: 100%
|
||||
.productSearch
|
||||
margin: 3px 0 2px 1px
|
||||
.productEditorTd
|
||||
background: #deeac0
|
||||
input[name="name"], .productTagsEditor, .productAliasesEditor, .productMeasuresEditor
|
||||
width: 100%
|
||||
.editorDiv
|
||||
margin: 4px 0
|
||||
label
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-size: .9em
|
||||
padding-bottom: 4px
|
||||
select2
|
||||
font-size: .4em
|
||||
> thead
|
||||
> tr
|
||||
> th.name
|
||||
width: auto
|
||||
> th.tags
|
||||
width: 220px
|
||||
> th.aliases
|
||||
width: 220px
|
||||
> th.measures
|
||||
width: 220px
|
||||
> th.actions
|
||||
width: 90px
|
||||
text-align: center
|
||||
.newProductButton
|
||||
margin-top: 4px
|
||||
padding: 0px 12px
|
||||
.fa-plus-circle
|
||||
display: inline-block
|
||||
.fa-times-circle
|
||||
display: none
|
||||
.newProductButton:active
|
||||
background-color: #fb557b
|
||||
color: black
|
||||
.fa-times-circle
|
||||
display: inline-block
|
||||
.fa-plus-circle
|
||||
display: none
|
||||
> tbody
|
||||
> tr
|
||||
.actionRemove
|
||||
color: #F77
|
||||
.actionEdit
|
||||
color: #44F
|
||||
.editorApply
|
||||
color: green
|
||||
.editorCancel
|
||||
color: red
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
.actionActivate
|
||||
color: #158b18
|
||||
.actionHide
|
||||
color: #6a0707
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
> tr.hidden
|
||||
background-color: #e995ff
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
.actionShow
|
||||
color: #027905
|
||||
> tr.hidden:hover
|
||||
background-color: #ffb5ff
|
||||
61
imports/ui/SaleDuplicates.html
Normal file
61
imports/ui/SaleDuplicates.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<template name="SaleDuplicates">
|
||||
<div id="saleDuplicates">
|
||||
<div class="controls">
|
||||
<div class="pageControls">
|
||||
<input class="duplicateScan btn btn-info" type="button" value="Scan For Duplicates"/>
|
||||
</div>
|
||||
<div class="tableControls">
|
||||
<span class="controlLabel">Show Hidden</span>
|
||||
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
|
||||
<label>
|
||||
<input type="checkbox" name="showHidden" {{showHidden}}><span></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="listRow">
|
||||
<div class="listCell">
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="amount noselect nonclickable">Duplicates</th>
|
||||
<th class="amount noselect nonclickable">Amount</th>
|
||||
<th class="product noselect nonclickable">Product <br/>{{>SaleDuplicateSearch columnName='productName' width='90%'}}</th>
|
||||
<th class="price noselect nonclickable">Price</th>
|
||||
<th class="measure noselect nonclickable">Measure</th>
|
||||
<th class="saleDate noselect nonclickable">Date (Week)</th>
|
||||
<th class="createdDate noselect nonclickable">Created On</th>
|
||||
<th class="venue noselect nonclickable">Venue</th>
|
||||
<th class="actions noselect nonclickable">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each sales}}
|
||||
{{> SaleDuplicate}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="SaleDuplicate">
|
||||
<tr class="{{duplicateClasses}}">
|
||||
<td class="tdLarge noselect nonclickable center">{{duplicateCount}}</td>
|
||||
<td class="tdLarge noselect nonclickable center">{{amount}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{productName}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{formatPrice price}}{{#if showTotalPrice amount}} ({{formatTotalPrice price amount}}){{/if}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{measureName}}</td> <!-- measureName measureId -->
|
||||
<td class="tdLarge noselect nonclickable left">{{formatDateAndWeek date}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{formatDateTime createdAt}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{venueName}}</td>
|
||||
<td class="tdLarge noselect left actions"><i class="fa fa-check fa-lg clickable ignoreDuplicatesButton {{#if ignoreDuplicates}}hidden{{/if}}" title="Ignore All Duplicates" aria-hidden="true"></i> <i class="fa fa-minus-circle fa-lg clickable removeAllDuplicatesButton" title="Remove All Duplicates" aria-hidden="true"></i> <span class="clickable removeOneDuplicateButton" title="Remove One Duplicate"><i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i><sup>1</sup></span></td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template name="SaleDuplicateSearch">
|
||||
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}" style="padding-right: 10px; width: {{width}}"/>
|
||||
</template>
|
||||
149
imports/ui/SaleDuplicates.import.styl
vendored
Normal file
149
imports/ui/SaleDuplicates.import.styl
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
#saleDuplicates
|
||||
display: table
|
||||
content-box: border-box
|
||||
padding: 10px 20px
|
||||
height: 100%
|
||||
width: 100%
|
||||
text-align: left
|
||||
.controls
|
||||
text-align: left
|
||||
display: table
|
||||
width: 100%
|
||||
.pageControls
|
||||
padding: 4px 8px
|
||||
margin: 4px 8px
|
||||
display: table-cell
|
||||
width: 240px
|
||||
.tableControls
|
||||
text-align: right
|
||||
padding: 4px 8px
|
||||
margin: 4px 12px 4px 8px
|
||||
display: table-cell
|
||||
.toggleShowHidden
|
||||
margin: 0 40px 0 0
|
||||
position: relative
|
||||
top: -4px
|
||||
display: inline-block
|
||||
.listRow
|
||||
display: table-row
|
||||
.listCell
|
||||
display: table-cell
|
||||
position: relative
|
||||
height: 100%
|
||||
width: 100%
|
||||
.tableContainer
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
width: auto
|
||||
height: auto
|
||||
//width: 100%
|
||||
//margin-bottom: 20px
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
overflow-y: auto
|
||||
//height: 100%
|
||||
label
|
||||
font-size: 10px
|
||||
font-weight: 800
|
||||
table
|
||||
table-layout: fixed
|
||||
min-width: 100%
|
||||
.saleRemove
|
||||
color: red
|
||||
margin-left: 8px
|
||||
.saleEdit
|
||||
color: darkblue
|
||||
margin-right: 8px
|
||||
.editorApply
|
||||
color: green
|
||||
.editorCancel
|
||||
color: red
|
||||
thead
|
||||
> tr
|
||||
> th.amount
|
||||
width: 90px
|
||||
> th.product
|
||||
width: auto
|
||||
min-width: 140px
|
||||
> th.price
|
||||
width: 140px
|
||||
> th.measure
|
||||
width: 100px
|
||||
> th.saleDate
|
||||
width: 140px
|
||||
> th.createdDate
|
||||
width: 100px
|
||||
> th.venue
|
||||
width: 160px
|
||||
> th.actions
|
||||
width: 90px
|
||||
tbody
|
||||
> tr
|
||||
> td.actions
|
||||
.ignoreDuplicatesButton
|
||||
padding: 0 2px
|
||||
color: green
|
||||
.ignoreDuplicatesButton:hover
|
||||
color: #00bb00
|
||||
.ignoreDuplicatesButton:active
|
||||
color: black
|
||||
.ignoreDuplicatesButton.hidden
|
||||
visibility: hidden
|
||||
.removeAllDuplicatesButton, .removeOneDuplicateButton
|
||||
padding: 0 2px
|
||||
color: #a00000
|
||||
.removeAllDuplicatesButton:hover, .removeOneDuplicateButton:hover
|
||||
color: red
|
||||
.removeAllDuplicatesButton:active, .removeOneDuplicateButton:active
|
||||
color: black
|
||||
> tr.hidden:nth-child(odd)
|
||||
background-color: #f4f0ab
|
||||
> tr.hidden:nth-child(even)
|
||||
background-color: #fff6c0
|
||||
> tr.hidden:hover
|
||||
background-color: #ded
|
||||
|
||||
.editComment
|
||||
color: grey
|
||||
.hasComment
|
||||
color: black
|
||||
.actionEdit
|
||||
margin-right: 6px
|
||||
color: #44F
|
||||
.saleEditor
|
||||
.heading
|
||||
font-size: 2em
|
||||
font-family: verdana, arial, helvetica, sans-serif
|
||||
text-transform: uppercase
|
||||
font-weight: 800
|
||||
margin: 6px 0 14px 0
|
||||
.priceContainer
|
||||
display: table
|
||||
width: 100%
|
||||
.price
|
||||
display: table-cell
|
||||
padding-right: 10px
|
||||
.priceButtons
|
||||
display: table-cell
|
||||
width: 1.5em
|
||||
.setDefaultPrice
|
||||
font-size: 1.5em
|
||||
padding: 6px 8px
|
||||
margin-left: 8px
|
||||
border-radius: 8px
|
||||
.setDefaultPrice:hover
|
||||
text-shadow: 0px 0px 6px #00b900
|
||||
.setDefaultPrice:active
|
||||
text-shadow: 0px 0px 6px grey
|
||||
.insertSaleForm
|
||||
.form-group, label
|
||||
text-align: left
|
||||
.formGroupHeading
|
||||
font-size: 1.6em
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-style: normal
|
||||
font-variant: normal
|
||||
font-weight: 500
|
||||
212
imports/ui/SaleDuplicates.js
Normal file
212
imports/ui/SaleDuplicates.js
Normal file
@@ -0,0 +1,212 @@
|
||||
|
||||
import './SaleDuplicates.html';
|
||||
import '/imports/util/selectize/selectize.js';
|
||||
import swal from 'sweetalert2';
|
||||
|
||||
/**
|
||||
* Notes:
|
||||
* The Sale object has a date field which stores the date as a number in the format YYYYMMDD. Converting this number into a local date is done with moment(sale.date.toString(), "YYYYMMDD").toDate(), and converting it to a number from a date can be accomplished with ~~(moment(date).format("YYYYMMDD")), where the ~~ is a bitwise not and converts a string to a number quickly and reliably.
|
||||
*/
|
||||
|
||||
let PREFIX = "SaleDuplicates.";
|
||||
let DuplicateSales = new Meteor.Collection("duplicateSales");
|
||||
let duplicateSalesSubscription;
|
||||
|
||||
Template.SaleDuplicates.onCreated(function() {
|
||||
let template = Template.instance();
|
||||
|
||||
//Tracker.autorun(function() {
|
||||
// let query = _.clone(Session.get(PREFIX + 'searchQuery'));
|
||||
//
|
||||
// duplicateSalesSubscription = template.subscribe("duplicateSales", query, Session.get(PREFIX + "showHidden"));
|
||||
//});
|
||||
|
||||
Tracker.autorun(function() {
|
||||
duplicateSalesSubscription = template.subscribe("duplicateSales", null, Session.get(PREFIX + "showHidden"));
|
||||
});
|
||||
});
|
||||
Template.SaleDuplicates.onDestroyed(function() {
|
||||
if(duplicateSalesSubscription) {
|
||||
duplicateSalesSubscription.stop();
|
||||
}
|
||||
});
|
||||
Template.SaleDuplicates.helpers({
|
||||
sales: function() {
|
||||
let dbQuery = [];
|
||||
let query = _.clone(Session.get(PREFIX + 'searchQuery'));
|
||||
|
||||
if(query) {
|
||||
// Add each query requirement sent by the client.
|
||||
_.each(_.keys(query), function(key) {
|
||||
//if(_.isObject(query[key])) dbQuery.push({[key]: query[key]});
|
||||
if(_.isObject(query[key])) {
|
||||
if(query[key].type === 'dateRange') {
|
||||
if(query[key].start && query[key].end)
|
||||
dbQuery.push({[key]: {$gte: query[key].start, $lte: query[key].end}});
|
||||
else if(query[key].start)
|
||||
dbQuery.push({[key]: {$gte: query[key].start}});
|
||||
else if(query[key].end)
|
||||
dbQuery.push({[key]: {$lte: query[key].end}});
|
||||
// Do nothing if a start and/or end are not provided.
|
||||
}
|
||||
else {
|
||||
dbQuery.push({[key]: query[key]});
|
||||
}
|
||||
}
|
||||
else if(_.isNumber(query[key])) dbQuery.push({[key]: query[key]});
|
||||
else {
|
||||
let searchValue = query[key];
|
||||
let searches = searchValue && searchValue.length > 0 ? searchValue.split(/\s+/) : undefined;
|
||||
|
||||
for(let search of searches) {
|
||||
dbQuery.push({[key]: {$regex: '\\b' + search, $options: 'i'}});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(dbQuery.length > 1) dbQuery = {$and: dbQuery};
|
||||
else if(dbQuery.length == 1) dbQuery = dbQuery[0];
|
||||
else dbQuery = {};
|
||||
|
||||
return DuplicateSales.find(dbQuery, {sort: {date: -1, productName: 1}});
|
||||
},
|
||||
showHidden: function() {
|
||||
return Session.get(PREFIX + "showHidden") ? "checked": "";
|
||||
}
|
||||
});
|
||||
Template.SaleDuplicates.events({
|
||||
'click .duplicateScan': function(event, template) {
|
||||
Meteor.call("markDuplicateSales", function(err, result) {
|
||||
Meteor.log.error(err);
|
||||
});
|
||||
},
|
||||
'change input[name="showHidden"]': function(event, template) {
|
||||
//console.log("changed " + $(event.target).prop('checked'));
|
||||
Session.set(PREFIX + "showHidden", $(event.target).prop('checked'));
|
||||
}
|
||||
});
|
||||
|
||||
Template.SaleDuplicate.helpers({
|
||||
//measureName: function(id) {
|
||||
// return Meteor.collections.Measures.findOne({_id: id}, {fields: {name: 1}}).name;
|
||||
//},
|
||||
//venueName: function(id) {
|
||||
// return Meteor.collections.Venues.findOne({_id: id}, {fields: {name: 1}}).name;
|
||||
//},
|
||||
//productName: function(id) {
|
||||
// return Meteor.collections.Products.findOne({_id: id}, {fields: {name: 1}}).name;
|
||||
//},
|
||||
formatDateAndWeek: function(date) {
|
||||
return moment.utc(date.toString(), "YYYYMMDD").utc().format("MM/DD/YYYY (w)");
|
||||
},
|
||||
formatDateTime: function(date) {
|
||||
return moment.utc(date).format("MM/DD/YYYY");
|
||||
},
|
||||
formatPrice: function(price) {
|
||||
return price.toLocaleString("en-US", {style: 'currency', currency: 'USD', minimumFractionDigits: 2});
|
||||
},
|
||||
formatTotalPrice: function(price, amount) {
|
||||
return (price * amount).toLocaleString("en-US", {style: 'currency', currency: 'USD', minimumFractionDigits: 2});
|
||||
},
|
||||
showTotalPrice: function(amount) {
|
||||
return amount > 1;
|
||||
},
|
||||
duplicateClasses: function() {
|
||||
return this.ignoreDuplicates ? "hidden" : "";
|
||||
}
|
||||
});
|
||||
Template.SaleDuplicate.events({
|
||||
"click .ignoreDuplicatesButton": function(event, template) {
|
||||
Meteor.call('ignoreDuplicateSales', this._id, function(err, result) {
|
||||
if(err) sAlert.error(err);
|
||||
//else sAlert.success("Duplicates Ignored");
|
||||
});
|
||||
},
|
||||
"click .removeAllDuplicatesButton": function(event, template) {
|
||||
let _this = this;
|
||||
swal({
|
||||
title: "Are you sure?",
|
||||
text: "This will permanently remove ALL duplicate sales.",
|
||||
type: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "Yes"
|
||||
}).then(
|
||||
function(isConfirm) {
|
||||
if(isConfirm) {
|
||||
Meteor.call('removeDuplicateSales', _this._id, function(err, result) {
|
||||
if(err) sAlert.error(err);
|
||||
//else sAlert.success("Duplicates Removed");
|
||||
});
|
||||
}
|
||||
},
|
||||
function(dismiss) {
|
||||
}
|
||||
);
|
||||
},
|
||||
"click .removeOneDuplicateButton": function(event, template) {
|
||||
let _this = this;
|
||||
swal({
|
||||
title: "Are you sure?",
|
||||
text: "This will permanently remove ONE duplicate sale.",
|
||||
type: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "Yes"
|
||||
}).then(
|
||||
function(isConfirm) {
|
||||
if(isConfirm) {
|
||||
Meteor.call('removeDuplicateSales', _this._id, true, function(err, result) {
|
||||
if(err) sAlert.error(err);
|
||||
//else sAlert.success("Duplicates Removed");
|
||||
});
|
||||
}
|
||||
},
|
||||
function(dismiss) {
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Template.SaleDuplicateSearch.helpers({
|
||||
searchValue: function() {
|
||||
let searchFields = Session.get(PREFIX + 'searchFields');
|
||||
|
||||
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
|
||||
}
|
||||
});
|
||||
Template.SaleDuplicateSearch.events({
|
||||
"keyup .searchInput": _.throttle(function(event, template) {
|
||||
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
|
||||
let searchFields = Session.get(PREFIX + 'searchFields') || {};
|
||||
let searchValue = template.$(event.target).val();
|
||||
|
||||
if(searchValue) {
|
||||
if(this.number) searchValue = parseFloat(searchValue);
|
||||
|
||||
// A collection name will be provided if there is a related table of data that will contain the text provided and will map to an ID that is then searched for in the current table of data.
|
||||
// For example we are displaying a table of Sales which has the ID of a Product. The Product table has a Name field and the search box searches for Product Names. The ID's of the Products found should be used to filter the Sales by Product ID.
|
||||
if(this.collection) {
|
||||
let ids = Meteor.collections[this.collection].find({[this.collectionQueryColumnName]: {$regex: searchValue, $options: 'i'}}, {fields: {[this.collectionResultColumnName]: 1}}).fetch();
|
||||
|
||||
//Convert the ids to an array of ids instead of an array of objects containing an id.
|
||||
for(let i = 0; i < ids.length; i++) {ids[i] = ids[i]._id;}
|
||||
searchQuery[this.columnName] = {$in: ids};
|
||||
searchFields[this.columnName] = searchValue;
|
||||
}
|
||||
else {
|
||||
searchFields[this.columnName] = searchQuery[this.columnName] = searchValue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
//Remove columns from the search query whose values are empty so we don't bother the database with them.
|
||||
delete searchQuery[this.columnName];
|
||||
delete searchFields[this.columnName];
|
||||
}
|
||||
|
||||
Session.set(PREFIX + 'searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchFields', searchFields);
|
||||
Session.set(PREFIX + 'skipCount', 0); //Reset the paging of the results.
|
||||
}, 500)
|
||||
});
|
||||
@@ -1,18 +1,23 @@
|
||||
<template name="Sales">
|
||||
<div id="salesMain">
|
||||
{{#if Template.subscriptionsReady}}
|
||||
<div class="tableControls">
|
||||
<select name="sortSelect">
|
||||
<option value="date" selected>Sale Date</option>
|
||||
<option value="createdAt">Data Entry Date</option>
|
||||
</select>
|
||||
<div class="pagination">
|
||||
<span class="prevButton noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextButton noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
<div class="controls">
|
||||
<div class="pageControls">
|
||||
<input type="button" class="showDuplicates btn btn-info" style="margin-right: 30px" value="Duplicate Analysis"/>
|
||||
</div>
|
||||
<div class="tableControls">
|
||||
<select name="sortSelect" class="form-control" style="width: auto; display: inline;">
|
||||
<option value="date" selected>Sale Date</option>
|
||||
<option value="createdAt">Data Entry Date</option>
|
||||
</select>
|
||||
<div class="pagination">
|
||||
<span class="prevButton noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextButton noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="salesListRow">
|
||||
<div class="salesListCell">
|
||||
<div class="listRow">
|
||||
<div class="listCell">
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
@@ -21,7 +26,7 @@
|
||||
<th class="product noselect nonclickable">Product <br/>{{>SaleSearch columnName='productId' collectionQueryColumnName='name' collection='Products' collectionResultColumnName='_id' width='90%'}}</th>
|
||||
<th class="price noselect nonclickable">Price {{>SaleSearch columnName='price' width='90%'}}</th>
|
||||
<th class="measure noselect nonclickable">Measure {{>SaleSearch columnName='measureId' collectionQueryColumnName='name' collection='Measures' collectionResultColumnName='_id' width='90%'}}</th>
|
||||
<th class="saleDate noselect nonclickable">Date (Week)</th>
|
||||
<th class="saleDate noselect nonclickable">Date (Week) {{>DateRangeSearch columnName='date' width='90%'}}</th>
|
||||
<th class="createdDate noselect nonclickable">Created On</th>
|
||||
<th class="venue noselect nonclickable">Venue {{>SaleSearch columnName='venueId' collectionQueryColumnName='name' collection='Venues' collectionResultColumnName='_id' width='90%'}}</th>
|
||||
<th class="actions noselect nonclickable">Actions <span class="newSaleButton btn btn-success" title="Create Sale"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span> <i class="fa fa-commenting fa-lg showOnlyComments clickable" title="Show Commented Sales" aria-hidden="true"></i></th>
|
||||
@@ -55,7 +60,7 @@
|
||||
<td class="tdLarge noselect nonclickable left">{{formatPrice price}}{{#if showTotalPrice amount}} ({{formatTotalPrice price amount}}){{/if}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{measureName measureId}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{formatDateAndWeek date}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{formatDate createdAt}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{formatDateTime createdAt}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{venueName venueId}}</td>
|
||||
<td class="tdLarge noselect left"><i class="fa fa-pencil-square-o fa-lg actionEdit noselect clickable" title="Edit" aria-hidden="true"></i> <i class="fa fa-commenting fa-lg editComment noselect clickable {{commentClass}}" aria-hidden="true"></i> <i class="fa fa-times-circle fa-lg saleRemove noselect clickable" aria-hidden="true"></i></td>
|
||||
</tr>
|
||||
@@ -72,7 +77,7 @@
|
||||
<div class="editorDiv"><label>Venue</label><input name="venue" class="form-control" type="text" required/></div>
|
||||
</div>
|
||||
<div class="col-6-12">
|
||||
<div class="editorDiv"><label>Amount</label><input type="number" class="form-control amount" name="amount" min="0" step="0.01" data-schema-key='amount' value="{{amount}}" required></div>
|
||||
<div class="editorDiv"><label>Amount</label><input type="number" class="form-control amount" name="amount" min="0" step="1" data-schema-key='amount' value="{{amount}}" required></div>
|
||||
<div class="editorDiv"><label>Price</label><div class="priceContainer"><input type="number" class="form-control price" name="price" min="0" step="0.01" data-schema-key='currency' value="{{price}}" required><div class="priceButtons"><i class="fa fa-cogs setDefaultPrice noselect clickable" title="Calculate Default Price" aria-hidden="true"></i></div></div></div>
|
||||
<div class="editorDiv"><label>Total</label><input type="number" class="form-control total" name="total" data-schema-key='currency' value="{{total}}" tabindex="-1" readonly></div>
|
||||
</div>
|
||||
@@ -87,6 +92,10 @@
|
||||
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}" style="padding-right: 10px; width: {{width}}"/>
|
||||
</template>
|
||||
|
||||
<template name="DateRangeSearch">
|
||||
<div style="padding-right: 10px; width: {{width}};"><input type="date" class="searchDateStartInput" value="{{startDate}}" data-schema-key='date' required> - <input type="date" class="searchDateEndInput" value="{{endDate}}" data-schema-key='date' required></div>
|
||||
</template>
|
||||
|
||||
<template name="InsertSale">
|
||||
<tr>
|
||||
<td colspan="8">
|
||||
|
||||
23
imports/ui/Sales.import.styl
vendored
23
imports/ui/Sales.import.styl
vendored
@@ -5,12 +5,23 @@
|
||||
height: 100%
|
||||
width: 100%
|
||||
text-align: left
|
||||
.tableControls
|
||||
text-align: right
|
||||
margin-right: 20px
|
||||
.salesListRow
|
||||
.controls
|
||||
text-align: left
|
||||
display: table
|
||||
width: 100%
|
||||
.pageControls
|
||||
padding: 4px 8px
|
||||
margin: 4px 8px
|
||||
display: table-cell
|
||||
width: 240px
|
||||
.tableControls
|
||||
text-align: right
|
||||
padding: 4px 8px
|
||||
margin: 4px 12px 4px 8px
|
||||
display: table-cell
|
||||
.listRow
|
||||
display: table-row
|
||||
.salesListCell
|
||||
.listCell
|
||||
display: table-cell
|
||||
position: relative
|
||||
height: 100%
|
||||
@@ -70,7 +81,7 @@
|
||||
display: inline-block
|
||||
.fa-times-circle
|
||||
display: none
|
||||
.newSaleButton.active
|
||||
.newSaleButton:active
|
||||
background-color: #fb557b
|
||||
color: black
|
||||
.fa-times-circle
|
||||
|
||||
@@ -3,30 +3,41 @@ import './Sales.html';
|
||||
import '/imports/util/selectize/selectize.js';
|
||||
import swal from 'sweetalert2';
|
||||
|
||||
/**
|
||||
* Notes:
|
||||
* The Sale object has a date field which stores the date as a number in the format YYYYMMDD. Converting this number into a local date is done with moment(sale.date.toString(), "YYYYMMDD").toDate(), and converting it to a number from a date can be accomplished with ~~(moment(date).format("YYYYMMDD")), where the ~~ is a bitwise not and converts a string to a number quickly and reliably.
|
||||
*/
|
||||
|
||||
let QUERY_LIMIT = 20;
|
||||
let PREFIX = "Sales.";
|
||||
|
||||
Meteor.subscribe("products");
|
||||
Session.set(PREFIX + "sortOption", "date");
|
||||
Session.set(PREFIX + "showOnlyComments", false);
|
||||
|
||||
Tracker.autorun(function() {
|
||||
let sortOption = Session.get(PREFIX + "sortOption");
|
||||
let sort = sortOption == 'createdAt' ? {createdAt: -1} : {date: -1, createdAt: -1};
|
||||
let showOnlyComments = Session.get(PREFIX + "showOnlyComments");
|
||||
let query = _.clone(Session.get(PREFIX + 'searchQuery'));
|
||||
|
||||
if(showOnlyComments) {
|
||||
if(!query) query = {};
|
||||
query.comment = {$exists: true};
|
||||
}
|
||||
|
||||
Meteor.subscribe("sales", query, sort, QUERY_LIMIT, Session.get(PREFIX + 'skipCount'));
|
||||
Session.set(PREFIX + 'saleCount', Meteor.call('getSalesCount', Session.get(PREFIX + 'searchQuery')));
|
||||
});
|
||||
|
||||
Template.Sales.onCreated(function() {
|
||||
Session.set(PREFIX + "displayNewSale", false);
|
||||
|
||||
Meteor.subscribe("products");
|
||||
Session.set(PREFIX + "sortOption", "date");
|
||||
Session.set(PREFIX + "showOnlyComments", false);
|
||||
|
||||
Tracker.autorun(function() {
|
||||
let sortOption = Session.get(PREFIX + "sortOption");
|
||||
let sort = sortOption == 'createdAt' ? {createdAt: -1} : {date: -1, createdAt: -1};
|
||||
let showOnlyComments = Session.get(PREFIX + "showOnlyComments");
|
||||
let query = _.clone(Session.get(PREFIX + 'searchQuery'));
|
||||
|
||||
if(showOnlyComments) {
|
||||
if(!query) query = {};
|
||||
query.comment = {$exists: true};
|
||||
}
|
||||
|
||||
//if(Template.Sales.salesSubscription) Template.Sales.salesSubscription.stop();
|
||||
Template.Sales.salesSubscription = Meteor.subscribe("sales", query, sort, QUERY_LIMIT, Session.get(PREFIX + 'skipCount'));
|
||||
Session.set(PREFIX + 'saleCount', Meteor.call('getSalesCount', Session.get(PREFIX + 'searchQuery')));
|
||||
});
|
||||
});
|
||||
Template.Sales.onDestroyed(function() {
|
||||
if(Template.Sales.salesSubscription) {
|
||||
Template.Sales.salesSubscription.stop();
|
||||
}
|
||||
});
|
||||
Template.Sales.helpers({
|
||||
displayNewSale: function() {
|
||||
@@ -77,6 +88,9 @@ Template.Sales.events({
|
||||
|
||||
Session.set(PREFIX + "showOnlyComments", !$button.hasClass('on'));
|
||||
$button.toggleClass('on');
|
||||
},
|
||||
'click .showDuplicates': function(event, template) {
|
||||
FlowRouter.go('SaleDuplicates');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -93,10 +107,10 @@ Template.Sale.helpers({
|
||||
return Meteor.collections.Products.findOne({_id: id}, {fields: {name: 1}}).name;
|
||||
},
|
||||
formatDateAndWeek: function(date) {
|
||||
return moment(date).format("MM/DD/YYYY (w)");
|
||||
return moment.utc(date.toString(), "YYYYMMDD").utc().format("MM/DD/YYYY (w)");
|
||||
},
|
||||
formatDate: function(date) {
|
||||
return moment(date).format("MM/DD/YYYY");
|
||||
formatDateTime: function(date) {
|
||||
return moment.utc(date).format("MM/DD/YYYY");
|
||||
},
|
||||
formatPrice: function(price) {
|
||||
return price.toLocaleString("en-US", {style: 'currency', currency: 'USD', minimumFractionDigits: 2});
|
||||
@@ -162,10 +176,8 @@ Template.Sale.events({
|
||||
});
|
||||
|
||||
Template.SaleEditor.onCreated(function() {
|
||||
let _this = this;
|
||||
|
||||
this.product = Meteor.collections.Products.findOne({_id: this.data.productId});
|
||||
this.selectedDate = new ReactiveVar(this.data.date);
|
||||
this.selectedDate = new ReactiveVar(moment(this.data.date.toString(), "YYYYMMDD").toDate());
|
||||
this.selectedVenue = new ReactiveVar(Meteor.collections.Venues.findOne({_id: this.data.venueId}));
|
||||
this.price = new ReactiveVar(this.data.price);
|
||||
this.amount = new ReactiveVar(this.data.amount);
|
||||
@@ -207,7 +219,7 @@ Template.SaleEditor.events({
|
||||
|
||||
//If this product has pricing data for the given measure, then either use the price, or the previousPrice (if there is one and the effectiveDate is after the sale date).
|
||||
if(priceData) {
|
||||
if(priceData.effectiveDate && date && moment(priceData.effectiveDate).isAfter(date))
|
||||
if(priceData.effectiveDate && date && moment.utc(priceData.effectiveDate.toString(), "YYYYMMDD").isAfter(date))
|
||||
price = priceData.previousPrice;
|
||||
else
|
||||
price = priceData.price
|
||||
@@ -231,7 +243,7 @@ Template.SaleEditor.events({
|
||||
template.$('form[name="editSaleForm"]').data('bs.validator').validate(function(isValid) {
|
||||
if(isValid) {
|
||||
let id = template.data._id;
|
||||
let date = template.selectedDate.get();
|
||||
let date = ~~(moment(template.selectedDate.get()).format("YYYYMMDD")); // Note: The ~~ is a bitwise not that is a fast method of converting a string to a number.
|
||||
let venue = template.selectedVenue.get();
|
||||
let price = template.price.get();
|
||||
let amount = template.amount.get();
|
||||
@@ -245,36 +257,6 @@ Template.SaleEditor.events({
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//let name = template.$("input[name='name']").val().trim();
|
||||
//let tags = template.$(".productTagsEditor").select2('data');
|
||||
//let aliases = template.$(".productAliasesEditor").select2('data');
|
||||
//let measures = template.$(".productMeasuresEditor").select2('data');
|
||||
//
|
||||
//tags = tags.map((n)=>n.id);
|
||||
//aliases = aliases.map((n)=>n.id);
|
||||
//measures = measures.map((n)=>n.id);
|
||||
//
|
||||
//if(Session.get(PREFIX + 'displayNewProduct')) {
|
||||
// Meteor.call("createProduct", name, tags, aliases, measures, function(error, result) {
|
||||
// if(error) sAlert.error(error);
|
||||
// else {
|
||||
// sAlert.success("Product created.");
|
||||
// Session.set(PREFIX + 'displayNewProduct', false);
|
||||
// template.parentTemplate().$('.newProductButton').removeClass('active');
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
//else {
|
||||
// Meteor.call("updateProduct", this._id, name, tags, aliases, measures, function(error, result) {
|
||||
// if(error) sAlert.error(error);
|
||||
// else {
|
||||
// sAlert.success("Product updated.");
|
||||
// Session.set(PREFIX + "editedProduct", undefined);
|
||||
// template.parentTemplate().$('.newProductButton').removeClass('active');
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -289,11 +271,13 @@ Template.SaleSearch.events({
|
||||
"keyup .searchInput": _.throttle(function(event, template) {
|
||||
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
|
||||
let searchFields = Session.get(PREFIX + 'searchFields') || {};
|
||||
let searchValue = template.$('.searchInput').val();
|
||||
let searchValue = template.$(event.target).val();
|
||||
|
||||
if(searchValue) {
|
||||
if(this.number) searchValue = parseFloat(searchValue);
|
||||
|
||||
// A collection name will be provided if there is a related table of data that will contain the text provided and will map to an ID that is then searched for in the current table of data.
|
||||
// For example we are displaying a table of Sales which has the ID of a Product. The Product table has a Name field and the search box searches for Product Names. The ID's of the Products found should be used to filter the Sales by Product ID.
|
||||
if(this.collection) {
|
||||
let ids = Meteor.collections[this.collection].find({[this.collectionQueryColumnName]: {$regex: searchValue, $options: 'i'}}, {fields: {[this.collectionResultColumnName]: 1}}).fetch();
|
||||
|
||||
@@ -313,11 +297,70 @@ Template.SaleSearch.events({
|
||||
}
|
||||
|
||||
Session.set(PREFIX + 'searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchFields', searchFields)
|
||||
Session.set(PREFIX + 'searchFields', searchFields);
|
||||
Session.set(PREFIX + 'skipCount', 0); //Reset the paging of the results.
|
||||
}, 500)
|
||||
});
|
||||
|
||||
Template.DateRangeSearch.helpers({
|
||||
startDate: function() {
|
||||
let searchFields = Session.get(PREFIX + 'searchFields');
|
||||
let searchValue = (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : {};
|
||||
|
||||
return searchValue.start ? moment(searchValue.start.toString(), "YYYYMMDD").format("MM/DD/YYYY") : "";
|
||||
},
|
||||
endDate: function() {
|
||||
let searchFields = Session.get(PREFIX + 'searchFields');
|
||||
let searchValue = (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : {};
|
||||
|
||||
return searchValue.end ? moment(searchValue.end.toString(), "YYYYMMDD").format("MM/DD/YYYY") : "";
|
||||
}
|
||||
});
|
||||
Template.DateRangeSearch.events({
|
||||
"change .searchDateStartInput": function(event, template) {Template.DateRangeSearch.dateChanged(true, event, template)},
|
||||
"keyup .searchDateStartInput": _.throttle(function(event, template) {Template.DateRangeSearch.dateChanged(true, event, template)}, 500),
|
||||
"change .searchDateEndInput": function(event, template) {Template.DateRangeSearch.dateChanged(false, event, template)},
|
||||
"keyup .searchDateEndInput": _.throttle(function(event, template) {Template.DateRangeSearch.dateChanged(false, event, template)}, 500)
|
||||
});
|
||||
Template.DateRangeSearch.dateChanged = function(isStart, event, template) {
|
||||
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
|
||||
let searchFields = Session.get(PREFIX + 'searchFields') || {};
|
||||
let searchValue = template.$(event.target).val();
|
||||
let columnName = template.data.columnName;
|
||||
|
||||
if(searchValue) {
|
||||
let search = searchQuery[columnName];
|
||||
|
||||
// Create a search object and attach it to the searchFields and searchQuery objects if needed.
|
||||
if(!search) {
|
||||
search = {type: 'dateRange'};
|
||||
searchFields[columnName] = searchQuery[columnName] = search;
|
||||
}
|
||||
|
||||
// Use moment to parse date and convert it to YYYYMMDD for searching the database.
|
||||
searchValue = ~~(moment(searchValue, searchValue.includes("-") ? "YYYY-MM-DD" : "MM/DD/YYYY").format("YYYYMMDD")); // Note: ~~ performs a bitwise not which is a fast method of converting a string to a number.
|
||||
// Save the search ending date.
|
||||
isStart ? search.start = searchValue : search.end = searchValue;
|
||||
}
|
||||
else {
|
||||
if(searchQuery[columnName]) {
|
||||
// Remove columns from the search query whose values are empty so we don't bother the database with them.
|
||||
if(isStart) {
|
||||
delete searchQuery[columnName].start;
|
||||
delete searchFields[columnName].start;
|
||||
}
|
||||
else {
|
||||
delete searchQuery[columnName].end;
|
||||
delete searchFields[columnName].end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Session.set(PREFIX + 'searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchFields', searchFields);
|
||||
Session.set(PREFIX + 'skipCount', 0); //Reset the paging of the results.
|
||||
};
|
||||
|
||||
Template.InsertSale.onCreated(function() {
|
||||
this.selectedDate = new ReactiveVar();
|
||||
this.selectedProduct = new ReactiveVar();
|
||||
@@ -351,7 +394,7 @@ Template.InsertSale.events({
|
||||
let insertSaleMeasures = template.$(".insertSaleMeasure");
|
||||
|
||||
let sale = {
|
||||
date: moment(template.find("[name='date']").value, "YYYY-MM-DD").toDate(),
|
||||
date: ~~(moment(template.find("[name='date']").value, "YYYY-MM-DD").format("YYYYMMDD")), // Note: ~~ performs a bitwise not which is a fast method of converting a string to a number.
|
||||
productId: template.selectedProduct.get()._id,
|
||||
venueId: template.selectedVenue.get()._id
|
||||
};
|
||||
@@ -381,6 +424,7 @@ Template.InsertSale.events({
|
||||
if(error) sAlert.error("Failed to insert the sale!\n" + error);
|
||||
else {
|
||||
sAlert.success("Sale Created");
|
||||
nextMeasure.find(".amount").val(0);
|
||||
|
||||
//Clear the measure quantity fields so the user can enter another sale without the quantities already set.
|
||||
for(let next = 0; next < insertSaleMeasures.length; next++) {
|
||||
|
||||
@@ -1,37 +1,44 @@
|
||||
<template name="Venues">
|
||||
<div id="venues">
|
||||
<div class="tableControls">
|
||||
<span class="controlLabel">Show Hidden</span>
|
||||
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
|
||||
<label>
|
||||
<input type="checkbox" name="showHidden"><span></span>
|
||||
</label>
|
||||
{{#if Template.subscriptionsReady}}
|
||||
<div class="tableControls">
|
||||
<span class="controlLabel">Show Hidden</span>
|
||||
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
|
||||
<label>
|
||||
<input type="checkbox" name="showHidden"><span></span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="pagination">
|
||||
<span class="prevVenues noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextVenues noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="pagination">
|
||||
<span class="prevVenues noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextVenues noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="name">Name {{>VenueSearch columnName='name'}}</th>
|
||||
<th class="type">Type {{>VenueSearch columnName='type'}}</th>
|
||||
<th class="actions">Actions <span class="newVenueButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
|
||||
</tr>
|
||||
<!--<button type="button" name="newVenueButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#if displayNewVenue}}
|
||||
{{> VenueEditor isNew=true}}
|
||||
{{/if}}
|
||||
{{#each venues}}
|
||||
{{> Venue}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="listRow">
|
||||
<div class="listCell">
|
||||
<div class="tableContainer">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="name">Name {{>VenueSearch columnName='name'}}</th>
|
||||
<th class="type">Type {{>VenueSearch columnName='type'}}</th>
|
||||
<th class="actions">Actions <span class="newVenueButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
|
||||
</tr>
|
||||
<!--<button type="button" name="newVenueButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#if displayNewVenue}}
|
||||
{{> VenueEditor isNew=true}}
|
||||
{{/if}}
|
||||
{{#each venues}}
|
||||
{{> Venue}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
163
imports/ui/Venues.import.styl
vendored
163
imports/ui/Venues.import.styl
vendored
@@ -1,6 +1,9 @@
|
||||
#venues
|
||||
margin: 20px 20px
|
||||
display: table
|
||||
content-box: border-box
|
||||
padding: 10px 20px
|
||||
height: 100%
|
||||
width: 100%
|
||||
text-align: left
|
||||
|
||||
.tableControls
|
||||
@@ -18,77 +21,89 @@
|
||||
top: -4px
|
||||
display: inline-block
|
||||
|
||||
.tableContainer
|
||||
width: 100%
|
||||
margin-bottom: 20px
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
|
||||
table
|
||||
table-layout: fixed
|
||||
.listRow
|
||||
display: table-row
|
||||
.listCell
|
||||
display: table-cell
|
||||
position: relative
|
||||
height: 100%
|
||||
width: 100%
|
||||
.venueSearch
|
||||
margin: 3px 0 2px 1px
|
||||
.venueEditorTd
|
||||
background: #deeac0
|
||||
input[name="name"], input[name="type"]
|
||||
width: 100%
|
||||
.editorDiv
|
||||
margin: 4px 0
|
||||
label
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-size: .9em
|
||||
padding-bottom: 4px
|
||||
select2
|
||||
font-size: .4em
|
||||
> thead
|
||||
> tr
|
||||
> th.name
|
||||
width: auto
|
||||
> th.type
|
||||
width: auto
|
||||
> th.actions
|
||||
width: 90px
|
||||
text-align: center
|
||||
.newVenueButton
|
||||
margin-top: 4px
|
||||
padding: 0px 12px
|
||||
.fa-plus-circle
|
||||
display: inline-block
|
||||
.fa-times-circle
|
||||
display: none
|
||||
.newVenueButton.active
|
||||
background-color: #fb557b
|
||||
color: black
|
||||
.fa-times-circle
|
||||
display: inline-block
|
||||
.fa-plus-circle
|
||||
display: none
|
||||
> tbody
|
||||
> tr
|
||||
.actionRemove
|
||||
color: #F77
|
||||
.actionEdit
|
||||
color: #44F
|
||||
.editorApply
|
||||
color: green
|
||||
.editorCancel
|
||||
color: red
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
.actionActivate
|
||||
color: #158b18
|
||||
.actionHide
|
||||
color: #6a0707
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
> tr.hidden
|
||||
background-color: #e995ff
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
.actionShow
|
||||
color: #027905
|
||||
> tr.hidden:hover
|
||||
background-color: #ffb5ff
|
||||
.tableContainer
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
width: auto
|
||||
height: auto
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
overflow-y: auto
|
||||
table
|
||||
table-layout: fixed
|
||||
width: 100%
|
||||
.venueSearch
|
||||
margin: 3px 0 2px 1px
|
||||
.venueEditorTd
|
||||
background: #deeac0
|
||||
input[name="name"], input[name="type"]
|
||||
width: 100%
|
||||
.editorDiv
|
||||
margin: 4px 0
|
||||
label
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-size: .9em
|
||||
padding-bottom: 4px
|
||||
select2
|
||||
font-size: .4em
|
||||
> thead
|
||||
> tr
|
||||
> th.name
|
||||
width: auto
|
||||
> th.type
|
||||
width: auto
|
||||
> th.actions
|
||||
width: 90px
|
||||
text-align: center
|
||||
.newVenueButton
|
||||
margin-top: 4px
|
||||
padding: 0px 12px
|
||||
.fa-plus-circle
|
||||
display: inline-block
|
||||
.fa-times-circle
|
||||
display: none
|
||||
.newVenueButton.active
|
||||
background-color: #fb557b
|
||||
color: black
|
||||
.fa-times-circle
|
||||
display: inline-block
|
||||
.fa-plus-circle
|
||||
display: none
|
||||
> tbody
|
||||
> tr
|
||||
.actionRemove
|
||||
color: #F77
|
||||
.actionEdit
|
||||
color: #44F
|
||||
.editorApply
|
||||
color: green
|
||||
.editorCancel
|
||||
color: red
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
.actionActivate
|
||||
color: #158b18
|
||||
.actionHide
|
||||
color: #6a0707
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
> tr.hidden
|
||||
background-color: #e995ff
|
||||
.actionEdit
|
||||
color: #0101e4
|
||||
.actionShow
|
||||
color: #027905
|
||||
> tr.hidden:hover
|
||||
background-color: #ffb5ff
|
||||
@@ -17,6 +17,11 @@
|
||||
User Management
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{isActiveRoute 'MiscManagement'}}">
|
||||
<a href="{{pathFor 'MiscManagement'}}">
|
||||
Misc Management
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
<li class="{{isActiveRoute 'Sales'}}">
|
||||
<a href="{{pathFor 'Sales'}}">
|
||||
|
||||
Reference in New Issue
Block a user