Added graphs and charts; Updated a lot of the collections for security and consistency; Updated all of the page to fix bugs and propagate fixes to all templates; Added the d3 library for graphing; Added a real ui for Measures and Venues.
This commit is contained in:
2
.idea/PetitTetonMeteor.iml
generated
2
.idea/PetitTetonMeteor.iml
generated
@@ -9,7 +9,7 @@
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="meteor-packages-auto-import-browser" level="project" />
|
||||
<orderEntry type="library" name="meteor-packages-auto-import-npm" level="project" />
|
||||
<orderEntry type="library" name="meteor-packages-auto-import-browser" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
2
.idea/dictionaries/Grumpy.xml
generated
2
.idea/dictionaries/Grumpy.xml
generated
@@ -2,7 +2,9 @@
|
||||
<dictionary name="Grumpy">
|
||||
<words>
|
||||
<w>clickable</w>
|
||||
<w>dataset</w>
|
||||
<w>noselect</w>
|
||||
<w>perserve</w>
|
||||
<w>signup</w>
|
||||
<w>styl</w>
|
||||
<w>yorkville</w>
|
||||
|
||||
@@ -234,7 +234,10 @@
|
||||
<item url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/row_trash/main.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/_component/component.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/doc_editor/inline.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/jcbernack_reactive-aggregate/0.7.0/web.browser/aggregate.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/meteorhacks_aggregate/1.3.0/web.browser/packages/meteorhacks_aggregate.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/doc_editor/docViewer.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/meteorhacks_collection-utils/1.2.0/web.browser/packages/meteorhacks_collection-utils.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/doc_insert/docInsert.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/row_header/header.js" />
|
||||
<item url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/row_account/account.js" />
|
||||
@@ -643,7 +646,10 @@
|
||||
<root url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/row_trash/main.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/_component/component.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/doc_editor/inline.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/jcbernack_reactive-aggregate/0.7.0/web.browser/aggregate.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/meteorhacks_aggregate/1.3.0/web.browser/packages/meteorhacks_aggregate.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/doc_editor/docViewer.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/meteorhacks_collection-utils/1.2.0/web.browser/packages/meteorhacks_collection-utils.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/doc_insert/docInsert.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/row_header/header.js" />
|
||||
<root url="file://C:/Tools/.meteor/packages/msavin_mongol/2.0.1/web.browser/client/row_account/account.js" />
|
||||
|
||||
958
.idea/workspace.xml
generated
958
.idea/workspace.xml
generated
File diff suppressed because it is too large
Load Diff
@@ -59,3 +59,4 @@ momentjs:moment
|
||||
mizzao:bootboxjs # ???
|
||||
aldeed:template-extension
|
||||
juliancwirko:s-alert # Client error/alert handling
|
||||
jcbernack:reactive-aggregate
|
||||
|
||||
@@ -43,6 +43,7 @@ html-tools@1.0.11
|
||||
htmljs@1.0.11
|
||||
http@1.1.8
|
||||
id-map@1.0.9
|
||||
jcbernack:reactive-aggregate@0.7.0
|
||||
jquery@1.11.10
|
||||
juliancwirko:s-alert@3.2.0
|
||||
kadira:blaze-layout@2.3.0
|
||||
@@ -55,6 +56,8 @@ matb33:collection-hooks@0.8.4
|
||||
mdg:validation-error@0.2.0
|
||||
meteor@1.6.0
|
||||
meteor-base@1.0.4
|
||||
meteorhacks:aggregate@1.3.0
|
||||
meteorhacks:collection-utils@1.2.0
|
||||
meteortoys:allthings@3.0.0
|
||||
meteortoys:authenticate@3.0.0
|
||||
meteortoys:autopub@3.0.0
|
||||
@@ -81,6 +84,7 @@ modules-runtime@0.7.7
|
||||
momentjs:moment@2.17.1
|
||||
mongo@1.1.14
|
||||
mongo-id@1.0.6
|
||||
mongo-livedata@1.0.12
|
||||
msavin:jetsetter@2.0.0
|
||||
msavin:mongol@2.0.1
|
||||
npm-bcrypt@0.9.2
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<head>
|
||||
<title>PT App</title>
|
||||
<!--<meta http-equiv="content-type" content="text/html; charset=UTF8">-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF8">
|
||||
</head>
|
||||
@@ -298,8 +298,10 @@ input, textarea, keygen, select, button, meter, progress
|
||||
|
||||
@import "../imports/ui/UserManagement.import.styl"
|
||||
@import "../imports/ui/Measures.import.styl"
|
||||
@import "../imports/ui/Venues.import.styl"
|
||||
@import "../imports/ui/Products.import.styl"
|
||||
@import "../imports/ui/ProductTags.import.styl"
|
||||
@import "../imports/ui/Sales.import.styl"
|
||||
@import "../imports/ui/Pricing.import.styl"
|
||||
@import "../imports/ui/Production.import.styl"
|
||||
@import "../imports/ui/Graphs.import.styl"
|
||||
@@ -45,82 +45,75 @@ Measures.attachSchema(new SimpleSchema({
|
||||
// denyInsert: true,
|
||||
optional: true
|
||||
},
|
||||
deletedAt: {
|
||||
type: Date,
|
||||
label: "Deleted On",
|
||||
deactivated: {
|
||||
type: Boolean,
|
||||
label: "Deactivated",
|
||||
optional: true
|
||||
},
|
||||
deletedBy: {
|
||||
type: String,
|
||||
label: "Deleted By",
|
||||
optional: true
|
||||
},
|
||||
restoredAt: {
|
||||
type: Date,
|
||||
label: "Restored On",
|
||||
optional: true
|
||||
},
|
||||
restoredBy: {
|
||||
type: String,
|
||||
label: "Restored By",
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
label: "Hidden",
|
||||
optional: true
|
||||
}
|
||||
}));
|
||||
|
||||
//https://github.com/zimme/meteor-collection-softremovable
|
||||
Measures.attachBehaviour("softRemovable", {
|
||||
removed: 'deleted',
|
||||
removedAt: 'deletedAt',
|
||||
removedBy: 'removedBy',
|
||||
restoredAt: 'restoredAt',
|
||||
restoredBy: 'restoredBy'
|
||||
});
|
||||
|
||||
if(Meteor.isServer) Meteor.publish('measures', function() {
|
||||
return Measures.find({});
|
||||
});
|
||||
|
||||
// Requires: meteor add matb33:collection-hooks
|
||||
Measures.before.insert(function(userId, doc) {
|
||||
// check(userId, String);
|
||||
doc.createdAt = new Date();
|
||||
});
|
||||
Measures.before.update(function(userId, doc, fieldNames, modifier, options) {
|
||||
modifier.$set = modifier.$set || {}; //Make sure there is an object.
|
||||
modifier.$set.updatedAt = new Date();
|
||||
});
|
||||
|
||||
if(Meteor.isServer) {
|
||||
Meteor.methods({
|
||||
insertMeasure: function(measure) {
|
||||
check(measure, {
|
||||
name: String,
|
||||
order: Number,
|
||||
postfix: String
|
||||
});
|
||||
|
||||
measure.createdAt = new Date();
|
||||
createMeasure: function(name, postfix, order) {
|
||||
check(name, String);
|
||||
check(postfix, String);
|
||||
check(order, Number);
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Measures.insert(measure);
|
||||
Measures.insert({name, postfix, order, createdAt: new Date()});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
deleteMeasure: function(id) {
|
||||
deactivateMeasure: function(id) {
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Measures.remove(id);
|
||||
//Measures.remove(id);
|
||||
Measures.update(id, {$set: {deactivated: true}}, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
updateMeasure: function(measure) {
|
||||
check(measure, {
|
||||
name: String,
|
||||
order: Number,
|
||||
postfix: String
|
||||
});
|
||||
reactivateMeasure: function(id) {
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Measures.update(id, {$set: {deactivated: false}}, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
hideMeasure: function(id) { //One step past deactivated - will only show in the measures list if hidden measures are enabled.
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
//Measures.remove(id);
|
||||
Measures.update(id, {$set: {hidden: true}}, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
showMeasure: function(id) { //Returns the measure to being simply deactivated. Will again show in lists.
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Measures.update(id, {$set: {hidden: false}}, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
//deleteMeasure: function(id) {
|
||||
// if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
// //TODO: Should troll the database looking for references to remove or replace. This is currently not used.
|
||||
// Measures.remove(id);
|
||||
// }
|
||||
// else throw new Meteor.Error(403, "Not authorized.");
|
||||
//},
|
||||
updateMeasure: function(id, name, postfix, order) {
|
||||
check(id, String);
|
||||
check(name, String);
|
||||
check(postfix, String);
|
||||
check(order, Number);
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Products.update(id, {$set: {name: measure.name, order: measure.order, postfix: measure.postfix, updateAt: new Date()}});
|
||||
Products.update(id, {$set: {name, postfix, order, updatedAt: new Date()}});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ if(Meteor.isServer) {
|
||||
if(measures) check(measures, [String]);
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Products.update(id, {$set: {name: name, tags: tags, aliases: aliases, measures: measures, updateAt: new Date()}}, {bypassCollection2: true});
|
||||
Products.update(id, {$set: {name: name, tags: tags, aliases: aliases, measures: measures, updatedAt: new Date()}}, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
|
||||
@@ -83,7 +83,7 @@ if(Meteor.isServer) {
|
||||
name: String
|
||||
});
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
ProductTags.update(tag._id, {$set: {name: tag.name, updateAt: new Date()}});
|
||||
ProductTags.update(tag._id, {$set: {name: tag.name, updatedAt: new Date()}});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
}
|
||||
|
||||
@@ -92,6 +92,86 @@ if(Meteor.isServer) {
|
||||
dbQuery = dbQuery.length > 0 ? {$and: dbQuery} : {};
|
||||
return Meteor.collections.Sales.find(dbQuery, {limit: limit, sort: {date: -1, createdAt: -1}, skip: skipCount});
|
||||
});
|
||||
// time: expects either undefined, 'weekly', or 'monthly'
|
||||
// options: expects either undefined, 'markets', or 'types'
|
||||
Meteor.publish('salesTotals', function(time, options) {
|
||||
let pipeline = [];
|
||||
let group = {
|
||||
$group: {
|
||||
_id: {
|
||||
year: {$dateToString: {format: '%Y', date: '$date'}}//{$year: '$date'}
|
||||
},
|
||||
'total': {
|
||||
$sum: {
|
||||
$multiply: ['$price', '$amount']
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let project = {
|
||||
$project: {
|
||||
year: '$_id.year',
|
||||
date: '$_id.year',
|
||||
total: true,
|
||||
_id: {$concat: ['$_id.year']}
|
||||
}
|
||||
};
|
||||
|
||||
pipeline.push(group);
|
||||
pipeline.push(project);
|
||||
|
||||
//Annual is assumed if not week or month.
|
||||
if(time === 'weekly') {
|
||||
group.$group._id.week = {$dateToString: {format: '%U', date: '$date'}}; //{$week: '$date'};
|
||||
project.$project.week = '$_id.week';
|
||||
project.$project.date = {$concat: ['$_id.week', '-', '$_id.year']};
|
||||
project.$project._id.$concat.push('$_id.week');
|
||||
}
|
||||
else if(time === 'monthly') {
|
||||
group.$group._id.month = {$dateToString: {format: '%m', date: '$date'}}; //{$month: '$date'};
|
||||
project.$project.month = '$_id.month';
|
||||
project.$project.date = {$concat: ['$_id.month', '-', '$_id.year']};
|
||||
project.$project._id.$concat.push('$_id.month');
|
||||
}
|
||||
|
||||
if(options === 'markets') {
|
||||
group.$group._id.venueId = '$venueId';
|
||||
project.$project.venueId = '$_id.venueId';
|
||||
project.$project._id.$concat.push('$_id.venueId');
|
||||
pipeline.push({$lookup: {from: 'Venues', localField: 'venueId', foreignField: '_id', as: 'venue'}});
|
||||
pipeline.push({$project: {year: 1, week: 1, month: 1, total: 1, venueId: 1, venue: {$arrayElemAt: ['$venue', 0]}}});
|
||||
pipeline.push({$project: {year: 1, week: 1, month: 1, total: 1, venueId: 1, venue: '$venue.name'}});
|
||||
}
|
||||
else if(options === 'types') {
|
||||
//query[0].$group.month = {$month: '$date'};
|
||||
//TODO: Need to divide the sales up by:
|
||||
// Sweets
|
||||
// Savories
|
||||
// Meats
|
||||
// VAP
|
||||
// Egg
|
||||
// Other Produce
|
||||
// Total Produce
|
||||
// Jars
|
||||
}
|
||||
|
||||
|
||||
ReactiveAggregate(this, Sales, pipeline, {clientCollection: 'salesTotals'});
|
||||
|
||||
/*
|
||||
{$sales: {
|
||||
_id: Random.id(),
|
||||
year: $_id.$year,
|
||||
total: $total
|
||||
}}
|
||||
*/
|
||||
//ReactiveAggregate(this, Sales, [query], {clientCollection: 'salesTotals', transform: function(doc) {
|
||||
// console.log("Running transform function");
|
||||
// Object.assign(doc._id, doc);
|
||||
// doc._id = Random.id();
|
||||
// return doc;
|
||||
//}});
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
getSalesCount: function(query) {
|
||||
@@ -105,7 +185,7 @@ if(Meteor.isServer) {
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Sales.insert(sale, function(err, id) {
|
||||
if(err) console.log(err);
|
||||
});
|
||||
}, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
@@ -113,7 +193,7 @@ if(Meteor.isServer) {
|
||||
check(id, String);
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Sales.remove(id);
|
||||
Sales.remove(id, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
}
|
||||
|
||||
@@ -29,38 +29,19 @@ let VenuesSchema = new SimpleSchema({
|
||||
label: "Updated On",
|
||||
optional: true
|
||||
},
|
||||
deletedAt: {
|
||||
type: Date,
|
||||
label: "Deleted On",
|
||||
deactivated: {
|
||||
type: Boolean,
|
||||
label: "Deactivated",
|
||||
optional: true
|
||||
},
|
||||
deletedBy: {
|
||||
type: String,
|
||||
label: "Deleted By",
|
||||
optional: true
|
||||
},
|
||||
restoredAt: {
|
||||
type: Date,
|
||||
label: "Restored On",
|
||||
optional: true
|
||||
},
|
||||
restoredBy: {
|
||||
type: String,
|
||||
label: "Restored By",
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
label: "Hidden",
|
||||
optional: true
|
||||
}
|
||||
});
|
||||
Venues.attachSchema(VenuesSchema);
|
||||
|
||||
//https://github.com/zimme/meteor-collection-softremovable
|
||||
Venues.attachBehaviour("softRemovable", {
|
||||
removed: 'deleted',
|
||||
removedAt: 'deletedAt',
|
||||
removedBy: 'removedBy',
|
||||
restoredAt: 'restoredAt',
|
||||
restoredBy: 'restoredBy'
|
||||
});
|
||||
|
||||
if(Meteor.isServer) Meteor.publish('venues', function() {
|
||||
return Venues.find({});
|
||||
});
|
||||
@@ -77,54 +58,58 @@ if(Meteor.isServer) {
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
insertVenue: function(venue) {
|
||||
check(venue, {
|
||||
name: String,
|
||||
type: String
|
||||
});
|
||||
|
||||
venue.createdAt = new Date();
|
||||
createVenue: function(name, type) {
|
||||
check(name, String);
|
||||
check(type, String);
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Venues.insert(venue);
|
||||
Venues.insert({name, type, createdAt: new Date()});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
deleteVenue: function(id) {
|
||||
deactivateVenue: function(id) {
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Venues.remove(id);
|
||||
//Venues.remove(id);
|
||||
Venues.update(id, {$set: {deactivated: true}}, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
updateVenue: function(venue) {
|
||||
check(venue, {
|
||||
name: String,
|
||||
type: String
|
||||
});
|
||||
reactivateVenue: function(id) {
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Venues.update(id, {$set: {deactivated: false}}, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
hideVenue: function(id) { //One step past deactivated - will only show in the venues list if hidden venues are enabled.
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
//Venues.remove(id);
|
||||
Venues.update(id, {$set: {hidden: true}}, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
showVenue: function(id) { //Returns the venue to being simply deactivated. Will again show in lists.
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Venues.update(id, {$set: {hidden: false}}, {bypassCollection2: true});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
},
|
||||
//deleteVenue: function(id) {
|
||||
// if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
// Venues.remove(id); //TODO: If this is ever allowed, we should either remove or replace references to the deleted venue in the rest of the database.
|
||||
// }
|
||||
// else throw new Meteor.Error(403, "Not authorized.");
|
||||
//},
|
||||
updateVenue: function(id, name, type) {
|
||||
check(id, String);
|
||||
check(name, String);
|
||||
check(type, String);
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Venues.update(id, {$set: {name: venue.name, type: venue.type, updateAt: new Date()}});
|
||||
Venues.update(id, {$set: {name, type, updatedAt: new Date()}});
|
||||
}
|
||||
else throw new Meteor.Error(403, "Not authorized.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Allows the client to do DB interaction without calling server side methods, while still retaining control over whether the user can make changes.
|
||||
// Meteor.allow({
|
||||
// insert: true,
|
||||
// update: ()->{return true},
|
||||
// remove: checkUser
|
||||
// };
|
||||
// checkUser = function(userId, doc) {
|
||||
// return doc && doc.userId === userId;
|
||||
// };
|
||||
|
||||
//Allows the client to interact with the db through the server via custom methods.
|
||||
// Meteor.methods({
|
||||
// deleteMeasure: function(id) {
|
||||
// Measures.remove(id);
|
||||
// }
|
||||
// });
|
||||
|
||||
export default Venues;
|
||||
@@ -57,3 +57,24 @@ pri.route('/pricing', {
|
||||
BlazeLayout.render('Body', {content: 'Pricing'});
|
||||
}
|
||||
});
|
||||
pri.route('/venues', {
|
||||
name: 'Venues',
|
||||
action: function(params, queryParams) {
|
||||
require("/imports/ui/Venues.js");
|
||||
BlazeLayout.render('Body', {content: 'Venues'});
|
||||
}
|
||||
});
|
||||
pri.route('/graphs', {
|
||||
name: 'Graphs',
|
||||
action: function(params, queryParams) {
|
||||
require("/imports/ui/Graphs.js");
|
||||
BlazeLayout.render('Body', {content: 'Graphs'});
|
||||
}
|
||||
});
|
||||
pri.route('/graphTest', {
|
||||
name: 'GraphTest',
|
||||
action: function(params, queryParams) {
|
||||
require("/imports/ui/GraphTest.js");
|
||||
BlazeLayout.render('Body', {content: 'GraphTest'});
|
||||
}
|
||||
});
|
||||
28
imports/ui/GraphTest.html
Normal file
28
imports/ui/GraphTest.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<template name="GraphTest">
|
||||
<div id="graphTest" style="margin: 20px">
|
||||
<button name="changeData">Change Data</button>
|
||||
|
||||
<svg class="salesGraph"></svg>
|
||||
|
||||
<div class="salesTable">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<!--<th class="month">Month</th>-->
|
||||
<th class="year">Year</th>
|
||||
<th class="total">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each sales}}
|
||||
<tr>
|
||||
<!--<td>{{month}}</td>-->
|
||||
<td>{{year}}</td>
|
||||
<td>{{formatTotal total}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
38
imports/ui/GraphTest.import.styl
vendored
Normal file
38
imports/ui/GraphTest.import.styl
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
#graphTest
|
||||
margin: 10px 20px
|
||||
height: 100%
|
||||
svg
|
||||
width: 100%
|
||||
.bar
|
||||
fill: steelblue
|
||||
.xAxisLabels
|
||||
font-size: 16px
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-weight: 800
|
||||
.yAxisLabels
|
||||
font-size: 12px
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-weight: 800
|
||||
.barText
|
||||
font-size: 14px
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-weight: 800
|
||||
table
|
||||
table-layout: fixed
|
||||
> thead
|
||||
> tr
|
||||
> th.total
|
||||
width: 200px
|
||||
> th.market
|
||||
width: 200px
|
||||
> th.week
|
||||
width: 200px
|
||||
> th.month
|
||||
width: 200px
|
||||
> th.year
|
||||
width: 200px
|
||||
> tbody
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
232
imports/ui/GraphTest.js
Normal file
232
imports/ui/GraphTest.js
Normal file
@@ -0,0 +1,232 @@
|
||||
|
||||
import './GraphTest.html';
|
||||
import d3 from 'd3';
|
||||
|
||||
let SalesTotals = new Meteor.Collection(null);
|
||||
|
||||
function changeData() {
|
||||
if(SalesTotals.find({}).count() > 0) {
|
||||
let sales = SalesTotals.find({}).fetch();
|
||||
|
||||
for(let sale of sales) {
|
||||
SalesTotals.update(sale._id, {$set: {year: sale.year, total: Math.round(Math.random() * 10000) / 100}});
|
||||
}
|
||||
}
|
||||
else {
|
||||
let startYear = Math.round(Math.random() * 10) + 2000;
|
||||
let yearCount = 4;
|
||||
|
||||
for(let i = 0; i < yearCount; i++)
|
||||
//for(let m = 0; m < 12; m++)
|
||||
SalesTotals.insert({year: startYear + i/*, month: m*/, total: Math.round(Math.random() * 10000) / 100});
|
||||
}
|
||||
}
|
||||
|
||||
Template.GraphTest.onCreated(function() {
|
||||
let template = Template.instance();
|
||||
|
||||
changeData();
|
||||
});
|
||||
Template.GraphTest.onRendered(function() {
|
||||
////Build the SVG Graphs
|
||||
let margin = {top: 20, right: 20, bottom: 30, left: 80};
|
||||
let width = 960 - margin.left - margin.right;
|
||||
let height = 500 - margin.top - margin.bottom;
|
||||
let x0Scale = d3.scaleBand().range([0, width]).padding(0.1);
|
||||
let yScale = d3.scaleLinear().range([height, 0]);
|
||||
let svg = d3.select('svg.salesGraph')
|
||||
.attr("viewBox", "0 0 960 500")
|
||||
.attr("perserveAspectRatio", "xMidYMid meet")
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
let xLabels = svg.append("g").attr("transform", "translate(0, " + height + ")");
|
||||
let yLabels = svg.append("g");
|
||||
|
||||
Tracker.autorun(function() {
|
||||
let dataset = SalesTotals.find({}).fetch();
|
||||
|
||||
//Update scale domains
|
||||
x0Scale.domain([...new Set(dataset.map(item => item.year))]);
|
||||
yScale.domain([0, d3.max(dataset, function(d) {return d.total})]);
|
||||
|
||||
//Join the data set with the existing data in the svg element.
|
||||
let bars = svg.selectAll('.bar').data(dataset);
|
||||
|
||||
//Handle existing elements.
|
||||
// bars.[do something here ie: attr('class', 'oldValues')]
|
||||
//Handle the new data elements by adding them to the svg element.
|
||||
bars.enter().append('rect')
|
||||
.attr('class', 'bar')
|
||||
.attr('x', function(d) {
|
||||
return x0Scale(d.year);
|
||||
})
|
||||
.attr('width', x0Scale.bandwidth())
|
||||
.attr('y', function(d) {
|
||||
return yScale(d.total);
|
||||
})
|
||||
.attr('height', function(d) {
|
||||
return height - yScale(d.total);
|
||||
});
|
||||
|
||||
bars.transition()
|
||||
// .delay(function(d, i) {
|
||||
// return i / dataset.length * 1000;
|
||||
// }) // this delay will make transistions sequential instead of parallel
|
||||
.duration(500)
|
||||
.attr("x", function(d, i) {
|
||||
return x0Scale(d.year);
|
||||
})
|
||||
.attr("y", function(d) {
|
||||
return yScale(d.total);
|
||||
})
|
||||
.attr("width", x0Scale.bandwidth())
|
||||
.attr("height", function(d) {
|
||||
return height - yScale(d.total);
|
||||
});
|
||||
//.attr("fill", function(d) {
|
||||
// return "rgb(0, 0, " + (d.total * 10) + ")";
|
||||
//});
|
||||
|
||||
bars.exit()
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr("x", -x0Scale.bandwidth())
|
||||
.remove();
|
||||
|
||||
let barTexts = svg.selectAll('text.barText').data(dataset);
|
||||
|
||||
barTexts.enter().append("text")
|
||||
.attr("class", "barText")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr('x', function(d) {return x0Scale(d.year) + x0Scale.bandwidth() / 2;})
|
||||
.attr('y', function(d) {return yScale(d.total) - 2;})
|
||||
.text(function(d) {return "$" + Math.round(d.total)});
|
||||
|
||||
barTexts.transition()
|
||||
.duration(500)
|
||||
.attr('x', function(d) {return x0Scale(d.year) + x0Scale.bandwidth() / 2;})
|
||||
.attr('y', function(d) {return yScale(d.total) - 2;})
|
||||
.text(function(d) {return "$" + Math.round(d.total)});
|
||||
|
||||
barTexts.exit()
|
||||
.remove();
|
||||
|
||||
//Add the x & y axis labels
|
||||
xLabels.attr("class", "xAxisLabels").call(d3.axisBottom(x0Scale));
|
||||
yLabels.attr("class", "yAxisLabels").call(d3.axisLeft(yScale));
|
||||
});
|
||||
});
|
||||
Template.GraphTest.helpers({
|
||||
sales: function() {
|
||||
let sort = [];
|
||||
|
||||
sort.push(['year', 'asc']);
|
||||
|
||||
return SalesTotals.find({}, {sort: sort});
|
||||
},
|
||||
formatTotal: function(total) {
|
||||
return "$" + total.toFixed(2);
|
||||
}
|
||||
});
|
||||
Template.GraphTest.events({
|
||||
'click button[name="changeData"]': function(event, template) {
|
||||
changeData();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
//Select…
|
||||
let bars = svg.selectAll("rect").data(dataset, key);
|
||||
|
||||
//Enter…
|
||||
bars.enter()
|
||||
.append("rect")
|
||||
.attr("x", w)
|
||||
.attr("y", function(d) {
|
||||
return h - yScale(d.total);
|
||||
})
|
||||
//.attr("width", x0Scale.rangeBand())
|
||||
.attr("width", x0Scale.bandwidth())
|
||||
.attr("height", function(d) {
|
||||
return yScale(d.total);
|
||||
})
|
||||
.attr("fill", function(d) {
|
||||
return "rgb(0, 0, " + (d.total * 10) + ")";
|
||||
})
|
||||
.attr("data-id", function(d){
|
||||
return d._id;
|
||||
});
|
||||
|
||||
//Update…
|
||||
bars.transition()
|
||||
// .delay(function(d, i) {
|
||||
// return i / dataset.length * 1000;
|
||||
// }) // this delay will make transistions sequential instead of paralle
|
||||
.duration(500)
|
||||
.attr("x", function(d, i) {
|
||||
return x0Scale(i);
|
||||
})
|
||||
.attr("y", function(d) {
|
||||
return h - yScale(d.total);
|
||||
})
|
||||
.attr("width", x0Scale.bandwidth())
|
||||
.attr("height", function(d) {
|
||||
return yScale(d.total);
|
||||
}).attr("fill", function(d) {
|
||||
return "rgb(0, 0, " + (d.total * 10) + ")";
|
||||
});
|
||||
|
||||
//Exit…
|
||||
bars.exit()
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr("x", -x0Scale.bandwidth())
|
||||
.remove();
|
||||
|
||||
|
||||
|
||||
//Update all labels
|
||||
|
||||
//Select…
|
||||
let labels = svg.selectAll("text")
|
||||
.data(dataset, key);
|
||||
|
||||
//Enter…
|
||||
labels.enter()
|
||||
.append("text")
|
||||
.text(function(d) {
|
||||
return d.total;
|
||||
})
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("x", w)
|
||||
.attr("y", function(d) {
|
||||
return h - yScale(d.total) + 14;
|
||||
})
|
||||
.attr("font-family", "sans-serif")
|
||||
.attr("font-size", "11px")
|
||||
.attr("fill", "white");
|
||||
|
||||
//Update…
|
||||
labels.transition()
|
||||
// .delay(function(d, i) {
|
||||
// return i / dataset.length * 1000;
|
||||
// }) // this delay will make transistions sequential instead of paralle
|
||||
.duration(500)
|
||||
.attr("x", function(d, i) {
|
||||
return x0Scale(i) + x0Scale.bandwidth() / 2;
|
||||
}).attr("y", function(d) {
|
||||
return h - yScale(d.total) + 14;
|
||||
}).text(function(d) {
|
||||
return d.total;
|
||||
});
|
||||
|
||||
//Exit…
|
||||
labels.exit()
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr("x", -x0Scale.bandwidth())
|
||||
.remove();
|
||||
*/
|
||||
53
imports/ui/Graphs.html
Normal file
53
imports/ui/Graphs.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<template name="Graphs">
|
||||
<div id="graphs">
|
||||
<label>Time</label>
|
||||
<select name="time">
|
||||
<option value="annual" selected>Annual</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
</select>
|
||||
<label>Options</label>
|
||||
<select name="options">
|
||||
<option value="none" selected>None</option>
|
||||
<option value="markets">Markets</option>
|
||||
<option value="types">Types</option>
|
||||
</select>
|
||||
<svg class="salesGraph"></svg>
|
||||
<div class="salesTable">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="year">Year</th>
|
||||
{{#if showTime "monthly"}}
|
||||
<th class="month">Month</th>
|
||||
{{/if}}
|
||||
{{#if showTime "weekly"}}
|
||||
<th class="week">Week</th>
|
||||
{{/if}}
|
||||
{{#if showOption "markets"}}
|
||||
<th class="market">Market</th>
|
||||
{{/if}}
|
||||
<th class="total">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each sales}}
|
||||
<tr>
|
||||
<td>{{year}}</td>
|
||||
{{#if showTime "monthly"}}
|
||||
<td>{{month}}</td>
|
||||
{{/if}}
|
||||
{{#if showTime "weekly"}}
|
||||
<td>{{week}}</td>
|
||||
{{/if}}
|
||||
{{#if showOption "markets"}}
|
||||
<td>{{venue}}</td>
|
||||
{{/if}}
|
||||
<td>{{formatTotal total}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
38
imports/ui/Graphs.import.styl
vendored
Normal file
38
imports/ui/Graphs.import.styl
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
#graphs
|
||||
margin: 10px 20px
|
||||
height: 100%
|
||||
svg
|
||||
width: 100%
|
||||
.bar
|
||||
fill: steelblue
|
||||
.xAxisLabels
|
||||
font-size: 7px
|
||||
font-family: "Arial", Gadget, sans-serif
|
||||
font-weight: 100
|
||||
.yAxisLabels
|
||||
font-size: 12px
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-weight: 800
|
||||
.barText
|
||||
font-size: 14px
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-weight: 800
|
||||
table
|
||||
table-layout: fixed
|
||||
> thead
|
||||
> tr
|
||||
> th.total
|
||||
width: 200px
|
||||
> th.market
|
||||
width: 200px
|
||||
> th.week
|
||||
width: 200px
|
||||
> th.month
|
||||
width: 200px
|
||||
> th.year
|
||||
width: 200px
|
||||
> tbody
|
||||
> tr.deactivated
|
||||
background-color: #fac0d1
|
||||
> tr.deactivated:hover
|
||||
background-color: #ffcadb
|
||||
404
imports/ui/Graphs.js
Normal file
404
imports/ui/Graphs.js
Normal file
@@ -0,0 +1,404 @@
|
||||
|
||||
import './Graphs.html';
|
||||
import d3 from 'd3';
|
||||
|
||||
let PREFIX = "graphs.";
|
||||
|
||||
let SalesTotals = new Meteor.Collection("salesTotals");
|
||||
|
||||
Meteor.subscribe("venues");
|
||||
Meteor.subscribe("productTags");
|
||||
|
||||
let salesTotalsSubscription;
|
||||
//Save the time and options values outside the session also (in addition to saving it in the session), so we can access it in the autorun for the graph without triggering a re-run of the graph.
|
||||
//Triggering a re-run of the graph from the session change will cause the graph to redraw BEFORE the data arrives from the server (due to the options changing), causing all sorts of grief.
|
||||
let time = "annual";
|
||||
let options = "none";
|
||||
|
||||
Template.Graphs.onCreated(function() {
|
||||
let template = Template.instance();
|
||||
|
||||
if(!Session.get(PREFIX + "time")) Session.set(PREFIX + "time", time);
|
||||
else time = Session.get(PREFIX + "time");
|
||||
if(!Session.get(PREFIX + "options")) Session.set(PREFIX + "options", options);
|
||||
else options = Session.get(PREFIX + "options");
|
||||
|
||||
Tracker.autorun(function() {
|
||||
salesTotalsSubscription = template.subscribe("salesTotals", Session.get(PREFIX + "time"), Session.get(PREFIX + "options"));
|
||||
});
|
||||
});
|
||||
Template.Graphs.onRendered(function() {
|
||||
//Reset the pull downs to their former states.
|
||||
Template.instance().$('select[name="time"]').val(time);
|
||||
Template.instance().$('select[name="options"]').val(options);
|
||||
|
||||
//Build the SVG Graphs
|
||||
let margin = {top: 10, right: 0, bottom: 30, left: 40};
|
||||
let width = 960 - margin.left - margin.right;
|
||||
let height = 500 - margin.top - margin.bottom;
|
||||
let x0Scale = d3.scaleBand().range([0, width]).padding(0.05);
|
||||
let x1Scale = d3.scaleBand().padding(0.05);
|
||||
let yScale = d3.scaleLinear().range([height, 0]);
|
||||
let svg = d3.select('svg.salesGraph')
|
||||
.attr("viewBox", "0 0 960 500")
|
||||
.attr("perserveAspectRatio", "xMidYMid meet")
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
let xLabels = svg.append("g").attr('class', 'xLabels').attr("transform", "translate(0, " + height + ")");
|
||||
let yLabels = svg.append("g").attr('class', 'yLabels');
|
||||
|
||||
Tracker.autorun(function() {
|
||||
if(salesTotalsSubscription.ready()) {
|
||||
//NOTE: Setting these time/options based flags is necessary in the autorun since they change, but don't use the session variables for the values, otherwise the autorun will re-run when the session changes, but before the new data arrives from the server.
|
||||
let showMonths = time == 'monthly';
|
||||
let showWeeks = time == 'weekly';
|
||||
let showMarkets = options == 'markets';
|
||||
let showTypes = options == 'types';
|
||||
|
||||
let grouped = time != 'annual' || options != 'none';
|
||||
let stacked = time != 'annual' && options != 'none';
|
||||
|
||||
let x1ScaleKeyFunction = function(d) {return showMonths ? d.month : showWeeks ? Number(d.week) : showMarkets ? d.venueId + "-" + d.year : d.year};
|
||||
|
||||
console.log("collection count: " + SalesTotals.find({}).count());
|
||||
let dataset = SalesTotals.find({}).fetch();
|
||||
let x1UniqueValues;
|
||||
|
||||
if(showMonths) x1UniqueValues = [...new Set(dataset.map(item => item.month))];
|
||||
else if(showWeeks) x1UniqueValues = ([...new Set(dataset.map(item => Number(item.week)))]).sort(function(a,b) {return a - b});
|
||||
else if(showMarkets) x1UniqueValues = [...new Set(dataset.map(item => item.venueId))];
|
||||
else x1UniqueValues = [""];
|
||||
|
||||
//Update scale domains
|
||||
let years = [...new Set(dataset.map(item => item.year))];
|
||||
x0Scale.domain(years); //Gets the unique set of years from the dataset.
|
||||
x1Scale.domain(x1UniqueValues).rangeRound([0, x0Scale.bandwidth()]);
|
||||
console.log("x1Scale's unique values: " + x1UniqueValues);
|
||||
console.log("x1Scale's bandwidth: " + x1Scale.bandwidth());
|
||||
yScale.domain([0, d3.max(dataset, function(d) {return d.total;})]);
|
||||
|
||||
//Join the data set with the existing data in the svg element.
|
||||
let barGroups = svg.selectAll('.barGroup').data(years);
|
||||
let barsInGroup;
|
||||
|
||||
let handleBarsInGroup = function(barsInGroup) {
|
||||
barsInGroup.exit()
|
||||
.transition()
|
||||
.duration(500)
|
||||
.style('opacity', 0)
|
||||
.remove();
|
||||
barsInGroup.enter().append('rect')
|
||||
.attr('class', 'bar')
|
||||
.attr('y', function(d) {return yScale(d.total)})
|
||||
.attr('height', function(d) {return height - yScale(d.total)})
|
||||
.attr('x', function(d) {return x1Scale(x1ScaleKeyFunction(d))})
|
||||
.attr('width', x1Scale.bandwidth());
|
||||
barsInGroup
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('y', function(d) {return yScale(d.total)})
|
||||
.attr('height', function(d) {return height - yScale(d.total)})
|
||||
.attr('x', function(d) {return x1Scale(x1ScaleKeyFunction(d))})
|
||||
.attr('width', x1Scale.bandwidth());
|
||||
};
|
||||
|
||||
barGroups.exit()
|
||||
.transition()
|
||||
.duration(500)
|
||||
.style('opacity', 0)
|
||||
.remove();
|
||||
barsInGroup = barGroups.enter().append('g')
|
||||
.attr('class', 'barGroup')
|
||||
.attr('transform', function(d) {return 'translate(' + x0Scale(d) + ',0)'})
|
||||
.selectAll('.bar')
|
||||
.data(function(d) {return dataset.filter(function(x) {return x.year == d})}, function(d) {return d._id});
|
||||
//barGroups = svg.selectAll('.barGroup').data(dataset);
|
||||
handleBarsInGroup(barsInGroup);
|
||||
barsInGroup = barGroups
|
||||
.attr('transform', function(d) {return 'translate(' + x0Scale(d) + ',0)'})
|
||||
.selectAll('.bar')
|
||||
.data(function(d) {return dataset.filter(function(x) {return x.year == d})}, function(d) {return d._id});
|
||||
handleBarsInGroup(barsInGroup);
|
||||
//barGroups.attr('transform', function(d) {return 'translate(' + x0Scale(d.year) + ',0)'});
|
||||
//barsInGroup = barsInGroup.data(function(d) {
|
||||
// console.log("Getting the data for a bar group for year: " + d);
|
||||
// let r = dataset.filter(function(x) {return x.year == d});
|
||||
// console.log(r);
|
||||
// return r;});
|
||||
|
||||
|
||||
|
||||
////Handle the new data elements by adding them to the svg element.
|
||||
//let barsInNewGroup = barGroups.enter().append('g')
|
||||
// .attr('transform', function(d) {return 'translate(' + x0Scale(d.year) + ',0)'})
|
||||
// .attr('class', 'barGroup')
|
||||
// .selectAll('bar')
|
||||
// .data(function(d) {return dataset.filter(function(x) {return x.year == d})});
|
||||
//
|
||||
//let barsInModifiedGroup = barGroups.transition()
|
||||
// .duration(500)
|
||||
// .attr('transform', function(d) {return 'translate(' + x0Scale(d.year) + ',0)'})
|
||||
// .selectAll('bar')
|
||||
// .data(function(d) {return dataset.filter(function(x) {return x.year == d})});
|
||||
//
|
||||
//let barHandler = function(barInGroup) {
|
||||
// barInGroup.enter().append('rect')
|
||||
// .attr('class', 'bar')
|
||||
// .attr('y', function(d) {return yScale(d.total)})
|
||||
// .attr('height', function(d) {return height - yScale(d.total)})
|
||||
// .attr('x', function(d) {return x1Scale(x1ScaleKeyFunction(d))})
|
||||
// .attr('width', x1Scale.bandwidth());
|
||||
// barInGroup.transition()
|
||||
// .duration(500)
|
||||
// .attr('y', function(d) {return yScale(d.total)})
|
||||
// .attr('height', function(d) {return height - yScale(d.total)})
|
||||
// .attr('x', function(d) {return x1Scale(x1ScaleKeyFunction(d))})
|
||||
// .attr('width', x1Scale.bandwidth());
|
||||
// barInGroup.exit()
|
||||
// .transition()
|
||||
// .duration(500)
|
||||
// .remove()
|
||||
// .attr("x", -x1Scale.bandwidth());
|
||||
//};
|
||||
//
|
||||
//barHandler(barsInNewGroup);
|
||||
//barHandler(barsInModifiedGroup);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
////Transition existing elements to their new states.
|
||||
//let barsTransition = barGroups.transition()
|
||||
// .duration(500)
|
||||
// .attr('y', function(d) {
|
||||
// return yScale(d.total);
|
||||
// })
|
||||
// .attr('height', function(d) {
|
||||
// return height - yScale(d.total);
|
||||
// })
|
||||
// .attr('x', function(d) {return x1Scale(x1ScaleKeyFunction(d))})
|
||||
// .attr('width', x1Scale.bandwidth());
|
||||
//
|
||||
////Transition removed elements off the screen.
|
||||
//let barsExit = barGroups.exit()
|
||||
// .transition()
|
||||
// .duration(500)
|
||||
// .remove()
|
||||
// .attr("x", -x1Scale.bandwidth());
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//let barTexts = svg.selectAll('text.barText').data(dataset);
|
||||
|
||||
//barTexts.enter().append("text")
|
||||
// .attr("class", "barText")
|
||||
// .attr("text-anchor", "middle")
|
||||
// .attr('x', function(d) {return x0Scale(d._id) + x0Scale.bandwidth() / 2;})
|
||||
// .attr('y', function(d) {return yScale(d.total) - 2;})
|
||||
// .text(function(d) {return "$" + Math.round(d.total)});
|
||||
//barTexts.transition()
|
||||
// .attr("class", "barText")
|
||||
// .attr("text-anchor", "middle")
|
||||
// .attr('x', function(d) {return x0Scale(d._id) + x0Scale.bandwidth() / 2;})
|
||||
// .attr('y', function(d) {return yScale(d.total) - 2;})
|
||||
// .text(function(d) {return "$" + Math.round(d.total)});
|
||||
//barTexts.exit()
|
||||
// .remove();
|
||||
|
||||
//Add the x & y axis labels
|
||||
//xLabels.attr("class", "xAxisLabels").call(d3.axisBottom(x1Scale));
|
||||
xLabels.attr('class', 'xAxisLabels').
|
||||
yLabels.attr("class", "yAxisLabels").call(d3.axisLeft(yScale));
|
||||
}
|
||||
});
|
||||
|
||||
//let margin = {top: 20, right: 20, bottom: 30, left: 80};
|
||||
//let width = 960 - margin.left - margin.right;
|
||||
//let height = 500 - margin.top - margin.bottom;
|
||||
//let x0Scale = d3.scaleBand().range([0, width]).padding(0.1);
|
||||
//let yScale = d3.scaleLinear().range([height, 0]);
|
||||
//let svg = d3.select('svg.salesGraph')//d3.select("#graphs").append("svg")
|
||||
// .attr("viewBox", "0 0 960 500")
|
||||
// .attr("perserveAspectRatio", "xMidYMid meet")
|
||||
// .append("g")
|
||||
// .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
//let xLabels = svg.append("g").attr("transform", "translate(0, " + height + ")");
|
||||
//let yLabels = svg.append("g");
|
||||
//
|
||||
//Tracker.autorun(function() {
|
||||
// console.log("Autorun");
|
||||
// if(salesTotalsSubscription.ready()) {
|
||||
// console.log("Ready");
|
||||
// let dataset = SalesTotals.find({}).fetch();
|
||||
//
|
||||
// //Update scale domains
|
||||
// x0Scale.domain(dataset.map(function(d) {return d._id}));
|
||||
// yScale.domain([0, d3.max(dataset, function(d) {return d.total;})]);
|
||||
//
|
||||
// //Join the data set with the existing data in the svg element.
|
||||
// let bars = svg.selectAll('.bar').data(dataset);
|
||||
//
|
||||
// //Handle existing elements.
|
||||
// // bars.[do something here ie: attr('class', 'oldValues')]
|
||||
// //Handle the new data elements by adding them to the svg element.
|
||||
// bars.enter().append('rect')
|
||||
// .attr('class', 'bar')
|
||||
// .attr('x', function(d) {
|
||||
// return x0Scale(d._id);
|
||||
// })
|
||||
// .attr('width', x0Scale.bandwidth())
|
||||
// .attr('y', function(d) {
|
||||
// return yScale(d.total);
|
||||
// })
|
||||
// .attr('height', function(d) {
|
||||
// return height - yScale(d.total);
|
||||
// });
|
||||
// //Handle removed data elements?
|
||||
// // bars.exit().remove();
|
||||
//
|
||||
// let barTexts = svg.selectAll('text.barText').data(dataset);
|
||||
//
|
||||
// barTexts.enter().append("text")
|
||||
// .attr("class", "barText")
|
||||
// .attr("text-anchor", "middle")
|
||||
// .attr('x', function(d) {return x0Scale(d._id) + x0Scale.bandwidth() / 2;})
|
||||
// .attr('y', function(d) {return yScale(d.total) - 2;})
|
||||
// .text(function(d) {return "$" + Math.round(d.total)});
|
||||
// //Add the x & y axis labels
|
||||
// xLabels.attr("class", "xAxisLabels").call(d3.axisBottom(x0Scale));
|
||||
// yLabels.attr("class", "yAxisLabels").call(d3.axisLeft(yScale));
|
||||
// }
|
||||
//});
|
||||
});
|
||||
Template.Graphs.helpers({
|
||||
sales: function() {
|
||||
let sort = [];
|
||||
|
||||
sort.push(['year', 'asc']);// year = 1;
|
||||
if(Session.get(PREFIX + "time") === 'weekly') sort.push(['week', 'asc']); // .week = 1;
|
||||
if(Session.get(PREFIX + "time") === 'monthly') sort.push(['month', 'asc']); // .month = 1;
|
||||
if(Session.get(PREFIX + "options") === 'markets') sort.push(['venue', 'asc']); // .month = 1;
|
||||
|
||||
return SalesTotals.find({}, {sort: sort});
|
||||
},
|
||||
showTime: function(time) {
|
||||
return Session.get(PREFIX + "time") === time;
|
||||
},
|
||||
showOption: function(option) {
|
||||
return Session.get(PREFIX + "options") === option;
|
||||
},
|
||||
formatTotal: function(total) {
|
||||
return "$" + total.toFixed(2);
|
||||
}
|
||||
});
|
||||
Template.Graphs.events({
|
||||
'change select[name="time"]': function(event, template) {
|
||||
Session.set(PREFIX + "time", $(event.target).val());
|
||||
time = $(event.target).val();
|
||||
},
|
||||
'change select[name="options"]': function(event, template) {
|
||||
Session.set(PREFIX + "options", $(event.target).val());
|
||||
options = $(event.target).val();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
//Select…
|
||||
let bars = svg.selectAll("rect").data(dataset, key);
|
||||
|
||||
//Enter…
|
||||
bars.enter()
|
||||
.append("rect")
|
||||
.attr("x", w)
|
||||
.attr("y", function(d) {
|
||||
return h - yScale(d.total);
|
||||
})
|
||||
//.attr("width", x0Scale.rangeBand())
|
||||
.attr("width", x0Scale.bandwidth())
|
||||
.attr("height", function(d) {
|
||||
return yScale(d.total);
|
||||
})
|
||||
.attr("fill", function(d) {
|
||||
return "rgb(0, 0, " + (d.total * 10) + ")";
|
||||
})
|
||||
.attr("data-id", function(d){
|
||||
return d._id;
|
||||
});
|
||||
|
||||
//Update…
|
||||
bars.transition()
|
||||
// .delay(function(d, i) {
|
||||
// return i / dataset.length * 1000;
|
||||
// }) // this delay will make transistions sequential instead of paralle
|
||||
.duration(500)
|
||||
.attr("x", function(d, i) {
|
||||
return x0Scale(i);
|
||||
})
|
||||
.attr("y", function(d) {
|
||||
return h - yScale(d.total);
|
||||
})
|
||||
.attr("width", x0Scale.bandwidth())
|
||||
.attr("height", function(d) {
|
||||
return yScale(d.total);
|
||||
}).attr("fill", function(d) {
|
||||
return "rgb(0, 0, " + (d.total * 10) + ")";
|
||||
});
|
||||
|
||||
//Exit…
|
||||
bars.exit()
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr("x", -x0Scale.bandwidth())
|
||||
.remove();
|
||||
|
||||
|
||||
|
||||
//Update all labels
|
||||
|
||||
//Select…
|
||||
let labels = svg.selectAll("text")
|
||||
.data(dataset, key);
|
||||
|
||||
//Enter…
|
||||
labels.enter()
|
||||
.append("text")
|
||||
.text(function(d) {
|
||||
return d.total;
|
||||
})
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("x", w)
|
||||
.attr("y", function(d) {
|
||||
return h - yScale(d.total) + 14;
|
||||
})
|
||||
.attr("font-family", "sans-serif")
|
||||
.attr("font-size", "11px")
|
||||
.attr("fill", "white");
|
||||
|
||||
//Update…
|
||||
labels.transition()
|
||||
// .delay(function(d, i) {
|
||||
// return i / dataset.length * 1000;
|
||||
// }) // this delay will make transistions sequential instead of paralle
|
||||
.duration(500)
|
||||
.attr("x", function(d, i) {
|
||||
return x0Scale(i) + x0Scale.bandwidth() / 2;
|
||||
}).attr("y", function(d) {
|
||||
return h - yScale(d.total) + 14;
|
||||
}).text(function(d) {
|
||||
return d.total;
|
||||
});
|
||||
|
||||
//Exit…
|
||||
labels.exit()
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr("x", -x0Scale.bandwidth())
|
||||
.remove();
|
||||
*/
|
||||
@@ -1,18 +1,70 @@
|
||||
<template name="Measures">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><td>Name</td></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each measures}}
|
||||
{{> MeasureRow}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<template name="MeasureRow">
|
||||
<tr>
|
||||
<td>{{name}}</td>
|
||||
<template name="Measure">
|
||||
<tr class="{{getRowClass}}">
|
||||
{{#if editing}}
|
||||
{{> MeasureEditor}}
|
||||
{{else}}
|
||||
<td class="noselect nonclickable left">{{name}}</td>
|
||||
<td class="noselect nonclickable left">{{postfix}}</td>
|
||||
{{#if hidden}}
|
||||
<td class="center"><i class="actionEdit fa fa-pencil-square-o fa-lg noselect clickable" title="Edit" aria-hidden="true"></i> / <i class="actionShow fa fa-eye fa-lg noselect clickable" title="Show" aria-hidden="true"></i></td>
|
||||
{{else}}
|
||||
{{#if deactivated}}
|
||||
<td class="center"><i class="actionEdit fa fa-pencil-square-o fa-lg noselect clickable" title="Edit" aria-hidden="true"></i> / <i class="actionActivate fa fa-toggle-on fa-lg noselect clickable" title="Activate" aria-hidden="true"></i> / <i class="actionHide fa fa-eye-slash fa-lg noselect clickable" title="Hide" aria-hidden="true"></i></td>
|
||||
{{else}}
|
||||
<td class="center"><i class="actionEdit fa fa-pencil-square-o fa-lg noselect clickable" title="Edit" aria-hidden="true"></i> / <i class="actionRemove fa fa-times-circle fa-lg noselect clickable" title="Deactivate" aria-hidden="true"></i></td>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template name="MeasureEditor">
|
||||
<td colspan="2" class="measureEditorTd">
|
||||
<div class="editorDiv"><label>Name:</label><input name="name" class="form-control" type="text" value="{{name}}" autocomplete="off" required></div>
|
||||
<div class="editorDiv"><label>Postfix:</label><input name="postfix" class="form-control" type="text" value="{{name}}" autocomplete="off" required></div>
|
||||
</td>
|
||||
<td class="center measureEditorTd"><i class="editorApply fa fa-check-square-o fa-lg noselect clickable" aria-hidden="true"></i> / <i class="editorCancel fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i></td>
|
||||
</template>
|
||||
|
||||
<template name="MeasureSearch">
|
||||
<div class="measureSearch">
|
||||
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}"/>
|
||||
</div>
|
||||
</template>
|
||||
94
imports/ui/Measures.import.styl
vendored
94
imports/ui/Measures.import.styl
vendored
@@ -0,0 +1,94 @@
|
||||
#measures
|
||||
margin: 20px 20px
|
||||
height: 100%
|
||||
text-align: left
|
||||
|
||||
.tableControls
|
||||
text-align: right
|
||||
margin-right: 20px
|
||||
.controlLabel
|
||||
font-size: 9px
|
||||
font-weight: 700
|
||||
color: #5a5a5a
|
||||
position: relative
|
||||
top: -2px
|
||||
.toggleShowHidden
|
||||
margin: 0 40px 0 0
|
||||
position: relative
|
||||
top: -4px
|
||||
display: inline-block
|
||||
|
||||
.tableContainer
|
||||
width: 100%
|
||||
margin-bottom: 20px
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
|
||||
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
|
||||
@@ -1,25 +1,217 @@
|
||||
|
||||
import './Measures.html';
|
||||
|
||||
let QUERY_LIMIT = 20;
|
||||
let PREFIX = "Measures.";
|
||||
|
||||
Tracker.autorun(function() {
|
||||
Meteor.subscribe("measures");
|
||||
});
|
||||
|
||||
Template.Measures.onCreated(function() {
|
||||
Session.set(PREFIX + "displayNewMeasure", false);
|
||||
Session.set(PREFIX + "showHidden", false);
|
||||
});
|
||||
Template.Measures.helpers({
|
||||
// someFunctionNameCalledByTemplate: function() {
|
||||
// return something;
|
||||
// }
|
||||
measures: function () {
|
||||
return Measures.find({});
|
||||
displayNewMeasure: function() {
|
||||
return Session.get(PREFIX + "displayNewMeasure");
|
||||
},
|
||||
measures: function() {
|
||||
let skipCount = Session.get(PREFIX + 'skipCount') || 0;
|
||||
let query = Session.get(PREFIX + 'searchQuery');
|
||||
let dbQuery = [];
|
||||
|
||||
if(query) {
|
||||
_.each(_.keys(query), function(key) {
|
||||
if(_.isFunction(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key]();
|
||||
else if(_.isObject(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key]; //Will look something like: {$in: [xxx,xxx,xxx]}
|
||||
else if(_.isNumber(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key];
|
||||
else {
|
||||
//dbQuery[key] = {$regex: query[key], $options: 'i'};
|
||||
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(!Session.get(PREFIX + "showHidden")) {
|
||||
//Ignore any hidden elements by showing those not hidden, or those without the hidden field.
|
||||
dbQuery.push({$or: [{hidden: false}, {hidden: {$exists:false}}]});
|
||||
}
|
||||
|
||||
dbQuery = dbQuery.length > 0 ? {$and: dbQuery} : {};
|
||||
Session.set(PREFIX + 'measureCount', Meteor.collections.Measures.find(dbQuery).count()); //Always get a full count.
|
||||
return Meteor.collections.Measures.find(dbQuery, {limit: QUERY_LIMIT, skip: skipCount, sort: {order: 1}});
|
||||
},
|
||||
disablePrev: function() {
|
||||
return (Session.get(PREFIX + 'skipCount') || 0) == 0;
|
||||
},
|
||||
disableNext: function() {
|
||||
return Session.get(PREFIX + 'measureCount') - (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT <= 0;
|
||||
}
|
||||
});
|
||||
|
||||
Template.Measures.events({
|
||||
// 'click .something': function() {
|
||||
// Meteor.call('someMethodOnServer', this.something, someotherparam);
|
||||
// Session.set('someValue', Session.get('someOtherValue'));
|
||||
// console.log("Got here");
|
||||
// }
|
||||
|
||||
'click .trash': function() {
|
||||
//Calls deleteMeasure which is in the collection for Measures.
|
||||
Meteor.call('deleteMeasure', this._id);
|
||||
console.log("Got here");
|
||||
'click .prevMeasures': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', Math.max(0, (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT));
|
||||
},
|
||||
'click .nextMeasures': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', (Session.get(PREFIX + 'skipCount') || 0) + QUERY_LIMIT);
|
||||
},
|
||||
'click .newMeasureButton': function(event, template) {
|
||||
if(template.$('.newMeasureButton').hasClass('active')) {
|
||||
Session.set(PREFIX + 'displayNewMeasure', false);
|
||||
}
|
||||
else {
|
||||
Session.set(PREFIX + 'displayNewMeasure', true);
|
||||
Session.set(PREFIX + "editedMeasure", undefined); //Clear the edited measure so that only one editor is open at a time.
|
||||
}
|
||||
template.$('.newMeasureButton').toggleClass('active');
|
||||
},
|
||||
'change input[name="showHidden"]': function(event, template) {
|
||||
Session.set(PREFIX + "showHidden", $(event.target).prop('checked'));
|
||||
}
|
||||
});
|
||||
|
||||
Template.MeasureSearch.events({
|
||||
"keyup .searchInput": _.throttle(function(event, template) {
|
||||
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
|
||||
let searchFields = Session.get(PREFIX + 'searchFields') || {};
|
||||
let searchValue = template.$('.searchInput').val();
|
||||
|
||||
if(searchValue) {
|
||||
if(this.number) searchValue = parseFloat(searchValue);
|
||||
|
||||
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)
|
||||
});
|
||||
Template.MeasureSearch.helpers({
|
||||
searchValue: function() {
|
||||
let searchFields = Session.get(PREFIX + 'searchFields');
|
||||
|
||||
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
|
||||
}
|
||||
});
|
||||
|
||||
Template.Measure.helpers({
|
||||
measures: function() {
|
||||
let result = "";
|
||||
|
||||
if(this.measures && this.measures.length > 0) {
|
||||
let measureNames = [];
|
||||
|
||||
for(let i = 0; i < this.measures.length; i++) {
|
||||
let measureObject = Meteor.collections.Measures.findOne(this.measures[i]);
|
||||
|
||||
if(measureObject && measureObject.name)
|
||||
measureNames.push(measureObject.name);
|
||||
}
|
||||
|
||||
result = measureNames.join(", ");
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
editing: function() {
|
||||
let editedMeasure = Session.get(PREFIX + "editedMeasure");
|
||||
|
||||
return editedMeasure == this._id;
|
||||
},
|
||||
getRowClass: function() {
|
||||
return this.hidden ? "hidden" : this.deactivated ? "deactivated" : "";
|
||||
}
|
||||
});
|
||||
Template.Measure.events({
|
||||
"click .actionEdit": function(event, template) {
|
||||
Session.set(PREFIX + "editedMeasure", this._id);
|
||||
Session.set(PREFIX + 'displayNewMeasure', false); //Ensure the new measure editor is closed.
|
||||
template.$('.newMeasureButton').removeClass('active');
|
||||
},
|
||||
"click .actionRemove": function(event, template) {
|
||||
Meteor.call('deactivateMeasure', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Measure Deactivated");
|
||||
});
|
||||
},
|
||||
'click .actionActivate': function(event, template) {
|
||||
Meteor.call('reactivateMeasure', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Measure Reactivated");
|
||||
});
|
||||
},
|
||||
"click .actionShow": function(event, template) {
|
||||
Meteor.call('showMeasure', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Measure Visibility Enabled");
|
||||
});
|
||||
},
|
||||
'click .actionHide': function(event, template) {
|
||||
Meteor.call('hideMeasure', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Measure Visibility Disabled");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Template.MeasureEditor.helpers({
|
||||
});
|
||||
Template.MeasureEditor.events({
|
||||
"click .editorCancel": function(event, template) {
|
||||
Session.set(PREFIX + "editedMeasure", undefined);
|
||||
Session.set(PREFIX + 'displayNewMeasure', false);
|
||||
template.parentTemplate().$('.newMeasureButton').removeClass('active');
|
||||
},
|
||||
"click .editorApply": function(event, template) {
|
||||
let name = template.$("input[name='name']").val().trim();
|
||||
let postfix = template.$("input[name='postfix']").val().trim();
|
||||
let order = 0; //TODO:
|
||||
|
||||
if(Session.get(PREFIX + 'displayNewMeasure')) {
|
||||
Meteor.call("createMeasure", name, postfix, order, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else {
|
||||
sAlert.success("Measure created.");
|
||||
Session.set(PREFIX + 'displayNewMeasure', false);
|
||||
template.parentTemplate().$('.newMeasureButton').removeClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
Meteor.call("updateMeasure", this._id, name, postfix, order, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else {
|
||||
sAlert.success("Measure updated.");
|
||||
Session.set(PREFIX + "editedMeasure", undefined);
|
||||
template.parentTemplate().$('.newMeasureButton').removeClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
@@ -24,7 +24,6 @@ Template.Pricing.onRendered(function() {
|
||||
});
|
||||
Template.Pricing.helpers({
|
||||
measures: function() {
|
||||
//return Meteor.collections.Measures.find({}, {sort: {order: 1}});
|
||||
let measures = Meteor.collections.Measures.find({}, {sort: {order: 1}}).fetch();
|
||||
|
||||
for(let i = 0; i < measures; i++) {
|
||||
|
||||
@@ -238,6 +238,7 @@ Template.ProductTag_ProductSearch.events({
|
||||
|
||||
Session.set(PREFIX + 'searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchFields', searchFields);
|
||||
Session.set(PREFIX + 'skipCount', 0); //Reset the paging of the results.
|
||||
}, 500)
|
||||
});
|
||||
Template.ProductTag_ProductSearch.helpers({
|
||||
|
||||
@@ -110,6 +110,7 @@ Template.ProductSearch.events({
|
||||
|
||||
Session.set(PREFIX + 'searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchFields', searchFields);
|
||||
Session.set(PREFIX + 'skipCount', 0); //Reset the paging of the results.
|
||||
}, 500)
|
||||
});
|
||||
Template.ProductSearch.helpers({
|
||||
@@ -234,7 +235,7 @@ Template.ProductEditor.events({
|
||||
"click .editorCancel": function(event, template) {
|
||||
Session.set(PREFIX + "editedProduct", undefined);
|
||||
Session.set(PREFIX + 'displayNewProduct', false);
|
||||
template.$('.newProductButton').removeClass('active');
|
||||
template.parentTemplate().$('.newProductButton').removeClass('active');
|
||||
},
|
||||
"click .editorApply": function(event, template) {
|
||||
let name = template.$("input[name='name']").val().trim();
|
||||
@@ -252,7 +253,7 @@ Template.ProductEditor.events({
|
||||
else {
|
||||
sAlert.success("Product created.");
|
||||
Session.set(PREFIX + 'displayNewProduct', false);
|
||||
template.$('.newProductButton').removeClass('active');
|
||||
template.parentTemplate().$('.newProductButton').removeClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -262,6 +263,7 @@ Template.ProductEditor.events({
|
||||
else {
|
||||
sAlert.success("Product updated.");
|
||||
Session.set(PREFIX + "editedProduct", undefined);
|
||||
template.parentTemplate().$('.newProductButton').removeClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
{{#if Template.subscriptionsReady}}
|
||||
<div class="insertSale">
|
||||
{{>InsertSale}}
|
||||
<div class="paginationContainer">
|
||||
<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="grid">
|
||||
<table class="table table-striped table-hover">
|
||||
@@ -33,10 +39,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<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>
|
||||
{{else}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
8
imports/ui/Sales.import.styl
vendored
8
imports/ui/Sales.import.styl
vendored
@@ -13,7 +13,13 @@
|
||||
|
||||
.insertSale
|
||||
width: 100%
|
||||
|
||||
position: relative
|
||||
.paginationContainer
|
||||
position: absolute
|
||||
right: 0
|
||||
bottom: -20px
|
||||
.pagination
|
||||
white-space: nowrap
|
||||
.form-group, label
|
||||
text-align: left
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
import './Sales.html';
|
||||
import '/imports/util/selectize/selectize.js'
|
||||
import ResizeSensor from '/imports/util/resize/ResizeSensor.js';
|
||||
import '/imports/util/selectize/selectize.js';
|
||||
|
||||
let QUERY_LIMIT = 20;
|
||||
let PREFIX = "Sales.";
|
||||
@@ -111,7 +110,8 @@ 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)
|
||||
});
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
import './UserManagement.html';
|
||||
import '/imports/util/selectize/selectize.js'
|
||||
|
||||
let PREFIX = "UserManagement";
|
||||
|
||||
Tracker.autorun(function() {
|
||||
Meteor.subscribe("users", Session.get('searchQuery'));
|
||||
Meteor.subscribe("users", Session.get(PREFIX + 'searchQuery'));
|
||||
Meteor.subscribe("roles");
|
||||
});
|
||||
|
||||
@@ -101,8 +103,8 @@ Template.User.helpers({
|
||||
|
||||
Template.UserSearch.events({
|
||||
"keyup .searchInput": _.throttle(function(event, template) {
|
||||
let searchQuery = Session.get('searchQuery') || {};
|
||||
let searchFields = Session.get('searchFields') || {};
|
||||
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
|
||||
let searchFields = Session.get(PREFIX + 'searchFields') || {};
|
||||
let searchValue = template.$('.searchInput').val();
|
||||
|
||||
if(searchValue) {
|
||||
@@ -126,12 +128,13 @@ Template.UserSearch.events({
|
||||
delete searchFields[this.columnName];
|
||||
}
|
||||
|
||||
Session.set('searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'skipCount', 0); //Reset the paging of the results.
|
||||
}, 500)
|
||||
});
|
||||
Template.UserSearch.helpers({
|
||||
searchValue: function() {
|
||||
let searchFields = Session.get('searchFields');
|
||||
let searchFields = Session.get(PREFIX + 'searchFields');
|
||||
|
||||
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
|
||||
}
|
||||
|
||||
70
imports/ui/Venues.html
Normal file
70
imports/ui/Venues.html
Normal file
@@ -0,0 +1,70 @@
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<template name="Venue">
|
||||
<tr class="{{getRowClass}}">
|
||||
{{#if editing}}
|
||||
{{> VenueEditor}}
|
||||
{{else}}
|
||||
<td class="noselect nonclickable left">{{name}}</td>
|
||||
<td class="noselect nonclickable left">{{type}}</td>
|
||||
{{#if hidden}}
|
||||
<td class="center"><i class="actionEdit fa fa-pencil-square-o fa-lg noselect clickable" title="Edit" aria-hidden="true"></i> / <i class="actionShow fa fa-eye fa-lg noselect clickable" title="Show" aria-hidden="true"></i></td>
|
||||
{{else}}
|
||||
{{#if deactivated}}
|
||||
<td class="center"><i class="actionEdit fa fa-pencil-square-o fa-lg noselect clickable" title="Edit" aria-hidden="true"></i> / <i class="actionActivate fa fa-toggle-on fa-lg noselect clickable" title="Activate" aria-hidden="true"></i> / <i class="actionHide fa fa-eye-slash fa-lg noselect clickable" title="Hide" aria-hidden="true"></i></td>
|
||||
{{else}}
|
||||
<td class="center"><i class="actionEdit fa fa-pencil-square-o fa-lg noselect clickable" title="Edit" aria-hidden="true"></i> / <i class="actionRemove fa fa-times-circle fa-lg noselect clickable" title="Deactivate" aria-hidden="true"></i></td>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template name="VenueEditor">
|
||||
<td colspan="2" class="venueEditorTd">
|
||||
<div class="editorDiv"><label>Name:</label><input name="name" class="form-control" type="text" value="{{name}}" autocomplete="off" required></div>
|
||||
<div class="editorDiv"><label>Type:</label><input name="type" class="form-control" type="text" value="{{name}}" autocomplete="off" required></div>
|
||||
</td>
|
||||
<td class="center venueEditorTd"><i class="editorApply fa fa-check-square-o fa-lg noselect clickable" aria-hidden="true"></i> / <i class="editorCancel fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i></td>
|
||||
</template>
|
||||
|
||||
<template name="VenueSearch">
|
||||
<div class="venueSearch">
|
||||
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}"/>
|
||||
</div>
|
||||
</template>
|
||||
94
imports/ui/Venues.import.styl
vendored
Normal file
94
imports/ui/Venues.import.styl
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
#venues
|
||||
margin: 20px 20px
|
||||
height: 100%
|
||||
text-align: left
|
||||
|
||||
.tableControls
|
||||
text-align: right
|
||||
margin-right: 20px
|
||||
.controlLabel
|
||||
font-size: 9px
|
||||
font-weight: 700
|
||||
color: #5a5a5a
|
||||
position: relative
|
||||
top: -2px
|
||||
.toggleShowHidden
|
||||
margin: 0 40px 0 0
|
||||
position: relative
|
||||
top: -4px
|
||||
display: inline-block
|
||||
|
||||
.tableContainer
|
||||
width: 100%
|
||||
margin-bottom: 20px
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
|
||||
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
|
||||
217
imports/ui/Venues.js
Normal file
217
imports/ui/Venues.js
Normal file
@@ -0,0 +1,217 @@
|
||||
|
||||
import './Venues.html';
|
||||
|
||||
let QUERY_LIMIT = 20;
|
||||
let PREFIX = "Venues.";
|
||||
|
||||
Tracker.autorun(function() {
|
||||
Meteor.subscribe("venues");
|
||||
});
|
||||
|
||||
Template.Venues.onCreated(function() {
|
||||
Session.set(PREFIX + "displayNewVenue", false);
|
||||
Session.set(PREFIX + "showHidden", false);
|
||||
});
|
||||
Template.Venues.helpers({
|
||||
displayNewVenue: function() {
|
||||
return Session.get(PREFIX + "displayNewVenue");
|
||||
},
|
||||
venues: function() {
|
||||
let skipCount = Session.get(PREFIX + 'skipCount') || 0;
|
||||
let query = Session.get(PREFIX + 'searchQuery');
|
||||
let dbQuery = [];
|
||||
|
||||
if(query) {
|
||||
_.each(_.keys(query), function(key) {
|
||||
if(_.isFunction(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key]();
|
||||
else if(_.isObject(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key]; //Will look something like: {$in: [xxx,xxx,xxx]}
|
||||
else if(_.isNumber(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key];
|
||||
else {
|
||||
//dbQuery[key] = {$regex: query[key], $options: 'i'};
|
||||
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(!Session.get(PREFIX + "showHidden")) {
|
||||
//Ignore any hidden elements by showing those not hidden, or those without the hidden field.
|
||||
dbQuery.push({$or: [{hidden: false}, {hidden: {$exists:false}}]});
|
||||
}
|
||||
|
||||
dbQuery = dbQuery.length > 0 ? {$and: dbQuery} : {};
|
||||
Session.set(PREFIX + 'venueCount', Meteor.collections.Venues.find(dbQuery).count()); //Always get a full count.
|
||||
return Meteor.collections.Venues.find(dbQuery, {limit: QUERY_LIMIT, skip: skipCount, sort: {order: 1}});
|
||||
},
|
||||
disablePrev: function() {
|
||||
return (Session.get(PREFIX + 'skipCount') || 0) == 0;
|
||||
},
|
||||
disableNext: function() {
|
||||
return Session.get(PREFIX + 'venueCount') - (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT <= 0;
|
||||
}
|
||||
});
|
||||
Template.Venues.events({
|
||||
'click .prevVenues': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', Math.max(0, (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT));
|
||||
},
|
||||
'click .nextVenues': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', (Session.get(PREFIX + 'skipCount') || 0) + QUERY_LIMIT);
|
||||
},
|
||||
'click .newVenueButton': function(event, template) {
|
||||
if(template.$('.newVenueButton').hasClass('active')) {
|
||||
Session.set(PREFIX + 'displayNewVenue', false);
|
||||
}
|
||||
else {
|
||||
Session.set(PREFIX + 'displayNewVenue', true);
|
||||
Session.set(PREFIX + "editedVenue", undefined); //Clear the edited venue so that only one editor is open at a time.
|
||||
}
|
||||
template.$('.newVenueButton').toggleClass('active');
|
||||
},
|
||||
'change input[name="showHidden"]': function(event, template) {
|
||||
Session.set(PREFIX + "showHidden", $(event.target).prop('checked'));
|
||||
}
|
||||
});
|
||||
|
||||
Template.VenueSearch.events({
|
||||
"keyup .searchInput": _.throttle(function(event, template) {
|
||||
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
|
||||
let searchFields = Session.get(PREFIX + 'searchFields') || {};
|
||||
let searchValue = template.$('.searchInput').val();
|
||||
|
||||
if(searchValue) {
|
||||
if(this.number) searchValue = parseFloat(searchValue);
|
||||
|
||||
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)
|
||||
});
|
||||
Template.VenueSearch.helpers({
|
||||
searchValue: function() {
|
||||
let searchFields = Session.get(PREFIX + 'searchFields');
|
||||
|
||||
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
|
||||
}
|
||||
});
|
||||
|
||||
Template.Venue.helpers({
|
||||
venues: function() {
|
||||
let result = "";
|
||||
|
||||
if(this.venues && this.venues.length > 0) {
|
||||
let venueNames = [];
|
||||
|
||||
for(let i = 0; i < this.venues.length; i++) {
|
||||
let venueObject = Meteor.collections.Venues.findOne(this.venues[i]);
|
||||
|
||||
if(venueObject && venueObject.name)
|
||||
venueNames.push(venueObject.name);
|
||||
}
|
||||
|
||||
result = venueNames.join(", ");
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
editing: function() {
|
||||
let editedVenue = Session.get(PREFIX + "editedVenue");
|
||||
|
||||
return editedVenue == this._id;
|
||||
},
|
||||
getRowClass: function() {
|
||||
return this.hidden ? "hidden" : this.deactivated ? "deactivated" : "";
|
||||
}
|
||||
});
|
||||
Template.Venue.events({
|
||||
"click .actionEdit": function(event, template) {
|
||||
Session.set(PREFIX + "editedVenue", this._id);
|
||||
Session.set(PREFIX + 'displayNewVenue', false); //Ensure the new venue editor is closed.
|
||||
template.$('.newVenueButton').removeClass('active');
|
||||
},
|
||||
"click .actionRemove": function(event, template) {
|
||||
Meteor.call('deactivateVenue', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Venue Deactivated");
|
||||
});
|
||||
},
|
||||
'click .actionActivate': function(event, template) {
|
||||
Meteor.call('reactivateVenue', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Venue Reactivated");
|
||||
});
|
||||
},
|
||||
"click .actionShow": function(event, template) {
|
||||
Meteor.call('showVenue', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Venue Visibility Enabled");
|
||||
});
|
||||
},
|
||||
'click .actionHide': function(event, template) {
|
||||
Meteor.call('hideVenue', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Venue Visibility Disabled");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Template.VenueEditor.helpers({
|
||||
});
|
||||
Template.VenueEditor.events({
|
||||
"click .editorCancel": function(event, template) {
|
||||
Session.set(PREFIX + "editedVenue", undefined);
|
||||
Session.set(PREFIX + 'displayNewVenue', false);
|
||||
template.parentTemplate().$('.newVenueButton').removeClass('active');
|
||||
},
|
||||
"click .editorApply": function(event, template) {
|
||||
let name = template.$("input[name='name']").val().trim();
|
||||
let type = template.$("input[name='type']").val().trim();
|
||||
let order = 0; //TODO:
|
||||
|
||||
if(Session.get(PREFIX + 'displayNewVenue')) {
|
||||
Meteor.call("createVenue", name, type, order, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else {
|
||||
sAlert.success("Venue created.");
|
||||
Session.set(PREFIX + 'displayNewVenue', false);
|
||||
template.parentTemplate().$('.newVenueButton').removeClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
Meteor.call("updateVenue", this._id, name, type, order, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else {
|
||||
sAlert.success("Venue updated.");
|
||||
Session.set(PREFIX + "editedVenue", undefined);
|
||||
template.parentTemplate().$('.newVenueButton').removeClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
@@ -45,6 +45,16 @@
|
||||
Measures
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{isActiveRoute 'Venues'}}">
|
||||
<a href="{{pathFor 'Venues'}}">
|
||||
Venues
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{isActiveRoute 'Graphs'}}">
|
||||
<a href="{{pathFor 'Graphs'}}">
|
||||
Graphs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="contentBody">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.18.0",
|
||||
"csv-parse": "latest",
|
||||
"d3": "^4.4.2",
|
||||
"jquery": "^3.1.1",
|
||||
"meteor-node-stubs": "^0.2.4",
|
||||
"properties-reader": "0.0.15",
|
||||
|
||||
99
public/barChart.html
Normal file
99
public/barChart.html
Normal file
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<style>
|
||||
|
||||
.axis .domain {
|
||||
display: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
<svg width="960" height="500"></svg>
|
||||
<script src="https://d3js.org/d3.v4.js"></script>
|
||||
<script>
|
||||
|
||||
var svg = d3.select("svg"),
|
||||
margin = {top: 20, right: 20, bottom: 30, left: 40},
|
||||
width = +svg.attr("width") - margin.left - margin.right,
|
||||
height = +svg.attr("height") - margin.top - margin.bottom,
|
||||
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
var x0 = d3.scaleBand()
|
||||
.rangeRound([0, width])
|
||||
.paddingInner(0.1);
|
||||
|
||||
var x1 = d3.scaleBand()
|
||||
.padding(0.05);
|
||||
|
||||
var y = d3.scaleLinear()
|
||||
.rangeRound([height, 0]);
|
||||
|
||||
var z = d3.scaleOrdinal()
|
||||
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
|
||||
|
||||
d3.csv("barChartData.csv", function(d, i, columns) {
|
||||
for (var i = 1, n = columns.length; i < n; ++i) d[columns[i]] = +d[columns[i]];
|
||||
return d;
|
||||
}, function(error, data) {
|
||||
if (error) throw error;
|
||||
|
||||
var keys = data.columns.slice(1);
|
||||
|
||||
x0.domain(data.map(function(d) { return d.State; }));
|
||||
x1.domain(keys).rangeRound([0, x0.bandwidth()]);
|
||||
y.domain([0, d3.max(data, function(d) { return d3.max(keys, function(key) { return d[key]; }); })]).nice();
|
||||
let t = y.domain();
|
||||
console.log(t);
|
||||
|
||||
g.append("g")
|
||||
.selectAll("g")
|
||||
.data(data)
|
||||
.enter().append("g")
|
||||
.attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; })
|
||||
.selectAll("rect")
|
||||
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }); })
|
||||
.enter().append("rect")
|
||||
.attr("x", function(d) { return x1(d.key); })
|
||||
.attr("y", function(d) { return y(d.value); })
|
||||
.attr("width", x1.bandwidth())
|
||||
.attr("height", function(d) { return height - y(d.value); })
|
||||
.attr("fill", function(d) { return z(d.key); });
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(d3.axisBottom(x0));
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis")
|
||||
.call(d3.axisLeft(y).ticks(null, "s"))
|
||||
.append("text")
|
||||
.attr("x", 2)
|
||||
.attr("y", y(y.ticks().pop()) + 0.5)
|
||||
.attr("dy", "0.32em")
|
||||
.attr("fill", "#000")
|
||||
.attr("font-weight", "bold")
|
||||
.attr("text-anchor", "start")
|
||||
.text("Population");
|
||||
|
||||
var legend = g.append("g")
|
||||
.attr("font-family", "sans-serif")
|
||||
.attr("font-size", 10)
|
||||
.attr("text-anchor", "end")
|
||||
.selectAll("g")
|
||||
.data(keys.slice().reverse())
|
||||
.enter().append("g")
|
||||
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
|
||||
|
||||
legend.append("rect")
|
||||
.attr("x", width - 19)
|
||||
.attr("width", 19)
|
||||
.attr("height", 19)
|
||||
.attr("fill", z);
|
||||
|
||||
legend.append("text")
|
||||
.attr("x", width - 24)
|
||||
.attr("y", 9.5)
|
||||
.attr("dy", "0.32em")
|
||||
.text(function(d) { return d; });
|
||||
});
|
||||
|
||||
</script>
|
||||
7
public/barChartData.csv
Normal file
7
public/barChartData.csv
Normal file
@@ -0,0 +1,7 @@
|
||||
State,Under 5 Years,5 to 13 Years,14 to 17 Years,18 to 24 Years,25 to 44 Years,45 to 64 Years,65 Years and Over
|
||||
CA,2704659,4499890,2159981,3853788,10604510,8819342,4114496
|
||||
TX,2027307,3277946,1420518,2454721,7017731,5656528,2472223
|
||||
NY,1208495,2141490,1058031,1999120,5355235,5120254,2607672
|
||||
FL,1140516,1938695,925060,1607297,4782119,4746856,3187797
|
||||
IL,894368,1558919,725973,1311479,3596343,3239173,1575308
|
||||
PA,737462,1345341,679201,1203944,3157759,3414001,1910571
|
||||
|
187
server/import.js
187
server/import.js
@@ -628,7 +628,7 @@ Meteor.methods({
|
||||
collectMetadata(data[0]);
|
||||
|
||||
//Remove everything first.
|
||||
Sales.remove({});
|
||||
Sales.remove({"importTag": "1"});
|
||||
|
||||
// console.log("CSV Column Mapping: " + JSON.stringify(map));
|
||||
// readRow(data[1]);
|
||||
@@ -821,9 +821,192 @@ Meteor.methods({
|
||||
|
||||
function insertSale(sale) {
|
||||
sale.createdAt = new Date();
|
||||
sale.importTag = "1";
|
||||
Sales.insert(sale, function(error) {
|
||||
if(error) console.log("Failed to insert the sale: " + JSON.stringify(sale) + "\n ERROR: " + error);
|
||||
});
|
||||
}, {bypassCollection2: true});
|
||||
}
|
||||
},
|
||||
"importSales2": function() {
|
||||
let fileName = "importSales2.csv";
|
||||
//The mapping of model attributes to CSV columns. The oz sizes are arrays of columns since there are multiple.
|
||||
let map = {};
|
||||
//The id's in the db for the letious jar sizes.
|
||||
let measureIdMap = {};
|
||||
let venueIdMap = {};
|
||||
let itemIdMap = {};
|
||||
let hasError = false;
|
||||
|
||||
{ //Load the object ids for the measures and venues we will encounter.
|
||||
let result;
|
||||
|
||||
result = Measures.findOne({name: 'Pounds'}, {fields: {_id: 1}});
|
||||
if(result) measureIdMap['lbs'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for Lbs"); hasError = true;}
|
||||
|
||||
result = Measures.findOne({name: 'Each'}, {fields: {_id: 1}});
|
||||
if(result) measureIdMap['each'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for Each"); hasError = true;}
|
||||
|
||||
result = Measures.findOne({name: 'Dozen Large'}, {fields: {_id: 1}});
|
||||
if(result) measureIdMap['dozen, large'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for Dozen, Large"); hasError = true;}
|
||||
|
||||
result = Venues.findOne({name: 'Boonville'}, {fields: {_id: 1}});
|
||||
if(result) venueIdMap['bv'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for Boonville"); hasError = true;}
|
||||
|
||||
result = Venues.findOne({name: 'Clement St'}, {fields: {_id: 1}});
|
||||
if(result) venueIdMap['sf'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for Clement St"); hasError = true;}
|
||||
|
||||
result = Venues.findOne({name: 'Ukiah'}, {fields: {_id: 1}});
|
||||
if(result) venueIdMap['uk'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for Ukiah"); hasError = true;}
|
||||
|
||||
result = Venues.findOne({name: 'Mendocino'}, {fields: {_id: 1}});
|
||||
if(result) venueIdMap['men'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for Mendocino"); hasError = true;}
|
||||
|
||||
result = Venues.findOne({name: 'Ft Bragg'}, {fields: {_id: 1}});
|
||||
if(result) venueIdMap['fb'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for Ft Bragg"); hasError = true;}
|
||||
|
||||
result = Venues.findOne({name: 'On Farm'}, {fields: {_id: 1}});
|
||||
if(result) venueIdMap['of'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for On Farm"); hasError = true;}
|
||||
|
||||
result = Venues.findOne({name: 'Unknown Restaurant'}, {fields: {_id: 1}});
|
||||
if(result) {venueIdMap['res'] = result._id; venueIdMap['w'] = result._id;}
|
||||
else {console.log("Error: Couldn't find the _id for Unknown Restaurant"); hasError = true;}
|
||||
|
||||
result = Venues.findOne({name: 'Yorkville Market'}, {fields: {_id: 1}});
|
||||
if(result) venueIdMap['ym'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for Yorkville Market"); hasError = true;}
|
||||
|
||||
result = Venues.findOne({name: 'Yorkville Cellars'}, {fields: {_id: 1}});
|
||||
if(result) venueIdMap['yc'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for Yorkville Cellars"); hasError = true;}
|
||||
|
||||
result = Venues.findOne({name: 'Mail Order'}, {fields: {_id: 1}});
|
||||
if(result) venueIdMap['mo'] = result._id;
|
||||
else {console.log("Error: Couldn't find the _id for Mail Order"); hasError = true;}
|
||||
|
||||
result = Products.find({}, {fields: {_id: 1, name: 1}, sort: {name: 1}}).fetch();
|
||||
for(let i = 0; i < result.length; i++) itemIdMap[result[i].name.toLowerCase()] = result[i]._id;
|
||||
}
|
||||
|
||||
readCSV(fileName, Meteor.bindEnvironment(function(error, data) {
|
||||
//Data is an array of arrays. data[0] = array of headers. data[1] = first row of data.
|
||||
if(error) console.log("Unable to read the importSales.csv file:" + error);
|
||||
else {
|
||||
//Collect the mapping data.
|
||||
collectMetadata(data[0]);
|
||||
//Remove everything first.
|
||||
Sales.remove({"importTag": "2"});
|
||||
|
||||
let undefinedItems = {};
|
||||
for(let i = 1; i < data.length; i++) {
|
||||
readRow(data[i], undefinedItems);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
//Collect the metadata from the first row of the CSV data - make a mapping.
|
||||
function collectMetadata(row) {
|
||||
let DATE = 'date';
|
||||
let VENUE = 'vendor';
|
||||
let ITEM = 'item';
|
||||
let LBS = 'lbs';
|
||||
let EACH = 'each';
|
||||
let DOZ = 'dozen, large';
|
||||
let TOTAL = 'total';
|
||||
|
||||
//Iterate over the columns to create a mapping.
|
||||
for(let i = 0; i < row.length; i++) {
|
||||
let next = row[i];
|
||||
|
||||
if(next && next != '') {
|
||||
switch(next.toLowerCase()) {
|
||||
case DATE:
|
||||
map.date = i;
|
||||
break;
|
||||
case VENUE:
|
||||
map.venue = i;
|
||||
break;
|
||||
case ITEM:
|
||||
map.item = i;
|
||||
break;
|
||||
case LBS:
|
||||
map.lbs = i;
|
||||
break;
|
||||
case EACH:
|
||||
map.each = i;
|
||||
break;
|
||||
case DOZ:
|
||||
map.doz = i;
|
||||
break;
|
||||
case TOTAL:
|
||||
map.total = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Reads a single row of CSV data and adds it to the database.
|
||||
function readRow(row, undefinedItems) {
|
||||
let date = moment(row[map.date], "M/D/YYYY").toDate();
|
||||
let venue = row[map.venue] ? row[map.venue].toLowerCase() : undefined;
|
||||
let item = row[map.item] ? row[map.item].trim() : undefined;
|
||||
item = item ? item.toLowerCase() : undefined;
|
||||
let lbs = row[map.lbs] == undefined ? 0 : Number(row[map.lbs]);
|
||||
let each = row[map.each] == undefined ? 0 : Number(row[map.each]);
|
||||
let doz = row[map.doz] == undefined ? 0 : Number(row[map.doz]);
|
||||
let total = row[map.total] == undefined ? 0 : Number(row[map.total]);
|
||||
let venueId = venueIdMap[venue];
|
||||
let itemId = itemIdMap[item];
|
||||
let year = date.getFullYear();
|
||||
|
||||
if(venueId == undefined) {
|
||||
console.log("Found an undefined venue: " + venue);
|
||||
console.log(row);
|
||||
}
|
||||
else if(itemId == undefined) {
|
||||
console.log("Error: Could not find the item: '" + item + "'");
|
||||
}
|
||||
else if(total == undefined || total <= 0) {
|
||||
console.log("Error: Invalid total '" + total + "' for the item: '" + item + "'");
|
||||
}
|
||||
else if(!(lbs > 0 || each > 0 || doz > 0)) {
|
||||
console.log("Error: Invalid measures for the item: '" + item + "'");
|
||||
}
|
||||
else {
|
||||
//Split it into multiple sales entries, one for each measure that has a positive value.
|
||||
if(lbs > 0) {
|
||||
let price = total / lbs;
|
||||
|
||||
insertSale({date: date, amount: lbs, price: price, venueId: venueId, productId: itemId, measureId: measureIdMap['lbs']});
|
||||
}
|
||||
if(each > 0) {
|
||||
let price = total / each;
|
||||
|
||||
insertSale({date: date, amount: each, price: price, venueId: venueId, productId: itemId, measureId: measureIdMap['each']});
|
||||
}
|
||||
if(doz > 0) {
|
||||
let price = total / doz;
|
||||
|
||||
insertSale({date: date, amount: doz, price: price, venueId: venueId, productId: itemId, measureId: measureIdMap['dozen, large']});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insertSale(sale) {
|
||||
sale.createdAt = new Date();
|
||||
sale.importTag = "2";
|
||||
Sales.insert(sale, function(error) {
|
||||
if(error) console.log("Failed to insert the sale: " + JSON.stringify(sale) + "\n ERROR: " + error);
|
||||
}, {bypassCollection2: true});
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user