Initial commit. Modified the Meteor todos app to create the Petit Teton data tracking app. Has working data for sales. Requires a Mongo database.

This commit is contained in:
Wynne Crisman
2017-01-15 11:33:37 -08:00
commit b757595cd6
104 changed files with 26824 additions and 0 deletions

3
imports/ui/Intro.html Normal file
View File

@@ -0,0 +1,3 @@
<template name="Intro">
<div id="intro">Intro</div>
</template>

4
imports/ui/Intro.import.styl vendored Normal file
View File

@@ -0,0 +1,4 @@
#intro
text-align: center
font-size: 4em
font-family: sans-serif

2
imports/ui/Intro.js Normal file
View File

@@ -0,0 +1,2 @@
import { Template } from 'meteor/templating';
import './Intro.html';

18
imports/ui/Measures.html Normal file
View File

@@ -0,0 +1,18 @@
<template name="Measures">
<table>
<thead>
<tr><td>Name</td></tr>
</thead>
<tbody>
{{#each measures}}
{{> MeasureRow}}
{{/each}}
</tbody>
</table>
</template>
<template name="MeasureRow">
<tr>
<td>{{name}}</td>
</tr>
</template>

0
imports/ui/Measures.import.styl vendored Normal file
View File

25
imports/ui/Measures.js Normal file
View File

@@ -0,0 +1,25 @@
import './Measures.html';
Template.Measures.helpers({
// someFunctionNameCalledByTemplate: function() {
// return something;
// }
measures: function () {
return Measures.find({});
}
});
Template.Measures.events({
// 'click .something': function() {
// Meteor.call('someMethodOnServer', this.something, someotherparam);
// Session.set('someValue', Session.get('someOtherValue'));
// console.log("Got here");
// }
'click .trash': function() {
//Calls deleteMeasure which is in the collection for Measures.
Meteor.call('deleteMeasure', this._id);
console.log("Got here");
}
});

20
imports/ui/Menu.html Normal file
View File

@@ -0,0 +1,20 @@
<template name="Menu">
<div id="menu">
<a class="option" href="/sales">
<i class="fa fa-usd"></i>
<p>Sales</p>
</a>
<a class="option" href="/prices">
<i class="fa fa-usd"></i>
<p>Prices</p>
</a>
<a class="option" href="/items">
<i class="fa fa-sitemap"></i>
<p>Items</p>
</a>
<a class="option" href="/configMenu">
<i class="fa fa-cog"></i>
<p>Settings</p>
</a>
</div>
</template>

56
imports/ui/Menu.import.styl vendored Normal file
View File

@@ -0,0 +1,56 @@
#menu {
flex: 0 0 100%;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -moz-flex;
display: -webkit-flex;
display: flex;
flex-flow: row wrap;
justify-content: center; //Spacing between items along the primary axis. (vertical spacing for a column layout)
align-items: flex-start; //Align the items within a line along the primary axis. (horizontal alignment for a column layout)
align-content: flex-start; //Spacing between lines along the secondary axis. (spacing between columns for a column layout)
width: 100%;
.option {
height: 120px;
width: 120px;
background: grey;
margin: 20px;
overflow: hidden;
color: white;
//Flex element options.
flex: 0 0 120px; //Grow, Shrink, Basis
//Flex container options.
flex-flow: column nowrap;
justify-content: space-around; //Spacing between items along the primary axis. (vertical spacing for a column layout)
align-items: center; //Align the items within a line along the primary axis. (horizontal alignment for a column layout)
align-content: center; //Spacing between lines along the secondary axis. (spacing between columns for a column layout)
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -moz-flex;
display: -webkit-flex;
display: flex;
text-decoration: none;
i {
flex: 0 0;
font-size: 8em;
}
p {
flex: 0 0;
font-size: 1.5em;
text-align: center;
margin: 0;
}
}
.option:hover {
-moz-box-shadow: inset 0 0 20px #7a5a7a;
-webkit-box-shadow: inset 0 0 20px #7a5a7a;
box-shadow: inset 0 0 20px #7a5a7a;
}
.option:active {
background: #CCC;
}
}

2
imports/ui/Menu.js Normal file
View File

@@ -0,0 +1,2 @@
import { Template } from 'meteor/templating';
import './Menu.html';

54
imports/ui/Pricing.html Normal file
View File

@@ -0,0 +1,54 @@
<template name="Pricing">
<div id="pricing">
<div class="controls">
<div class="controlGroup floatRight">
<label class='controlLabel'>New Price: </label>
<input type="number" class="price" name="price" min="0" data-schema-key='currency' value="{{price}}" required>
<input type="button" class="btn btn-success applyButton" value="Apply">
<!--<span class="toggleUpdateHistory toggleButton clickable">Set Prev</span>-->
<div class="controlGroup outline">
<span class="controlLabel">Set Previous:</span>
<div class="toggleUpdateHistory checkbox checkbox-slider--b-flat">
<label>
<input type="checkbox" name="setPrevious" checked><span></span>
</label>
</div>
<label class='controlLabel'>Effective: </label>
<input type="date" class="form-control" name="date" data-schema-key='date' required>
</div>
</div>
<div class="controlGroup outline floatLeft" style="position: relative; top: 12px">
<label class='controlLabel'>Selected Measure: </label>
<select name="measures">
{{#each measures}}
<option value="{{_id}}">{{name}}</option>
{{/each}}
</select>
</div>
</div>
<table>
<thead>
<tr>
<th class="name">Name</th>
<th class="current">Current</th>
<th class="changeDate">Change Date</th>
<th class="previous">Previous</th>
</tr>
</thead>
<tbody>
{{#each product}}
{{> PricingForProduct}}
{{/each}}
</tbody>
</table>
</div>
</template>
<template name="PricingForProduct">
<tr class="clickable noselect">
<td>{{name}}</td>
<td>{{currentPrice}}</td>
<td>{{priceChangeDate}}</td>
<td>{{previousPrice}}</td>
</tr>
</template>

95
imports/ui/Pricing.import.styl vendored Normal file
View File

@@ -0,0 +1,95 @@
#pricing
margin: 10px 20px
height: 100%
.controls
text-align: left
.controlGroup
padding: 4px 8px
margin: 4px 8px
display: inline-block
.outline
border: 2px dotted #32747e
border-radius: 10px
.floatLeft
float: left
.floatRight
float: right
.controlLabel
font-size: 1.5em
font-weight: 700
select[name="measures"]
padding: 4px 8px
font-size: 1.5em
input
padding: 4px 8px
font-size: 1.5em
input[type="number"]
width: 80px
input[type="button"]
margin-top: -6px
margin-right: 20px
//.toggleButton
// padding: 6px 8px
// border: 1px solid #4cae4c
// border-radius: 4px
// font-size: 1.5em
// color: white
// background: #5b5
// font-family: inherit
//.toggleButton.inactive
// background: #FF6F77
// color: 888
.toggleUpdateHistory
margin: 0
position: relative
top: -4px
display: inline-block
//.inactive
// background: #666
input[type="date"]
width: 180px
display: inline-block
table
width: 100%
margin-bottom: 20px
border: 0
table-layout: fixed
font-size: 1.3em
thead
font-weight: 800
tr > th
background: #333
color: white
tr > th.name
width: auto
tr > th.current
width: 200px
tr > th.previous
width: 200px
tr > th.changeDate
width: 200px
tbody
text-align: left
tr:nth-child(even)
background: #DDD
.rowGroupHead
color: white
background: #333
tr.selected
//background: yellow
background-attachment: fixed
background-repeat: no-repeat
background-position: 0 0
background-image: linear-gradient(to left, #FCF8D1 70%,#f1da36 100%)
tr:nth-child(even).selected
background-attachment: fixed
background-repeat: no-repeat
background-position: 0 0
background-image: linear-gradient(to left, #E0DCBA 70%,#f1da36 100%)

114
imports/ui/Pricing.js Normal file
View File

@@ -0,0 +1,114 @@
import './Pricing.html';
Tracker.autorun(function() {
Meteor.subscribe("products");
Meteor.subscribe("measures");
});
Template.Pricing.onRendered(function() {
this.$('input[name="date"]').val(new Date().toDateInputValue());
});
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++) {
if(Meteor.collections.Products.find({measures: {$all: [measures[i]._id]}}, {sort: {name: 1}}).count() == 0)
measures.splice(i, 1); //Remove the measure from the list.
}
return measures;
},
product: function() {
let measureId = Session.get("selectedMeasure");
return Meteor.collections.Products.find({measures: {$all: [measureId]}}, {sort: {name: 1}});
}
});
Template.Pricing.events({
'change select[name="measures"]': function(event, template) {
Session.set("selectedMeasure", $(event.target).val());
},
'click .applyButton': function(event, template) {
let measureId = Session.get("selectedMeasure");
let $selectedRows = template.$('tr.selected');
// let selectedProducts = $selectedRows.map(function() {return $(this).data('product')});
let price = Number(template.$('input[name="price"]').val());
let setPrevious = template.$('input[name="setPrevious"]').prop('checked');
let date = template.$('input[name="date"]').val();
date = moment(date ? date : new Date().toDateInputValue(), "YYYY-MM-DD").toDate();
setPrevious = setPrevious == true || setPrevious == 'on' || setPrevious == "true" || setPrevious == "yes";
if(setPrevious == true && !date) {
sAlert.error("Unexpected input.");
}
if(!price || isNaN(price) || price < 0) {
sAlert.error("Unexpected input.");
}
for(let i = 0; i < $selectedRows.length; i++) {
let product = $($selectedRows[i]).data('product');
Meteor.call("setProductPrice", product._id, measureId, price, setPrevious, date);
}
}
});
// Template.PricingForProduct.onCreated(function() {
//
// });
Template.PricingForProduct.onRendered(function() {
this.$('tr').data("product", this.data);
});
Template.PricingForProduct.helpers({
currentPrice: function() {
let measureId = Session.get("selectedMeasure");
let price = this.prices && measureId && this.prices[measureId] && this.prices[measureId].price ? this.prices[measureId].price : undefined;
return price ? price.toLocaleString("en-US", {style: 'currency', currency: 'USD', minimumFractionDigits: 2}) : "-";
},
previousPrice: function() {
let measureId = Session.get("selectedMeasure");
let price = this.prices && measureId && this.prices[measureId] && this.prices[measureId].previousPrice ? this.prices[measureId].previousPrice : undefined;
return price ? price.toLocaleString("en-US", {style: 'currency', currency: 'USD', minimumFractionDigits: 2}) : "-";
},
priceChangeDate: function() {
let measureId = Session.get("selectedMeasure");
let date = this.prices && measureId && this.prices[measureId] && this.prices[measureId].effectiveDate ? this.prices[measureId].effectiveDate : undefined;
return date ? moment(date).format("MM/DD/YYYY (w)") : "-";
}
});
Template.PricingForProduct.events({
'click tr': function(event, template) {
let $row = template.$(event.target).closest("tr");
let parentTemplate = template.parentTemplate(1);
if(event.shiftKey) {
let $lastRow = parentTemplate.$lastClickedRow;
let $range = ($row.index() > $lastRow.index() ? $lastRow.nextUntil($row) : $row.nextUntil($lastRow)).add($row);
if(event.ctrlKey) {
$range.toggleClass("selected");
}
else {
$range.addClass("selected");
}
}
else if(event.ctrlKey) {
$row.toggleClass("selected");
}
else {
$row.addClass("selected");
$row.siblings().removeClass('selected');
}
//Store the last row clicked on in a non-reactive variable attached to the parent template.
parentTemplate.$lastClickedRow = $row;
}
});

View File

@@ -0,0 +1,66 @@
<template name="ProductTags">
<div id="productTags">
{{#if Template.subscriptionsReady}}
<div class="insert">
{{>ProductTagInsert}}
</div>
<div class="grid">
<table class="dataTable table table-striped table-hover">
<thead>
<tr class="headers">
<th class="tdLarge noselect nonclickable" style="max-width: 300px">Name</th>
<th class="tdLarge noselect nonclickable" style="width: 90px">Actions</th>
</tr>
<tr class="footers">
<th>{{>ProductTagSearch columnName='name'}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each productTags}}
{{> ProductTag}}
{{/each}}
</tbody>
</table>
</div>
{{else}}
{{/if}}
</div>
</template>
<template name="ProductTag">
<tr>
{{#if editing}}
<td><input name="name" class="form-control" type="text" value="{{name}}" required></td>
<td class="center tdLarge"><i class="editorCancel fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i>&nbsp;/&nbsp;<i class="editorApply fa fa-check-square-o fa-lg noselect clickable" aria-hidden="true"></i></td>
{{else}}
<td class="tdLarge noselect nonclickable">{{name}}</td>
<td class="center tdLarge"><i class="tagRemove fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i>&nbsp;/&nbsp;<i class="tagEdit fa fa-pencil-square-o fa-lg noselect clickable" aria-hidden="true"></i></td>
{{/if}}
</tr>
</template>
<template name="ProductTagSearch">
<div class="">
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}"/>
</div>
</template>
<template name="ProductTagInsert">
<form name="insert" autocomplete="off">
<div class="row">
<div class="col-md-3 col-sm-0"></div>
<div class="col-md-6 col-sm-12">
<div class="formGroupHeading">New Product Tag</div>
<div class="form-group">
<label class='control-label'>Name</label>
<input name="username" type="text" class="form-control" required>
</div>
<div class="form-group">
<input type="submit" class="btn btn-success" value="Create">
</div>
</div>
<div class="col-md-3 col-sm-0"></div>
</div>
</form>
</template>

88
imports/ui/ProductTags.import.styl vendored Normal file
View File

@@ -0,0 +1,88 @@
#productTags
margin: 20px 20px
height: 100%
//Flex container options.
flex-flow: column nowrap
justify-content: space-around //Spacing between sales along the primary axis. (vertical spacing for a column layout)
align-items: flex-start //Align the sales within a line along the primary axis. (horizontal alignment for a column layout)
align-content: center //Spacing between lines along the secondary axis. (spacing between columns for a column layout)
display: -webkit-box
display: -moz-box
display: -ms-flexbox
display: -moz-flex
display: -webkit-flex
display: flex
text-align: left
.editor
height: 100%
overflow-y: auto
.insert
flex: none
width: 100%
.col-md-6
padding: 10px 30px 0 30px
background: #EFEFEF
border-radius: 1em
.formGroupHeading
font-size: 1.6em
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
font-style: normal
font-variant: normal
font-weight: 500
.grid
flex: auto
align-self: stretch
overflow-y: auto
overflow-x: auto
margin-bottom: 20px
border: 0
padding-top: 20px
.table > thead > tr > th
border: 0
padding-top: 0
padding-bottom: 6px
.dataTable
table-layout: fixed
width: auto
.tdLarge
font-size: 1.5em
.tagRemove
color: red
.tagEdit
color: darkblue
.editorApply
color: green
.editorCancel
color: red
td.roles
.role
padding: 4px 4px
border: 1px solid #555
border-radius: .25em
background: white
color: #999
cursor: pointer
.selected
color: black
div.roles
padding: 4px 0
.role
padding: 4px 4px
border: 1px solid #555
border-radius: .25em
background: white
color: #999
cursor: pointer
.selected
color: black
.center
vertical-align: middle !important

132
imports/ui/ProductTags.js Normal file
View File

@@ -0,0 +1,132 @@
import './ProductTags.html';
Tracker.autorun(function() {
Meteor.subscribe("productTags");
});
Template.ProductTags.helpers({
productTags: function() {
return Meteor.collections.ProductTags.find(Session.get('searchQuery') || {}, {sort: {name: -1}});
}
});
Template.ProductTag.onCreated(function() {
this.edited = new ReactiveVar();
});
Template.ProductTag.events({
"click .tagEdit": function(event, template) {
template.edited.set(this);
},
"click .tagRemove": function(event, template) {
let _this = this;
bootbox.confirm({
message: "Delete the product tag?",
buttons: {confirm: {label: "Yes", className: 'btn-success'}, cancel: {label: "No", className: "btn-danger"}},
callback: function(result) {
if(result) {
Meteor.call('deleteProductTag', _this._id, function(error, result) {
if(error) {
sAlert.error(error);
}
else {
sAlert.success("Product tag removed.");
}
});
}
}
});
},
"click .editorCancel": function(event, template) {
template.edited.set(undefined);
},
"click .editorApply": function(event, template) {
let name = template.$("input[name='name']").val().trim();
//Basic validation.
if(name) {
Meteor.call("updateProductTag", {_id: this._id, name: name}, function(error, result) {
if(error) {
sAlert.error(error);
}
else {
sAlert.success("Product tag updated.");
}
});
}
template.edited.set(undefined);
},
"click .role": function(event, template) {
$(event.target).toggleClass("selected");
}
});
Template.ProductTag.helpers({
editing: function() {
return Template.instance().edited.get() == this;
}
});
Template.ProductTagSearch.events({
"keyup .searchInput": _.throttle(function(event, template) {
let searchQuery = Session.get('searchQuery') || {};
let searchFields = Session.get('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('searchQuery', searchQuery);
}, 500)
});
Template.ProductTagSearch.helpers({
searchValue: function() {
let searchFields = Session.get('searchFields');
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
}
});
Template.ProductTagInsert.onRendered(function() {
this.$('form[name="insert"]').validator();
});
Template.ProductTagInsert.events({
'click input[type="submit"]': function(event, template) {
event.preventDefault();
template.$('form[name="insert"]').data('bs.validator').validate(function(isValid) {
if(isValid) {
let name = template.$('input[name="name"]').val();
Meteor.call('insertProductTag', name, function(error, result) {
if(error) {
sAlert.error(error);
}
else {
sAlert.success("Product tag created.");
}
});
}
});
},
"click .role": function(event, template) {
$(event.target).toggleClass("selected");
}
});

View File

@@ -0,0 +1,5 @@
<template name="Production">
<div id="production">
todo
</div>
</template>

60
imports/ui/Production.import.styl vendored Normal file
View File

@@ -0,0 +1,60 @@
#production
margin: 10px 20px
height: 100%
//Flex container options.
flex-flow: column nowrap
justify-content: space-around //Spacing between sales along the primary axis. (vertical spacing for a column layout)
align-items: flex-start //Align the sales within a line along the primary axis. (horizontal alignment for a column layout)
align-content: center //Spacing between lines along the secondary axis. (spacing between columns for a column layout)
display: -webkit-box
display: -moz-box
display: -ms-flexbox
display: -moz-flex
display: -webkit-flex
display: flex
.editor
height: 100%
overflow-y: auto
.insertSale
flex: none
width: 100%
.formGroupHeading
font-size: 1.6em
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
font-style: normal
font-variant: normal
font-weight: 500
.grid
flex: auto
align-self: stretch
overflow-y: auto
overflow-x: auto
margin-bottom: 20px
border: 0
padding-top: 20px
.table > thead > tr > th
border: 0
padding-top: 0
padding-bottom: 6px
.left
text-align: left
.center
text-align: center
.dataTable
table-layout: fixed
.tdLarge
font-size: 1.3em
.saleRemove
color: red
margin-left: 8px
.saleEdit
color: darkblue
margin-right: 8px

2
imports/ui/Production.js Normal file
View File

@@ -0,0 +1,2 @@
import './Production.html';

38
imports/ui/Products.html Normal file
View File

@@ -0,0 +1,38 @@
<template name="Products">
<div id="products">
<div class="grid">
<div class="dataTable">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name {{>ProductSearch columnName='name'}}</th>
<th>Tags {{>ProductSearch columnName='tags'}}</th>
<th>Aliases {{>ProductSearch columnName='aliases'}}</th>
<th>Measures {{>ProductSearch columnName='measures' collectionQueryColumnName='name' collection='Measures' collectionResultColumnName='_id'}}</th>
</tr>
</thead>
<tbody>
{{#each products}}
{{> Product}}
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
</template>
<template name="Product">
<tr>
<td class="tdLarge noselect nonclickable left">{{name}}</td>
<td class="tdLarge noselect nonclickable left">{{tags}}</td>
<td class="tdLarge noselect nonclickable left">{{aliases}}</td>
<td class="tdLarge noselect nonclickable left">{{measures}}</td>
</tr>
</template>
<template name="ProductSearch">
<div class="">
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}"/>
</div>
</template>

52
imports/ui/Products.import.styl vendored Normal file
View File

@@ -0,0 +1,52 @@
#products
height: 100%;
.editor
height: 100%;
overflow-y: auto;
.grid
height: 100%;
//Flex container options.
flex-flow: column nowrap;
justify-content: space-around; //Spacing between items along the primary axis. (vertical spacing for a column layout)
align-items: flex-start; //Align the items within a line along the primary axis. (horizontal alignment for a column layout)
align-content: center; //Spacing between lines along the secondary axis. (spacing between columns for a column layout)
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -moz-flex;
display: -webkit-flex;
display: flex;
.buttonContainer
//Flex element options.
//flex: 0 0; //Grow, Shrink, Basis
flex: none;
.dataTable
overflow-y: auto;
//Flex element options.
flex: auto;
align-self: stretch;
height: 10%;
max-height: 100%;
.padding
flex: none;
height: 1px;
width: 100%;
#DFAliases
width: 100%;
height: 150px;
overflow: auto;
span
font-family: Arial, Helvetica, sans-serif;
font-size: 1.5em;
cursor: pointer;
display: block;
span.selected
background-color: rgba(255, 248, 131, 0.51);

103
imports/ui/Products.js Normal file
View File

@@ -0,0 +1,103 @@
import './Products.html';
Tracker.autorun(function() {
Meteor.subscribe("products");
Meteor.subscribe("productTags");
});
Template.Products.helpers({
products: function() {
let query = Session.get('searchQuery');
let dbQuery = {};
if(query) {
_.each(_.keys(query), function(key) {
if(_.isFunction(query[key])) dbQuery[key] = query[key]();
else if(_.isObject(query[key])) dbQuery[key] = query[key];
else if(_.isNumber(query[key])) dbQuery[key] = query[key];
else dbQuery[key] = {$regex: query[key], $options: 'i'};
})
}
return Meteor.collections.Products.find(dbQuery, {limit: 20, sort: {name: 1}});
}
});
Template.ProductSearch.events({
"keyup .searchInput": _.throttle(function(event, template) {
let searchQuery = Session.get('searchQuery') || {};
let searchFields = Session.get('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('searchQuery', searchQuery);
}, 500)
});
Template.ProductSearch.helpers({
searchValue: function() {
let searchFields = Session.get('searchFields');
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
}
});
Template.Product.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;
},
tags: function() {
let result = "";
if(this.tags && this.tags.length > 0) {
let tagNames = [];
for(let i = 0; i < this.tags.length; i++) {
let obj = Meteor.collections.ProductTags.findOne(this.tags[i]);
if(obj && obj.name)
tagNames.push(obj.name);
}
result = tagNames.join(", ");
}
return result;
}
});

129
imports/ui/Sales.html Normal file
View File

@@ -0,0 +1,129 @@
<template name="Sales">
<div id="salesMain">
{{#if Template.subscriptionsReady}}
<div class="insertSale">
{{>InsertSale}}
</div>
<div class="grid">
<table class="dataTable table table-striped table-hover">
<thead>
<tr class="headers">
<th class="tdLarge noselect nonclickable" style="width: 80px">Amount</th>
<th class="tdLarge noselect nonclickable">Product</th>
<th class="tdLarge noselect nonclickable" style="width: 140px">Price</th>
<th class="tdLarge noselect nonclickable" style="width: 90px">Measure</th>
<th class="tdLarge noselect nonclickable" style="width: 140px">Date (Week)</th>
<th class="tdLarge noselect nonclickable" style="width: 120px">Venue</th>
<th class="tdLarge noselect nonclickable" style="width: 90px">Actions</th>
</tr>
<tr class="footers">
<th>{{>SaleSearch columnName='amount' width='90%'}}</th>
<th>{{>SaleSearch columnName='productId' collectionQueryColumnName='name' collection='Products' collectionResultColumnName='_id' width='90%'}}</th>
<th>{{>SaleSearch columnName='price' width='90%'}}</th>
<th>{{>SaleSearch columnName='measureId' collectionQueryColumnName='name' collection='Measures' collectionResultColumnName='_id' width='90%'}}</th>
<th></th>
<th>{{>SaleSearch columnName='venueId' collectionQueryColumnName='name' collection='Venues' collectionResultColumnName='_id' width='90%'}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each sales}}
{{> Sale}}
{{/each}}
</tbody>
</table>
</div>
{{else}}
{{/if}}
</div>
</template>
<template name="Sale">
<tr>
<!--{{#if editable}}-->
<td class="tdLarge noselect nonclickable center">{{amount}}</td>
<td class="tdLarge noselect nonclickable left">{{productName productId}}</td>
<td class="tdLarge noselect nonclickable left">{{formatPrice price}}{{#if showTotalPrice amount}} ({{formatTotalPrice price amount}}){{/if}}</td>
<td class="tdLarge noselect nonclickable left">{{measureName measureId}}</td>
<td class="tdLarge noselect nonclickable left">{{formatDate date}}</td>
<td class="tdLarge noselect nonclickable left">{{venueName venueId}}</td>
<td class="tdLarge noselect left"><i class="fa fa-times-circle fa-lg saleRemove clickable" aria-hidden="true"></i></td>
<!--<a class="saleEdit" href="javascript:"><i class="fa fa-pencil-square-o fa-lg" aria-hidden="true"></i></a>/-->
<!--{{else}}-->
<!--<form class="editSaleForm" autocomplete="off">-->
<!--<td><input name="amount" class="form-control" type="number" min="0" data-schema-key='amount' value="{{amount}}" required></td>-->
<!--<td><input name="product" class="form-control" type="text" required/></td>-->
<!--<td><input name="price" class="form-control" type="number" min="0" data-schema-key='currency' value="{{price}}" required></td>-->
<!--<td>-->
<!--<select name="measure" class="form-control" required>-->
<!--{{#each measures}}-->
<!--<option value="{{this._id}}">{{this.name}}</option>-->
<!--{{/each}}-->
<!--</select>-->
<!--</td>-->
<!--<td><input type="date" class="form-control" name="date" data-schema-key='date' value="{{date}}" required></td>-->
<!--<td>-->
<!--<select name="venue" class="form-control" required>-->
<!--{{#each venues}}-->
<!--<option value="{{this._id}}">{{this.name}}</option>-->
<!--{{/each}}-->
<!--</select>-->
<!--</td>-->
<!--<td><a class="editorSave" href="javascript:"><i class="fa fa-check-square-o fa-lg" aria-hidden="true"></i></a>/<a class="editorCancel" href="javascript:"><i class="fa fa-times-circle fa-lg" aria-hidden="true"></i></a></td>-->
<!--</form>-->
<!--{{/if}}-->
</tr>
</template>
<template name="SaleSearch">
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}" style="padding-right: 10px; width: {{width}}"/>
</template>
<template name="InsertSale">
<form id="insertSale" autocomplete="off">
<div class="row">
<div class="col-md-4 col-sm-6">
<div class="formGroupHeading">New Sale</div>
<div class="form-group">
<label for='InsertSaleDate' class='control-label'>Date</label>
<input type="date" class="form-control" name="date" data-schema-key='date' required>
</div>
<div class="form-group">
<label for='InsertSaleProduct' class='control-label'>Product</label>
<input name="product" class="form-control" type="text" required/>
</div>
<div class="form-group">
<label for='InsertSaleVenue' class='control-label'>Venue</label>
<input name="venue" class="form-control" type="text" required/>
</div>
</div>
{{#each productMeasures}}
{{>InsertSaleMeasure this}}
{{/each}}
<div class="col-md-12">
<div class="form-group">
<input type="submit" class="btn btn-success" value="Save Sale">
</div>
</div>
</div>
</form>
</template>
<template name="InsertSaleMeasure">
<div class="col-md-4 col-sm-6 insertSaleMeasure">
<div class="formGroupHeading">{{name}}</div>
<input type="hidden" class="measureId" value="{{this._id}}">
<div class="form-group">
<label class='control-label'>Amount</label>
<input type="number" class="form-control amount" name="amount" min="0" data-schema-key='amount' value="{{amount}}" required>
</div>
<div class="form-group">
<label class='control-label'>Price</label>
<input type="number" class="form-control price" name="price" min="0" data-schema-key='currency' value="{{price}}" required>
</div>
<div class="form-group">
<label class='control-label'>Total</label>
<input type="number" class="form-control total" name="total" tabindex="-1" data-schema-key='currency' value="{{total}}" readonly>
</div>
</div>
</template>

58
imports/ui/Sales.import.styl vendored Normal file
View File

@@ -0,0 +1,58 @@
#salesMain
margin: 10px 20px
height: 100%
//Flex container options.
flex-flow: column nowrap
justify-content: space-around //Spacing between sales along the primary axis. (vertical spacing for a column layout)
align-items: flex-start //Align the sales within a line along the primary axis. (horizontal alignment for a column layout)
align-content: center //Spacing between lines along the secondary axis. (spacing between columns for a column layout)
display: -webkit-box
display: -moz-box
display: -ms-flexbox
display: -moz-flex
display: -webkit-flex
display: flex
.editor
height: 100%
overflow-y: auto
.insertSale
flex: none
width: 100%
.form-group, label
text-align: left
.formGroupHeading
font-size: 1.6em
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
font-style: normal
font-variant: normal
font-weight: 500
.grid
flex: auto
align-self: stretch
overflow-y: auto
overflow-x: auto
margin-bottom: 20px
border: 0
padding-top: 20px
.table > thead > tr > th
border: 0
padding-top: 0
padding-bottom: 6px
.dataTable
table-layout: fixed
.tdLarge
font-size: 1.3em
.saleRemove
color: red
margin-left: 8px
.saleEdit
color: darkblue
margin-right: 8px

267
imports/ui/Sales.js Normal file
View File

@@ -0,0 +1,267 @@
import './Sales.html';
import '/imports/util/selectize/selectize.js'
import ResizeSensor from '/imports/util/resize/ResizeSensor.js';
Tracker.autorun(function() {
Meteor.subscribe("products");
Meteor.subscribe("sales", Session.get('searchQuery'));
});
Template.Sales.onRendered(function() {
// console.log("moving headers");
// try {
// //Move the headers into the header table that will maintain its position.
// //Link the column widths to the header widths.
// let newHeaderRow = this.$('.dataTableHeader thead tr:first');
// let newFooterRow = this.$('.dataTableFooter thead tr:first');
// let oldHeaders = this.$('.dataTable thead tr.headers th');
// let oldFooters = this.$('.dataTable thead tr.footers th');
//
// console.log("header count " + oldHeaders.length);
//
// for(let index = 0; index < oldHeaders.length; index++) {
// let width = this.$('.dataTable tbody tr:first td:eq(' + index + ')').width();
// let oldHeader = oldHeaders.eq(index);
// let newHeader = $("<th\>");
// oldHeader.replaceWith(newHeader);
// newHeader.width(width);
// oldHeader.appendTo(newHeaderRow);
// //Link the two headers so that the visible header changes size with the hidden one.
// //TODO: Turn this off if manually resizing the visible headers - while resizing.
// new ResizeSensor(newHeader, function() {
// oldHeader.width(newHeader.width());
// });
// }
//
// for(let index = 0; index < oldFooters.length; index++) {
// let width = this.$('.dataTable tbody tr:first td:eq(' + index + ')').width();
// let oldFooter = oldFooters.eq(index);
// let newFooter = $("<th\>");
// oldFooter.replaceWith(newFooter);
// newFooter.width(width);
// oldFooter.appendTo(newFooterRow);
// //Link the two headers so that the visible header changes size with the hidden one.
// //TODO: Turn this off if manually resizing the visible headers - while resizing.
// new ResizeSensor(newFooter, function() {
// oldFooter.width(newFooter.width());
// });
// }
// }
// catch(err) {
// console.log(err);
// }
});
Template.Sales.helpers({
sales: function() {
return Meteor.collections.Sales.find({}, {sort: {date: -1}});
}
});
Template.Sale.onCreated(function() {
});
Template.Sale.events({
"click .saleRemove": function(event, template) {
let _this = this;
bootbox.confirm({
message: "Delete the sale?",
buttons: {confirm: {label: "Yes", className: 'btn-success'}, cancel: {label: "No", className: "btn-danger"}},
callback: function(result) {
if(result) {
// Meteor.collections.Sales.remove(_this._id);
Meteor.call('deleteSale', _this._id);
}
}
});
}
});
Template.Sale.helpers({
measureName: function(id) {
return Meteor.collections.Measures.findOne({_id: id}, {fields: {name: 1}}).name;
},
venueName: function(id) {
return Meteor.collections.Venues.findOne({_id: id}, {fields: {name: 1}}).name;
},
productName: function(id) {
return Meteor.collections.Products.findOne({_id: id}, {fields: {name: 1}}).name;
},
formatDate: function(date) {
return moment(date).format("MM/DD/YYYY (w)");
},
formatPrice: function(price) {
return price.toLocaleString("en-US", {style: 'currency', currency: 'USD', minimumFractionDigits: 2});
},
formatTotalPrice: function(price, amount) {
return (price * amount).toLocaleString("en-US", {style: 'currency', currency: 'USD', minimumFractionDigits: 2});
},
showTotalPrice: function(amount) {
return amount > 1;
}
});
Template.SaleSearch.events({
"keyup .searchInput": _.throttle(function(event, template) {
let searchQuery = Session.get('searchQuery') || {};
let searchFields = Session.get('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('searchQuery', searchQuery);
}, 500)
});
Template.SaleSearch.helpers({
searchValue: function() {
let searchFields = Session.get('searchFields');
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
}
});
// let SelectedProduct = new ReactiveVar();
Template.InsertSale.onCreated(function() {
// $('#insertSale').validator();
// $('#insertSale').data('bs.validator');
// this.products = new ReactiveVar([]);
this.selectedProduct = new ReactiveVar();
this.selectedVenue = new ReactiveVar();
});
Template.InsertSale.onRendered(function() {
$('#insertSale').validator();
// this.$('[name="product"]').
// this.autorun(function() {
// this.$('[name="product"]').buildCombo(Meteor.collections.Products.find({}).fetch(), {textAttr: 'name', listClass: 'comboList'});
// });
this.$('[name="product"]').buildCombo({cursor: Meteor.collections.Products.find({}), selection: this.selectedProduct, textAttr: 'name', listClass: 'comboList'});
this.$('[name="venue"]').buildCombo({cursor: Meteor.collections.Venues.find({}), selection: this.selectedVenue, textAttr: 'name', listClass: 'comboList'});
// this.autorun(function(){
// this.products.set(Meteor.collections.Products.find({}));
// }.bind(this));
});
Template.InsertSale.events({
'change #InsertSaleProduct': function(event, template) {
let selectedId = $('#InsertSaleProduct').val();
let selected = Meteor.collections.Products.findOne(selectedId);
template.selectedProduct.set(selected);
},
'click input[type="submit"]': function(event, template) {
event.preventDefault();
$('#insertSale').data('bs.validator').validate(function(isValid) {
if(isValid) {
let sales = [];
let sale = {
date: moment(template.find("[name='date']").value, "YYYY-MM-DD").toDate(),
productId: template.selectedProduct.get()._id,
venueId: template.selectedVenue.get()._id
};
let insertSaleMeasures = template.$(".insertSaleMeasure");
for(let next = 0; next < insertSaleMeasures.length; next++) {
let nextMeasure = $(insertSaleMeasures[next]);
let measureId = nextMeasure.find(".measureId").val();
let price = parseFloat(nextMeasure.find(".price").val()).toFixed(2);
let amount = parseFloat(nextMeasure.find(".amount").val()).toFixed(2);
if(amount > 0) {
let nextSale = _.clone(sale);
nextSale.measureId = measureId;
nextSale.price = price;
nextSale.amount = amount;
sales.push(nextSale);
}
}
// let debug = "Inserting: ";
// for(next in sales) {
// debug += "\n\t" + next;
// }
// console.log(debug);
for(let index = 0; index < sales.length; index++) {
let next = sales[index];
console.log("Inserting: " + JSON.stringify(next));
// Meteor.collections.Sales.insert(next, function(err, id) {
// if(err) console.log(err);
// });
Meteor.call('insertSale', next);
}
}
});
}
});
Template.InsertSale.helpers({
// sales: function() {
// return Meteor.collections.Sales;
// },
products: function() {
//return Meteor.collections.Products.find({});
//return this.products;
return [{label: "Hermies", value: 1}, {label: "Ralfe", value: 2}, {label: "Bob", value: 3}];
},
productMeasures: function() {
let product = Template.instance().selectedProduct.get();
let result = product ? product.measures : [];
for(let i = 0; i < result.length; i++) {
result[i] = Meteor.collections.Measures.findOne(result[i]);
}
if(product) console.log("Found " + result.length + " measures for the product " + product.name);
else console.log("No product!");
return result;
},
venues: function() {
return Meteor.collections.Venues.find({});
}
});
Template.InsertSaleMeasure.onCreated(function() {
let prices = this.parentTemplate().selectedProduct.get().prices;
let price = 0;
if(prices) price = prices[this._id];
this.price = new ReactiveVar(price);
this.amount = new ReactiveVar(0);
});
Template.InsertSaleMeasure.events({
'change .price': function(event, template) {
template.price.set(parseFloat($(event.target).val()).toFixed(2));
},
'change .amount': function(event, template) {
template.amount.set(parseFloat($(event.target).val()).toFixed(2));
}
});
Template.InsertSaleMeasure.helpers({
price: function() {
return Template.instance().price.get();
},
total: function() {
let template = Template.instance();
return template.price.get() * template.amount.get();
},
amount: function() {
return Template.instance().amount.get();
}
});

View File

@@ -0,0 +1,34 @@
<template name="Subcategories">
<div id="subcategories">
<div class="grid">
<div class="dataTable">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name {{>SubcategorySearch columnName='name'}}</th>
<th>Category {{>SubcategorySearch columnName='categoryId' collectionQueryColumnName='name' collection='Categories' collectionResultColumnName='_id'}}</th>
</tr>
</thead>
<tbody>
{{#each subcategories}}
{{> Subcategory}}
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
</template>
<template name="Subcategory">
<tr>
<td>{{name}}</td>
<td>{{categoryName categoryId}}</td>
</tr>
</template>
<template name="SubcategorySearch">
<div class="">
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}"/>
</div>
</template>

52
imports/ui/Subcategories.import.styl vendored Normal file
View File

@@ -0,0 +1,52 @@
#subcategories
height: 100%;
.editor
height: 100%;
overflow-y: auto;
.grid
height: 100%;
//Flex container options.
flex-flow: column nowrap;
justify-content: space-around; //Spacing between items along the primary axis. (vertical spacing for a column layout)
align-items: flex-start; //Align the items within a line along the primary axis. (horizontal alignment for a column layout)
align-content: center; //Spacing between lines along the secondary axis. (spacing between columns for a column layout)
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -moz-flex;
display: -webkit-flex;
display: flex;
.buttonContainer
//Flex element options.
//flex: 0 0; //Grow, Shrink, Basis
flex: none;
.dataTable
overflow-y: auto;
//Flex element options.
flex: auto;
align-self: stretch;
height: 10%;
max-height: 100%;
.padding
flex: none;
height: 1px;
width: 100%;
#DFAliases
width: 100%;
height: 150px;
overflow: auto;
span
font-family: Arial, Helvetica, sans-serif;
font-size: 1.5em;
cursor: pointer;
display: block;
span.selected
background-color: rgba(255, 248, 131, 0.51);

View File

@@ -0,0 +1,76 @@
import './Subcategories.html';
// Tracker.autorun(function() {
// Meteor.subscribe("subcategories");
// });
Template.Subcategories.helpers({
subcategories: function() {
let query = Session.get('searchQuery');
let dbQuery = {};
if(query) {
_.each(_.keys(query), function(key) {
if(_.isFunction(query[key])) dbQuery[key] = query[key]();
else if(_.isObject(query[key])) dbQuery[key] = query[key];
else if(_.isNumber(query[key])) dbQuery[key] = query[key];
else dbQuery[key] = {$regex: query[key], $options: 'i'};
})
}
return Meteor.collections.Subcategories.find(dbQuery, {limit: 20, sort: {updatedAt: -1}});
}
});
// Template.Subcategories.events({
// 'click .trash': function() {
// Meteor.call('deleteSubcategory', this._id);
// console.log("Got here");
// }
// });
Template.Subcategory.helpers({
categoryName: function(id) {
return Meteor.collections.Categories.findOne({_id: id}, {fields: {name: 1}}).name;
}
});
Template.SubcategorySearch.events({
"keyup .searchInput": _.throttle(function(event, template) {
let searchQuery = Session.get('searchQuery') || {};
let searchFields = Session.get('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('searchQuery', searchQuery);
}, 500)
});
Template.SubcategorySearch.helpers({
searchValue: function() {
let searchFields = Session.get('searchFields');
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
}
});

View File

@@ -0,0 +1,90 @@
<template name="UserManagement">
<div id="userManagement">
{{#if Template.subscriptionsReady}}
<div class="insert">
{{>UserInsert}}
</div>
<div class="grid">
<table class="dataTable table table-striped table-hover">
<thead>
<tr class="headers">
<th>Username</th>
<th>Email</th>
<th>Roles</th>
<th>Actions</th>
</tr>
<tr class="footers">
<th>{{>UserSearch columnName='username' maxWidth='40' minWidth='30'}}</th>
<th>{{>UserSearch columnName='email' collectionQueryColumnName='name' collection='Items' collectionResultColumnName='_id' maxWidth='150' minWidth='50'}}</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{{#each users}}
{{> User}}
{{/each}}
</tbody>
</table>
</div>
{{else}}
{{/if}}
</div>
</template>
<template name="User">
<tr>
{{#if editing}}
<td><input name="username" class="form-control" type="text" value="{{username}}" required></td>
<td><input name="email" class="form-control" type="text" value="{{email}}" required></td>
<td class="roles center" style="font-size: 1.2em">
{{#each allRoles}}
<span class="role {{getRoleState this}} noselect">{{name}}</span>
{{/each}}
</td>
<td class="center tdLarge"><i class="editorApply fa fa-check-square-o fa-lg noselect clickable" aria-hidden="true"></i>&nbsp;/&nbsp;<i class="editorCancel fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i></td>
{{else}}
<td class="tdLarge noselect nonclickable">{{username}}</td>
<td class="tdLarge noselect nonclickable">{{email}}</td>
<td class="tdLarge noselect nonclickable">{{roles}}</td>
<td class="center tdLarge"><i class="userRemove fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i>&nbsp;/&nbsp;<i class="userEdit fa fa-pencil-square-o fa-lg noselect clickable" aria-hidden="true"></i></td>
{{/if}}
</tr>
</template>
<template name="UserSearch">
<div class="">
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}" style="max-width: {{maxWidth}}px; min-width: {{minWidth}}px;"/>
</div>
</template>
<template name="UserInsert">
<form name="insert" autocomplete="off">
<div class="row">
<div class="col-md-3 col-sm-0"></div>
<div class="col-md-6 col-sm-12">
<div class="formGroupHeading">New User</div>
<div class="form-group">
<label class='control-label'>User Name</label>
<input name="username" type="text" class="form-control" required>
</div>
<div class="form-group">
<label class='control-label'>Email</label>
<input name="email" class="form-control" type="text" required/>
</div>
<div class="form-group">
<label class='control-label'>Roles</label>
<div class="roles">
{{#each allRoles}}
<span class="role">{{name}}</span>
{{/each}}
</div>
</div>
<div class="form-group">
<input type="submit" class="btn btn-success" value="Create">
</div>
</div>
<div class="col-md-3 col-sm-0"></div>
</div>
</form>
</template>

87
imports/ui/UserManagement.import.styl vendored Normal file
View File

@@ -0,0 +1,87 @@
#userManagement
margin: 20px 20px
height: 100%
//Flex container options.
flex-flow: column nowrap
justify-content: space-around //Spacing between sales along the primary axis. (vertical spacing for a column layout)
align-items: flex-start //Align the sales within a line along the primary axis. (horizontal alignment for a column layout)
align-content: center //Spacing between lines along the secondary axis. (spacing between columns for a column layout)
display: -webkit-box
display: -moz-box
display: -ms-flexbox
display: -moz-flex
display: -webkit-flex
display: flex
text-align: left
.editor
height: 100%
overflow-y: auto
.insert
flex: none
width: 100%
.col-md-6
padding: 10px 30px 0 30px
background: #EFEFEF
border-radius: 1em
.formGroupHeading
font-size: 1.6em
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
font-style: normal
font-variant: normal
font-weight: 500
.grid
flex: auto
align-self: stretch
overflow-y: auto
overflow-x: auto
margin-bottom: 20px
border: 0
padding-top: 20px
.table > thead > tr > th
border: 0
padding-top: 0
padding-bottom: 6px
.dataTable
table-layout: fixed
.tdLarge
font-size: 1.5em
.userRemove
color: red
.userEdit
color: darkblue
.editorApply
color: green
.editorCancel
color: red
td.roles
.role
padding: 4px 4px
border: 1px solid #555
border-radius: .25em
background: white
color: #999
cursor: pointer
.selected
color: black
div.roles
padding: 4px 0
.role
padding: 4px 4px
border: 1px solid #555
border-radius: .25em
background: white
color: #999
cursor: pointer
.selected
color: black
.center
vertical-align: middle !important

View File

@@ -0,0 +1,178 @@
import './UserManagement.html';
import '/imports/util/selectize/selectize.js'
Tracker.autorun(function() {
Meteor.subscribe("users", Session.get('searchQuery'));
Meteor.subscribe("roles");
});
Template.UserManagement.helpers({
users: function() {
return Meteor.collections.Users.find({}, {sort: {username: 1}});
}
});
Template.User.onCreated(function() {
this.edited = new ReactiveVar();
});
Template.User.events({
"click .userEdit": function(event, template) {
template.edited.set(this);
},
"click .userRemove": function(event, template) {
let _this = this;
bootbox.confirm({
message: "Delete the user?",
buttons: {confirm: {label: "Yes", className: 'btn-success'}, cancel: {label: "No", className: "btn-danger"}},
callback: function(result) {
if(result) {
Meteor.call('deleteUser', _this._id, function(error, result) {
if(error) {
sAlert.error(error);
}
else {
sAlert.success("User removed.");
}
});
}
}
});
},
"click .editorCancel": function(event, template) {
template.edited.set(undefined);
},
"click .editorApply": function(event, template) {
let username = template.$("input[name='username']").val().trim();
let email = template.$("input[name='email']").val().trim();
let roleSpans = template.$(".roles > span");
let roles = [];
for(let i = 0; i < roleSpans.length; i++) {
if($(roleSpans[i]).hasClass("selected")) {
roles.push($(roleSpans[i]).text());
}
}
//Basic validation.
if(username && username.length > 0 && email && email.length > 0) {
let emails = _.clone(this.emails);
if(!emails || emails.length == 0) {
emails = [{address: email, verified: true}];
}
else {
emails[0].address = email;
emails[0].verified = true;
}
Meteor.call("updateUser", {_id: this._id, username: username, emails: emails, roles: roles}, function(error, result) {
if(error) {
sAlert.error(error);
}
else {
sAlert.success("User updated.");
}
});
}
template.edited.set(undefined);
},
"click .role": function(event, template) {
$(event.target).toggleClass("selected");
}
});
Template.User.helpers({
email: function() {
return this.emails && this.emails.length > 0 ? this.emails[0].address : "";
},
editing: function() {
return Template.instance().edited.get() == this;
},
allRoles: function() {
return Meteor.collections.UserRoles.find();
},
getRoleState: function(role) {
let user = Template.parentData(1);
return user.roles.includes(role.name) ? "selected" : "";
}
});
Template.UserSearch.events({
"keyup .searchInput": _.throttle(function(event, template) {
let searchQuery = Session.get('searchQuery') || {};
let searchFields = Session.get('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('searchQuery', searchQuery);
}, 500)
});
Template.UserSearch.helpers({
searchValue: function() {
let searchFields = Session.get('searchFields');
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
}
});
Template.UserInsert.onRendered(function() {
this.$('form[name="insert"]').validator();
});
Template.UserInsert.events({
'click input[type="submit"]': function(event, template) {
event.preventDefault();
template.$('form[name="insert"]').data('bs.validator').validate(function(isValid) {
if(isValid) {
let user = {};
let roles = [];
user.username = template.$('input[name="username"]').val();
user.email = template.$('input[name="email"]').val();
let roleSpans = template.$('.role.selected');
for(let i = 0; i < roleSpans.length; i++) {
roles.push($(roleSpans[i]).text());
}
Meteor.call('insertUser', user, roles, function(error, result) {
if(error) {
sAlert.error(error);
}
else {
sAlert.success("User created.");
}
});
}
});
},
"click .role": function(event, template) {
$(event.target).toggleClass("selected");
}
});
Template.UserInsert.helpers({
allRoles: function() {
return Meteor.collections.UserRoles.find();
}
});

View File

@@ -0,0 +1,65 @@
<template name="Auth_page">
<div class="page auth">
<nav>
<div class="nav-group">
<a href="#" class="js-menu nav-item">
<span class="icon-list-unordered"></span>
</a>
</div>
</nav>
<div class="content-scrollable">
<div class="wrapper-auth">
{{> atForm}}
</div>
</div>
</div>
</template>
<template name="override-atPwdFormBtn">
<button type="submit" class="btn-primary">
{{buttonText}}
</button>
</template>
<template name="override-atTextInput">
<div class="input {{#if isValidating}}validating{{/if}} {{#if hasError}}error{{/if}} {{#if hasSuccess}}has-success{{/if}} {{#if feedback}}has-feedback{{/if}}">
<input type="{{type}}" id="at-field-{{_id}}" name="at-field-{{_id}}" placeholder="{{placeholder}}" autocapitalize="none" autocorrect="off">
{{#if hasIcon}}
<span class="{{iconClass}}"></span>
{{/if}}
{{#if hasError}}
<span>{{errorText}}</span>
{{/if}}
</div>
</template>
<template name="override-atTitle">
<h1 class="title-auth">{{title}}</h1>
<p class="subtitle-auth">Signing in allows you to have private lists</p>
</template>
<template name="override-atError">
<div class="list-errors">
{{#each error}}
<div class="list-item">{{errorText}}</div>
{{/each}}
</div>
</template>
<template name="override-atPwdForm">
<div class="at-pwd-form">
<form role="form" id="at-pwd-form" class="{{disabled}}" novalidate action="#" method="POST">
{{#each fields}}
{{> atInput}}
{{/each}}
{{#if showReCaptcha}}
{{> atReCaptcha}}
{{/if}}
{{> atPwdFormBtn}}
{{#if showForgotPasswordLink}}
{{> atPwdLink}}
{{/if}}
</form>
</div>
</template>

View File

@@ -0,0 +1,11 @@
import { Template } from 'meteor/templating';
import './accounts.html';
// We identified the templates that need to be overridden by looking at the available templates
// here: https://github.com/meteor-useraccounts/unstyled/tree/master/lib
// Template['override-atPwdFormBtn'].replaces('atPwdFormBtn');
// Template['override-atPwdForm'].replaces('atPwdForm');
// Template['override-atTextInput'].replaces('atTextInput');
// Template['override-atTitle'].replaces('atTitle');
// Template['override-atError'].replaces('atError');

8
imports/ui/helpers.js Normal file
View File

@@ -0,0 +1,8 @@
// General use helpers - available to all views.
UI.registerHelper('currentUserName', function() {
if(Meteor.user()){
return Meteor.user().emails[0].address;
}
});

View File

@@ -0,0 +1,106 @@
<template name="Body">
{{> sAlert}}
<div id="layoutBody">
<div class="mainBody">
<div class="leftSidebar">
<i class="fa fa-sign-out fa-2x signOut" aria-hidden="true"></i>
<div class="logo">
<img src="/images/PetitTetonLogo_v2.png"/>
</div>
<ul>
{{#if isInRole 'manage'}}
<li class="{{isActiveRoute 'UserManagement'}}">
<a href="{{pathFor 'UserManagement'}}">
User Management
</a>
</li>
{{/if}}
<li class="{{isActiveRoute 'Sales'}}">
<a href="{{pathFor 'Sales'}}">
Sales <span class="tag">Test Tag</span>
</a>
</li>
<li class="{{isActiveRoute 'Production'}}">
<a href="{{pathFor 'Production'}}">
Production <span class="tag">sample</span>
</a>
</li>
<li class="{{isActiveRoute 'Products'}}">
<a href="{{pathFor 'Products'}}">
Products
</a>
</li>
<li class="{{isActiveRoute 'Pricing'}}">
<a href="{{pathFor 'Pricing'}}">
Pricing
</a>
</li>
<li class="{{isActiveRoute 'ProductTags'}}">
<a href="{{pathFor 'ProductTags'}}">
Tags
</a>
</li>
<li class="{{isActiveRoute 'Measures'}}">
<a href="{{pathFor 'Measures'}}">
Measures
</a>
</li>
</ul>
</div>
<div class="contentBody">
<div class="contentContainer">
<div class="header">
&nbsp;
</div>
<div class="content">
{{> Template.dynamic template=content}}
</div>
</div>
</div>
</div>
<div class="footer">
&copy; Petit Teton LLC 2017
</div>
</div>
</template>
<!--<template name="Body">-->
<!--<div id="layoutBody">-->
<!--<div class="bodyTableRow">-->
<!--<div class="left bodyTableCell">-->
<!--<ul>-->
<!--{{#if isInRole 'manage'}}-->
<!--<li class="{{isActiveRoute 'UserManagement'}}">-->
<!--<a href="{{pathFor 'UserManagement'}}">-->
<!--User Management-->
<!--</a>-->
<!--</li>-->
<!--{{/if}}-->
<!--<li class="{{isActiveRoute 'Sales'}}">-->
<!--<a href="{{pathFor 'Sales'}}">-->
<!--Sales <span class="tag">Test Tag</span>-->
<!--</a>-->
<!--</li>-->
<!--<li class="{{isActiveRoute 'Production'}}">-->
<!--<a href="{{pathFor 'Production'}}">-->
<!--Production <span class="tag">sample</span>-->
<!--</a>-->
<!--</li>-->
<!--</ul>-->
<!--</div>-->
<!--<div class="bodyTableCell">-->
<!--<div class="bodyTable">-->
<!--<div class="header bodyTableRow">-->
<!--&nbsp;-->
<!--</div>-->
<!--<div class="content bodyTableRow">-->
<!--{{> Template.dynamic template=content}}-->
<!--</div>-->
<!--</div>-->
<!--</div>-->
<!--</div>-->
<!--<div class="footer bodyTableRow">-->
<!--&copy; Petit Teton LLC 2017-->
<!--</div>-->
<!--</div>-->
<!--</template>-->

279
imports/ui/layouts/Body.import.styl vendored Normal file
View File

@@ -0,0 +1,279 @@
#layoutBody
width: 100%
height: 100%
display: table
text-align: center
margin: 0
padding: 0
border: 0
.mainBody
display: table
height: 100%
width: 100%
margin: 0
padding: 0
border: 0
.leftSidebar
display: table-cell
position: relative
border: 0
vertical-align: top
padding: 0
text-align: left
width: 220px
height: 100%
//Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#627d4d+0,1f3b08+100;Olive+3D
background: #90b272 //Old browsers
background: -moz-linear-gradient(-180deg, #90b272 0%, #4d7727 100%) //FF3.6-15
background: -webkit-linear-gradient(-180deg, #90b272 0%,#4d7727 100%) //Chrome10-25,Safari5.1-6
background: linear-gradient(180deg, #90b272 0%,#4d7727 100%) //W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+
font-size: 14px
font-weight: 700
.signOut
position: absolute
left: 10px
top: 10px
color: white
cursor: pointer
.signOut:hover
color: #BBB
.signOut:active
color: black
.logo
text-align: center
margin-top: 20px
ul
padding: 20px 0 0 0
margin: 0
list-style: none
li:first-child
border-top: 1px solid #e4e5e7
li
border-bottom: 1px solid #e4e5e7
color: #96a2ae
text-transform: uppercase
display: block
a
color: black
padding: 15px 20px
cursor: pointer
text-decoration: none
display: block
.tag
padding: .2em .5em
font-size: .7em
color: #fff
white-space: nowrap
vertical-align: baseline
border-radius: .25em
border: 1px solid #000000
float: right
li:hover
background-color: #333
li.active
background-color: #333
a
color: #96a2ae
.contentBody
display: table-cell
//background: #4d7727
.contentContainer
display: table
width: 100%
height: 100%
//border-radius 20px
//border: 0;
background: white
.header
display: table-row
background: #90b272
width: 100%
height: 1px
.content
display: table-row
width: 100%
-webkit-box-shadow: inset 4px 2px 6px 2px rgba(168,165,168,1)
-moz-box-shadow: inset 4px 2px 6px 2px rgba(168,165,168,1)
box-shadow: inset 4px 2px 6px 2px rgba(168,165,168,1)
.footer
display: table-row
height: 1px
text-align: center
background: #4d7727
color: white
//#layoutBody
// width: 100%
// height: 100%
// display: table
// margin: 0
// padding: 0
// border: 0
//
// .bodyTable
// display: table
// margin: 0
// padding: 0
// border: 0
// .bodyTableRow
// display: table-row
// .bodyTableCell
// display: table-cell
//
// .left
// display: table-cell
// border: 0
// vertical-align: top
// padding: 0
// text-align: left
// width: 220px
// height: 100%
// //Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#627d4d+0,1f3b08+100;Olive+3D
// background: #627d4d //Old browsers
// background: -moz-linear-gradient(-180deg, #627d4d 0%, #1f3b08 100%) //FF3.6-15
// background: -webkit-linear-gradient(-180deg, #627d4d 0%,#1f3b08 100%) //Chrome10-25,Safari5.1-6
// background: linear-gradient(180deg, #627d4d 0%,#1f3b08 100%) //W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+
// font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif
// font-size: 14px
// font-weight: 700
//
// ul
// padding: 50px 0 0 0
// margin: 0
// list-style: none
//
// li:first-child
// border-top: 1px solid #e4e5e7
// li
// border-bottom: 1px solid #e4e5e7
// color: #96a2ae
// text-transform: uppercase
// display: block
//
// a
// color: #96a2ae
// padding: 15px 20px
// cursor: pointer
// text-decoration: none
// display: block
//
// .tag
// padding: .2em .5em
// font-size: .7em
// color: #fff
// white-space: nowrap
// vertical-align: baseline
// border-radius: .25em
// border: 1px solid #000000
// float: right
// li:hover
// background-color: #333
// li.active
// background-color: #333
//
// .header
// height: 1px
// background: #627d4d
// width: 100%
// .content
// background: white
// .footer
// text-align: center
// height: 1px;
// background: #1f3b08
// color: white
//
//.header
// display: table-row
// height: 1px
// background: #627d4d
//
//#layoutBody.body
// display: table
// margin: 0
// padding: 0
// width: 100%
// height: 100%
//
// .body
// display: table-row
// width: 100%
//
// .left
// display: table-cell
// border: 0
// vertical-align: top
// padding: 0
// text-align: left
// width: 220px
// //Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#627d4d+0,1f3b08+100;Olive+3D
// background: #627d4d //Old browsers
// background: -moz-linear-gradient(-180deg, #627d4d 0%, #1f3b08 100%) //FF3.6-15
// background: -webkit-linear-gradient(-180deg, #627d4d 0%,#1f3b08 100%) //Chrome10-25,Safari5.1-6
// background: linear-gradient(180deg, #627d4d 0%,#1f3b08 100%) //W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+
// font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif
// font-size: 14px;
// font-weight: 700
//
// ul
// padding: 50px 0 0 0
// margin: 0
// list-style: none
//
// li:first-child
// border-top: 1px solid #e4e5e7
// li
// border-bottom: 1px solid #e4e5e7
// color: #96a2ae
// text-transform: uppercase
// display: block
//
// a
// color: #96a2ae
// padding: 15px 20px
// cursor: pointer
// text-decoration: none
// display: block
//
// .tag
// padding: .2em .5em
// font-size: .7em
// color: #fff
// white-space: nowrap
// vertical-align: baseline
// border-radius: .25em
// border: 1px solid #000000
// float: right
// li:hover
// background-color: #333
// li.active
// background-color: #333
// .main
// display: table-row
// background: white
// border: 0
// vertical-align: top
// padding: 0
// text-align: left
//
// .footer
// display: table-row
// text-align: center
// height: 1px;
// background: #1f3b08
// color: white

View File

@@ -0,0 +1,9 @@
import { Template } from 'meteor/templating';
import './Body.html';
Template.Body.events({
"click .signOut": function(event, template) {
AccountsTemplates.logout();
}
});

View File

@@ -0,0 +1,7 @@
<template name="Full">
<div id="full" class="content">
<div class="form">
{{> Template.dynamic template=content}}
</div>
</div>
</template>

11
imports/ui/layouts/Full.import.styl vendored Normal file
View File

@@ -0,0 +1,11 @@
#full.content
//width: 100%
//height: 100%
//background-image: linear-gradient(to bottom, #315481, #918e82 100%)'
background: white
.form
//margin: 5% auto 0 auto
margin: 0 auto
width: 300px

View File

@@ -0,0 +1 @@
import './Full.html';