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:
Wynne Crisman
2017-02-03 09:20:29 -08:00
parent 55337521f6
commit 184ce1133f
38 changed files with 2564 additions and 641 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -59,3 +59,4 @@ momentjs:moment
mizzao:bootboxjs # ???
aldeed:template-extension
juliancwirko:s-alert # Client error/alert handling
jcbernack:reactive-aggregate

View File

@@ -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

View File

@@ -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>

View File

@@ -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/Production.import.styl"
@import "../imports/ui/Graphs.import.styl"

View File

@@ -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.");
}

View File

@@ -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.");
},

View File

@@ -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.");
}

View File

@@ -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.");
}

View File

@@ -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;

View File

@@ -56,4 +56,25 @@ pri.route('/pricing', {
require("/imports/ui/Pricing.js");
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
View 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
View 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
View 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
View 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
View 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
View 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();
*/

View File

@@ -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>&nbsp;/&nbsp;<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>&nbsp;/&nbsp;<i class="actionActivate fa fa-toggle-on fa-lg noselect clickable" title="Activate" aria-hidden="true"></i>&nbsp;/&nbsp;<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>&nbsp;/&nbsp;<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>&nbsp;/&nbsp;<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>

View File

@@ -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

View File

@@ -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 .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.Measures.events({
// 'click .something': function() {
// Meteor.call('someMethodOnServer', this.something, someotherparam);
// Session.set('someValue', Session.get('someOtherValue'));
// console.log("Got here");
// }
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] : '';
}
});
'click .trash': function() {
//Calls deleteMeasure which is in the collection for Measures.
Meteor.call('deleteMeasure', this._id);
console.log("Got here");
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');
}
});
}
}
});

View File

@@ -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++) {

View File

@@ -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({

View File

@@ -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');
}
});
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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)
});

View File

@@ -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
View 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>&nbsp;/&nbsp;<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>&nbsp;/&nbsp;<i class="actionActivate fa fa-toggle-on fa-lg noselect clickable" title="Activate" aria-hidden="true"></i>&nbsp;/&nbsp;<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>&nbsp;/&nbsp;<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>&nbsp;/&nbsp;<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
View 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
View 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');
}
});
}
}
});

View File

@@ -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">

View File

@@ -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
View 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
View 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
1 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
2 CA 2704659 4499890 2159981 3853788 10604510 8819342 4114496
3 TX 2027307 3277946 1420518 2454721 7017731 5656528 2472223
4 NY 1208495 2141490 1058031 1999120 5355235 5120254 2607672
5 FL 1140516 1938695 925060 1607297 4782119 4746856 3187797
6 IL 894368 1558919 725973 1311479 3596343 3239173 1575308
7 PA 737462 1345341 679201 1203944 3157759 3414001 1910571

View File

@@ -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});
}
}
});