- | {{name}} |
+
+
+ {{#if editing}}
+ {{> MeasureEditor}}
+ {{else}}
+ | {{name}} |
+ {{postfix}} |
+ {{#if hidden}}
+ / |
+ {{else}}
+ {{#if deactivated}}
+ / / |
+ {{else}}
+ / |
+ {{/if}}
+ {{/if}}
+ {{/if}}
+
+
+
+
+
+
+ |
+ / |
+
+
+
+
+
+
\ No newline at end of file
diff --git a/imports/ui/Measures.import.styl b/imports/ui/Measures.import.styl
index e69de29..bdfd12d 100644
--- a/imports/ui/Measures.import.styl
+++ b/imports/ui/Measures.import.styl
@@ -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
\ No newline at end of file
diff --git a/imports/ui/Measures.js b/imports/ui/Measures.js
index 3b4901a..4b67eae 100644
--- a/imports/ui/Measures.js
+++ b/imports/ui/Measures.js
@@ -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');
+ }
+ });
+ }
+
}
});
\ No newline at end of file
diff --git a/imports/ui/Pricing.js b/imports/ui/Pricing.js
index 38efd3a..0bce1bb 100644
--- a/imports/ui/Pricing.js
+++ b/imports/ui/Pricing.js
@@ -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++) {
diff --git a/imports/ui/ProductTags.js b/imports/ui/ProductTags.js
index 8d8e60e..6291970 100644
--- a/imports/ui/ProductTags.js
+++ b/imports/ui/ProductTags.js
@@ -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({
diff --git a/imports/ui/Products.js b/imports/ui/Products.js
index 6bc7a83..da3c38e 100644
--- a/imports/ui/Products.js
+++ b/imports/ui/Products.js
@@ -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');
}
});
}
diff --git a/imports/ui/Sales.html b/imports/ui/Sales.html
index 23edb38..e5eb631 100644
--- a/imports/ui/Sales.html
+++ b/imports/ui/Sales.html
@@ -3,6 +3,12 @@
{{#if Template.subscriptionsReady}}
{{>InsertSale}}
+
-
{{else}}
{{/if}}
diff --git a/imports/ui/Sales.import.styl b/imports/ui/Sales.import.styl
index 1897491..7ee07cf 100644
--- a/imports/ui/Sales.import.styl
+++ b/imports/ui/Sales.import.styl
@@ -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
diff --git a/imports/ui/Sales.js b/imports/ui/Sales.js
index 42e3fca..e62301a 100644
--- a/imports/ui/Sales.js
+++ b/imports/ui/Sales.js
@@ -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)
});
diff --git a/imports/ui/UserManagement.js b/imports/ui/UserManagement.js
index b7f7164..69199b1 100644
--- a/imports/ui/UserManagement.js
+++ b/imports/ui/UserManagement.js
@@ -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] : '';
}
diff --git a/imports/ui/Venues.html b/imports/ui/Venues.html
new file mode 100644
index 0000000..d58ceb9
--- /dev/null
+++ b/imports/ui/Venues.html
@@ -0,0 +1,70 @@
+
+
+
+
Show Hidden
+
+
+
+
+
+
+
+
+
+ | Name {{>VenueSearch columnName='name'}} |
+ Type {{>VenueSearch columnName='type'}} |
+ Actions |
+
+
+
+
+ {{#if displayNewVenue}}
+ {{> VenueEditor isNew=true}}
+ {{/if}}
+ {{#each venues}}
+ {{> Venue}}
+ {{/each}}
+
+
+
+
+
+
+
+
+ {{#if editing}}
+ {{> VenueEditor}}
+ {{else}}
+ | {{name}} |
+ {{type}} |
+ {{#if hidden}}
+ / |
+ {{else}}
+ {{#if deactivated}}
+ / / |
+ {{else}}
+ / |
+ {{/if}}
+ {{/if}}
+ {{/if}}
+
+
+
+
+
+
+
+ |
+ / |
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/imports/ui/Venues.import.styl b/imports/ui/Venues.import.styl
new file mode 100644
index 0000000..3d362ca
--- /dev/null
+++ b/imports/ui/Venues.import.styl
@@ -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
\ No newline at end of file
diff --git a/imports/ui/Venues.js b/imports/ui/Venues.js
new file mode 100644
index 0000000..91a7cdc
--- /dev/null
+++ b/imports/ui/Venues.js
@@ -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');
+ }
+ });
+ }
+
+ }
+});
\ No newline at end of file
diff --git a/imports/ui/layouts/Body.html b/imports/ui/layouts/Body.html
index bb59039..f4ceec1 100644
--- a/imports/ui/layouts/Body.html
+++ b/imports/ui/layouts/Body.html
@@ -45,6 +45,16 @@
Measures
+
+
+ Venues
+
+
+
+
+ Graphs
+
+
diff --git a/package.json b/package.json
index 1e9211f..a1dcc53 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/public/barChart.html b/public/barChart.html
new file mode 100644
index 0000000..5da369e
--- /dev/null
+++ b/public/barChart.html
@@ -0,0 +1,99 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/public/barChartData.csv b/public/barChartData.csv
new file mode 100644
index 0000000..ac84883
--- /dev/null
+++ b/public/barChartData.csv
@@ -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
\ No newline at end of file
diff --git a/server/import.js b/server/import.js
index 5234335..19d90e3 100644
--- a/server/import.js
+++ b/server/import.js
@@ -628,7 +628,7 @@ Meteor.methods({
collectMetadata(data[0]);
//Remove everything first.
- Sales.remove({});
+ Sales.remove({"importTag": "1"});
// console.log("CSV Column Mapping: " + JSON.stringify(map));
// readRow(data[1]);
@@ -821,9 +821,192 @@ Meteor.methods({
function insertSale(sale) {
sale.createdAt = new Date();
+ sale.importTag = "1";
Sales.insert(sale, function(error) {
if(error) console.log("Failed to insert the sale: " + JSON.stringify(sale) + "\n ERROR: " + error);
- });
+ }, {bypassCollection2: true});
+ }
+ },
+ "importSales2": function() {
+ let fileName = "importSales2.csv";
+ //The mapping of model attributes to CSV columns. The oz sizes are arrays of columns since there are multiple.
+ let map = {};
+ //The id's in the db for the letious jar sizes.
+ let measureIdMap = {};
+ let venueIdMap = {};
+ let itemIdMap = {};
+ let hasError = false;
+
+ { //Load the object ids for the measures and venues we will encounter.
+ let result;
+
+ result = Measures.findOne({name: 'Pounds'}, {fields: {_id: 1}});
+ if(result) measureIdMap['lbs'] = result._id;
+ else {console.log("Error: Couldn't find the _id for Lbs"); hasError = true;}
+
+ result = Measures.findOne({name: 'Each'}, {fields: {_id: 1}});
+ if(result) measureIdMap['each'] = result._id;
+ else {console.log("Error: Couldn't find the _id for Each"); hasError = true;}
+
+ result = Measures.findOne({name: 'Dozen Large'}, {fields: {_id: 1}});
+ if(result) measureIdMap['dozen, large'] = result._id;
+ else {console.log("Error: Couldn't find the _id for Dozen, Large"); hasError = true;}
+
+ result = Venues.findOne({name: 'Boonville'}, {fields: {_id: 1}});
+ if(result) venueIdMap['bv'] = result._id;
+ else {console.log("Error: Couldn't find the _id for Boonville"); hasError = true;}
+
+ result = Venues.findOne({name: 'Clement St'}, {fields: {_id: 1}});
+ if(result) venueIdMap['sf'] = result._id;
+ else {console.log("Error: Couldn't find the _id for Clement St"); hasError = true;}
+
+ result = Venues.findOne({name: 'Ukiah'}, {fields: {_id: 1}});
+ if(result) venueIdMap['uk'] = result._id;
+ else {console.log("Error: Couldn't find the _id for Ukiah"); hasError = true;}
+
+ result = Venues.findOne({name: 'Mendocino'}, {fields: {_id: 1}});
+ if(result) venueIdMap['men'] = result._id;
+ else {console.log("Error: Couldn't find the _id for Mendocino"); hasError = true;}
+
+ result = Venues.findOne({name: 'Ft Bragg'}, {fields: {_id: 1}});
+ if(result) venueIdMap['fb'] = result._id;
+ else {console.log("Error: Couldn't find the _id for Ft Bragg"); hasError = true;}
+
+ result = Venues.findOne({name: 'On Farm'}, {fields: {_id: 1}});
+ if(result) venueIdMap['of'] = result._id;
+ else {console.log("Error: Couldn't find the _id for On Farm"); hasError = true;}
+
+ result = Venues.findOne({name: 'Unknown Restaurant'}, {fields: {_id: 1}});
+ if(result) {venueIdMap['res'] = result._id; venueIdMap['w'] = result._id;}
+ else {console.log("Error: Couldn't find the _id for Unknown Restaurant"); hasError = true;}
+
+ result = Venues.findOne({name: 'Yorkville Market'}, {fields: {_id: 1}});
+ if(result) venueIdMap['ym'] = result._id;
+ else {console.log("Error: Couldn't find the _id for Yorkville Market"); hasError = true;}
+
+ result = Venues.findOne({name: 'Yorkville Cellars'}, {fields: {_id: 1}});
+ if(result) venueIdMap['yc'] = result._id;
+ else {console.log("Error: Couldn't find the _id for Yorkville Cellars"); hasError = true;}
+
+ result = Venues.findOne({name: 'Mail Order'}, {fields: {_id: 1}});
+ if(result) venueIdMap['mo'] = result._id;
+ else {console.log("Error: Couldn't find the _id for Mail Order"); hasError = true;}
+
+ result = Products.find({}, {fields: {_id: 1, name: 1}, sort: {name: 1}}).fetch();
+ for(let i = 0; i < result.length; i++) itemIdMap[result[i].name.toLowerCase()] = result[i]._id;
+ }
+
+ readCSV(fileName, Meteor.bindEnvironment(function(error, data) {
+ //Data is an array of arrays. data[0] = array of headers. data[1] = first row of data.
+ if(error) console.log("Unable to read the importSales.csv file:" + error);
+ else {
+ //Collect the mapping data.
+ collectMetadata(data[0]);
+ //Remove everything first.
+ Sales.remove({"importTag": "2"});
+
+ let undefinedItems = {};
+ for(let i = 1; i < data.length; i++) {
+ readRow(data[i], undefinedItems);
+ }
+ }
+ }));
+
+ //Collect the metadata from the first row of the CSV data - make a mapping.
+ function collectMetadata(row) {
+ let DATE = 'date';
+ let VENUE = 'vendor';
+ let ITEM = 'item';
+ let LBS = 'lbs';
+ let EACH = 'each';
+ let DOZ = 'dozen, large';
+ let TOTAL = 'total';
+
+ //Iterate over the columns to create a mapping.
+ for(let i = 0; i < row.length; i++) {
+ let next = row[i];
+
+ if(next && next != '') {
+ switch(next.toLowerCase()) {
+ case DATE:
+ map.date = i;
+ break;
+ case VENUE:
+ map.venue = i;
+ break;
+ case ITEM:
+ map.item = i;
+ break;
+ case LBS:
+ map.lbs = i;
+ break;
+ case EACH:
+ map.each = i;
+ break;
+ case DOZ:
+ map.doz = i;
+ break;
+ case TOTAL:
+ map.total = i;
+ break;
+ }
+ }
+ }
+ }
+
+ //Reads a single row of CSV data and adds it to the database.
+ function readRow(row, undefinedItems) {
+ let date = moment(row[map.date], "M/D/YYYY").toDate();
+ let venue = row[map.venue] ? row[map.venue].toLowerCase() : undefined;
+ let item = row[map.item] ? row[map.item].trim() : undefined;
+ item = item ? item.toLowerCase() : undefined;
+ let lbs = row[map.lbs] == undefined ? 0 : Number(row[map.lbs]);
+ let each = row[map.each] == undefined ? 0 : Number(row[map.each]);
+ let doz = row[map.doz] == undefined ? 0 : Number(row[map.doz]);
+ let total = row[map.total] == undefined ? 0 : Number(row[map.total]);
+ let venueId = venueIdMap[venue];
+ let itemId = itemIdMap[item];
+ let year = date.getFullYear();
+
+ if(venueId == undefined) {
+ console.log("Found an undefined venue: " + venue);
+ console.log(row);
+ }
+ else if(itemId == undefined) {
+ console.log("Error: Could not find the item: '" + item + "'");
+ }
+ else if(total == undefined || total <= 0) {
+ console.log("Error: Invalid total '" + total + "' for the item: '" + item + "'");
+ }
+ else if(!(lbs > 0 || each > 0 || doz > 0)) {
+ console.log("Error: Invalid measures for the item: '" + item + "'");
+ }
+ else {
+ //Split it into multiple sales entries, one for each measure that has a positive value.
+ if(lbs > 0) {
+ let price = total / lbs;
+
+ insertSale({date: date, amount: lbs, price: price, venueId: venueId, productId: itemId, measureId: measureIdMap['lbs']});
+ }
+ if(each > 0) {
+ let price = total / each;
+
+ insertSale({date: date, amount: each, price: price, venueId: venueId, productId: itemId, measureId: measureIdMap['each']});
+ }
+ if(doz > 0) {
+ let price = total / doz;
+
+ insertSale({date: date, amount: doz, price: price, venueId: venueId, productId: itemId, measureId: measureIdMap['dozen, large']});
+ }
+ }
+ }
+
+ function insertSale(sale) {
+ sale.createdAt = new Date();
+ sale.importTag = "2";
+ Sales.insert(sale, function(error) {
+ if(error) console.log("Failed to insert the sale: " + JSON.stringify(sale) + "\n ERROR: " + error);
+ }, {bypassCollection2: true});
}
}
});
\ No newline at end of file