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:
@@ -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;
|
||||
@@ -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
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 .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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user