Redesigned the querying for the sale duplicates screen to use aggregation; Finished the styling of the sale duplicate screen; Tested the functionality of sale duplicates; Added a way to show hidden (ignored) duplicates.

This commit is contained in:
Wynne Crisman
2017-05-26 11:17:32 -07:00
parent e1b0b19589
commit 210517a5c2
42 changed files with 15153 additions and 8505 deletions

View File

@@ -1,37 +1,44 @@
<template name="Measures">
<div id="measures">
<div class="tableControls">
<span class="controlLabel">Show Hidden</span>
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
<label>
<input type="checkbox" name="showHidden"><span></span>
</label>
{{#if Template.subscriptionsReady}}
<div class="tableControls">
<span class="controlLabel">Show Hidden</span>
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
<label>
<input type="checkbox" name="showHidden"><span></span>
</label>
</div>
<span class="pagination">
<span class="prevMeasures noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
<span class="nextMeasures noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
</span>
</div>
<span class="pagination">
<span class="prevMeasures noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
<span class="nextMeasures noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
</span>
</div>
<div class="tableContainer">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="name">Name {{>MeasureSearch columnName='name'}}</th>
<th class="postfix">Postfix {{>MeasureSearch columnName='postfix'}}</th>
<th class="actions">Actions <span class="newMeasureButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
</tr>
<!--<button type="button" name="newMeasureButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
</thead>
<tbody>
{{#if displayNewMeasure}}
{{> MeasureEditor isNew=true}}
{{/if}}
{{#each measures}}
{{> Measure}}
{{/each}}
</tbody>
</table>
</div>
<div class="listRow">
<div class="listCell">
<div class="tableContainer">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="name">Name {{>MeasureSearch columnName='name'}}</th>
<th class="postfix">Postfix {{>MeasureSearch columnName='postfix'}}</th>
<th class="actions">Actions <span class="newMeasureButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
</tr>
<!--<button type="button" name="newMeasureButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
</thead>
<tbody>
{{#if displayNewMeasure}}
{{> MeasureEditor isNew=true}}
{{/if}}
{{#each measures}}
{{> Measure}}
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
{{else}}
{{/if}}
</div>
</template>

View File

@@ -1,6 +1,9 @@
#measures
margin: 20px 20px
display: table
content-box: border-box
padding: 10px 20px
height: 100%
width: 100%
text-align: left
.tableControls
@@ -18,77 +21,89 @@
top: -4px
display: inline-block
.tableContainer
width: 100%
margin-bottom: 20px
border: 0
font-size: 12.5px
table
table-layout: fixed
.listRow
display: table-row
.listCell
display: table-cell
position: relative
height: 100%
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
.tableContainer
position: absolute
top: 0
bottom: 0
left: 0
right: 0
width: auto
height: auto
border: 0
font-size: 12.5px
overflow-y: auto
table
table-layout: fixed
width: 100%
.measureSearch
margin: 3px 0 2px 1px
.measureEditorTd
background: #deeac0
input[name="name"], input[name="postfix"]
width: 100%
.editorDiv
margin: 4px 0
label
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
font-size: .9em
padding-bottom: 4px
select2
font-size: .4em
> thead
> tr
> th.name
width: auto
> th.postfix
width: auto
> th.actions
width: 90px
text-align: center
.newMeasureButton
margin-top: 4px
padding: 0px 12px
.fa-plus-circle
display: inline-block
.fa-times-circle
display: none
.newMeasureButton.active
background-color: #fb557b
color: black
.fa-times-circle
display: inline-block
.fa-plus-circle
display: none
> tbody
> tr
.actionRemove
color: #F77
.actionEdit
color: #44F
.editorApply
color: green
.editorCancel
color: red
> tr.deactivated
background-color: #fac0d1
.actionActivate
color: #158b18
.actionHide
color: #6a0707
.actionEdit
color: #0101e4
> tr.deactivated:hover
background-color: #ffcadb
> tr.hidden
background-color: #e995ff
.actionEdit
color: #0101e4
.actionShow
color: #027905
> tr.hidden:hover
background-color: #ffb5ff

View File

@@ -0,0 +1,27 @@
<template name="MiscManagement">
<div id="miscManagement">
{{#if Template.subscriptionsReady}}
<div class="controls">
<a href="javascript:" class="cleanDates">Clean Dates (removes time components)</a><br/>
<!--<a href="javascript:" class="importMissingSalesData">Import Sales Data (JSON)</a><br/>-->
<a href="javascript:" class="clearLogs">Clear Logs</a><br/>
<a href="javascript:" class="countDuplicateSales">Count Duplicate Sales</a><br/>
<a href="javascript:" class="deleteDuplicateSales">Delete Duplicate Sales</a><br/>
<div class="logCount">{{logCount}}</div>
</div>
<div class="pageContentRow">
<div class="pageContentCell">
<div class="pageContentContainer">
<ul class="logs">
{{#each logs}}
<li>{{message}}</li>
{{/each}}
</ul>
</div>
</div>
</div>
{{else}}
{{/if}}
</div>
</template>

35
imports/ui/MiscManagement.import.styl vendored Normal file
View File

@@ -0,0 +1,35 @@
#miscManagement
display: table
content-box: border-box
padding: 10px 20px
height: 100%
width: 100%
text-align: left
.controls
text-align: right
margin-right: 20px
.pageContentRow
display: table-row
.pageContentCell
display: table-cell
position: relative
height: 100%
width: 100%
.pageContentContainer
position: absolute
top: 0
bottom: 0
left: 0
right: 0
width: auto
height: auto
border: 0
font-size: 12.5px
overflow-y: auto
.logs
list-style-type: none
height: 100%
:hover
background: #CCC

View File

@@ -0,0 +1,76 @@
import './MiscManagement.html';
import '/imports/util/selectize/selectize.js'
let PREFIX = "MiscManagement";
Meteor.subscribe("logs");
Meteor.subscribe("products");
Meteor.subscribe("venues");
Meteor.subscribe("measures");
Template.MiscManagement.helpers({
logs: function() {
return Meteor.collections.Logs.find({}, {sort: {date: 1}});
},
logCount: function() {
return Meteor.collections.Logs.find({}).count();
}
});
Template.MiscManagement.events({
"click .cleanDates": function(event, template) {
Meteor.call("cleanDates");
},
"click .importMissingSalesData": function(event, template) {
console.log("Calling importMissingSales");
Meteor.call("importMissingSales");
},
"click .clearLogs": function(event, template) {
Meteor.call("clearLogs");
},
"click .countDuplicateSales": function(event, template) {
Meteor.log.info("Starting to count duplicates...");
let products = Meteor.collections.Products.find({}).fetch();
let venues = Meteor.collections.Venues.find({}).fetch();
let measures = Meteor.collections.Measures.find({}).fetch();
let productNameMap = {};
let venueNameMap = {};
let measureNameMap = {};
for(let i = 0; i < products.length; i++) {
productNameMap[products[i]._id] = products[i].name;
}
for(let i = 0; i < venues.length; i++) {
venueNameMap[venues[i]._id] = venues[i].name;
}
for(let i = 0; i < measures.length; i++) {
measureNameMap[measures[i]._id] = measures[i].name;
}
Meteor.call("countSales", function(err, result) {
if(err) Meteor.log.error(err);
else {
let salesCount = result;
Meteor.call("countDuplicateSales", function(err, result) {
if(err) Meteor.log.error(err);
else {
Meteor.log.info("Duplicate Sales Counted: " + result.length + " out of " + salesCount + " total sales.");
for(let i = 0; i < result.length; i++) {
let sale = result[i][0];
Meteor.log.info("\tdate: " + sale.date + " product: " + productNameMap[sale.productId] + " venue: " + venueNameMap[sale.venueId] + " measure" + measureNameMap[sale.measureId] + " price: " + sale.price.toFixed(2) + " amount: " + sale.amount + " id: " + sale._id);
sale = result[i][1];
Meteor.log.info("\tdate: " + sale.date + " product: " + productNameMap[sale.productId] + " venue: " + venueNameMap[sale.venueId] + " measure" + measureNameMap[sale.measureId] + " price: " + sale.price.toFixed(2) + " amount: " + sale.amount + " id: " + sale._id);
Meteor.log.info(" -- ");
}
}
});
}
});
},
"click .deleteDuplicateSales": function(event, template) {
Meteor.call("deleteDuplicateSales");
}
});

View File

@@ -1,54 +1,61 @@
<template name="Pricing">
<div id="pricing">
<div class="controls">
<div class="measureGroup" style="vertical-align: bottom">
<label class='controlLabel'>Selected Measure: </label>
<select name="measures">
{{#each measures}}
<option value="{{_id}}">{{name}}</option>
{{/each}}
</select>
</div>
<div class="controlGroup" style="text-align: center">
<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" title="Applies the price to selected products." value="Apply">
<input type="button" class="btn btn-danger resetButton" title="Resets this form." value="Reset">
<br/>
<!--<span class="toggleUpdateHistory toggleButton clickable">Set Prev</span>-->
<div class="previousSettings 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>
{{#if Template.subscriptionsReady}}
<div class="controls">
<div class="measureGroup" style="vertical-align: bottom">
<label class='controlLabel'>Selected Measure: </label>
<select name="measures">
{{#each measures}}
<option value="{{_id}}">{{name}}</option>
{{/each}}
</select>
</div>
<div class="controlGroup" style="text-align: center">
<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" title="Applies the price to selected products." value="Apply">
<input type="button" class="btn btn-danger resetButton" title="Resets this form." value="Reset">
<br/>
<!--<span class="toggleUpdateHistory toggleButton clickable">Set Prev</span>-->
<div class="previousSettings 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' style="margin-left: 10px">Effective: </label>
<input type="date" class="form-control" name="date" data-schema-key='date' required>
</div>
</div>
<span class="pagination">
<span class="prevProducts noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
<span class="nextProducts noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
</span>
</div>
<div class="listRow">
<div class="listCell">
<div class="tableContainer">
<table class="table table-striped table-hover">
<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>
<label class='controlLabel' style="margin-left: 10px">Effective: </label>
<input type="date" class="form-control" name="date" data-schema-key='date' required>
</div>
</div>
<span class="pagination">
<span class="prevProducts noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
<span class="nextProducts noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
</span>
</div>
<div class="tableContainer">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="name">Name</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>
{{else}}
{{/if}}
</div>
</template>

View File

@@ -1,6 +1,9 @@
#pricing
margin: 20px 20px
display: table
content-box: border-box
padding: 10px 20px
height: 100%
width: 100%
text-align: left
.controls
@@ -60,26 +63,39 @@
.resetButton
margin-left: 20px
.tableContainer
width: 100%
margin-bottom: 20px
border: 0
font-size: 12.5px
table
table-layout: fixed
.listRow
display: table-row
.listCell
display: table-cell
position: relative
height: 100%
width: 100%
> thead
> tr
> th.name
width: auto
> th.current
width: 200px
> th.previous
width: 200px
> th.changeDate
width: 200px
> tbody
> tr.deactivated
background-color: #fac0d1
> tr.deactivated:hover
background-color: #ffcadb
.tableContainer
position: absolute
top: 0
bottom: 0
left: 0
right: 0
width: auto
height: auto
border: 0
font-size: 12.5px
overflow-y: auto
table
table-layout: fixed
width: 100%
> thead
> tr
> th.name
width: auto
> th.current
width: 200px
> th.previous
width: 200px
> th.changeDate
width: 200px
> tbody
> tr.deactivated
background-color: #fac0d1
> tr.deactivated:hover
background-color: #ffcadb

View File

@@ -1,6 +1,13 @@
import './Pricing.html';
/**
* Notes:
* The Product object has a prices field which is an object whose fields names are Measure ID's. Each field value (for each Measure ID) is an object that has a 'price', 'effectiveDate', and 'previousPrice'.
* The effectiveDate field stores the date as a number in the format YYYYMMDD. Converting this number into a local date is done with moment(sale.date.toString(), "YYYYMMDD").toDate(), and converting it to a number from a date can be accomplished with ~~(moment(date).format("YYYYMMDD")), where the ~~ is a bitwise not and converts a string to a number quickly and reliably.
* Because the structure of the Product object is so complicated, the normal checking that is done by the framework cannot be used.
*/
let QUERY_LIMIT = 20;
let PREFIX = "Pricing.";
@@ -70,7 +77,7 @@ Template.Pricing.events({
Meteor.call("clearProductPrice", productIds, measureId)
}
else {
date = moment(date ? date : new Date().toDateInputValue(), "YYYY-MM-DD").toDate();
date = ~~(moment(date ? date : new Date().toDateInputValue(), "YYYY-MM-DD").format("YYYYMMDD")); // The ~~ is a bitwise not which converts the string into a number in the format of YYYYMMDD for storage in the database; to avoid timezone issues.
setPrevious = setPrevious == true || setPrevious == 'on' || setPrevious == "true" || setPrevious == "yes";
if(setPrevious == true && !date) {
@@ -117,9 +124,8 @@ Template.PricingForProduct.helpers({
},
priceChangeDate: function() {
let measureId = Session.get(PREFIX + "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)") : "-";
return this.prices && measureId && this.prices[measureId] && this.prices[measureId].effectiveDate ? moment(this.prices[measureId].effectiveDate.toString(), "YYYYMMDD").format("MM/DD/YYYY (w)") : "-";
},
rowClass: function() {
return this.deactivated ? "deactivated" : "";

View File

@@ -26,20 +26,24 @@
</span>
</div>
</div>
<div class="tableContainer">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="name">Name {{>ProductTag_ProductSearch columnName='name'}}</th>
<th class="tags">Tags {{>ProductTag_ProductSearch columnName='tags' collectionQueryColumnName='name' collection='ProductTags' collectionResultColumnName='_id'}}</th>
</tr>
</thead>
<tbody>
{{#each products}}
{{> ProductTag_Product}}
{{/each}}
</tbody>
</table>
<div class="listRow">
<div class="listCell">
<div class="tableContainer">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="name">Name {{>ProductTag_ProductSearch columnName='name'}}</th>
<th class="tags">Tags {{>ProductTag_ProductSearch columnName='tags' collectionQueryColumnName='name' collection='ProductTags' collectionResultColumnName='_id'}}</th>
</tr>
</thead>
<tbody>
{{#each products}}
{{> ProductTag_Product}}
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
{{else}}
{{/if}}

View File

@@ -1,6 +1,9 @@
#productTags
margin: 20px 20px
display: table
content-box: border-box
padding: 10px 20px
height: 100%
width: 100%
text-align: left
.tagInfo
@@ -107,25 +110,38 @@
display: table-cell
width: 240px
vertical-align: bottom;
.tableContainer
width: 100%
margin-bottom: 20px
border: 0
font-size: 12.5px
table
table-layout: fixed
.listRow
display: table-row
.listCell
display: table-cell
position: relative
height: 100%
width: 100%
> thead
> tr
> th.name
width: auto
> th.tags
width: auto
> tbody
> tr.deactivated
background-color: #fac0d1
> tr.deactivated:hover
background-color: #ffcadb
.tableContainer
position: absolute
top: 0
bottom: 0
left: 0
right: 0
width: auto
height: auto
border: 0
font-size: 12.5px
overflow-y: auto
table
table-layout: fixed
width: 100%
> thead
> tr
> th.name
width: auto
> th.tags
width: auto
> tbody
> tr.deactivated
background-color: #fac0d1
> tr.deactivated:hover
background-color: #ffcadb
td.roles
.role
padding: 4px 4px

View File

@@ -1,39 +1,46 @@
<template name="Products">
<div id="products">
<div class="tableControls">
<span class="controlLabel">Show Hidden</span>
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
<label>
<input type="checkbox" name="showHidden"><span></span>
</label>
{{#if Template.subscriptionsReady}}
<div class="tableControls">
<span class="controlLabel">Show Hidden</span>
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
<label>
<input type="checkbox" name="showHidden"><span></span>
</label>
</div>
<span class="pagination">
<span class="prevProducts noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
<span class="nextProducts noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
</span>
</div>
<span class="pagination">
<span class="prevProducts noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
<span class="nextProducts noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
</span>
</div>
<div class="tableContainer">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="name">Name {{>ProductSearch columnName='name'}}</th>
<th class="tags">Tags {{>ProductSearch columnName='tags' collectionQueryColumnName='name' collection='ProductTags' collectionResultColumnName='_id'}}</th>
<th class="aliases">Aliases {{>ProductSearch columnName='aliases'}}</th>
<th class="measures">Measures {{>ProductSearch columnName='measures' collectionQueryColumnName='name' collection='Measures' collectionResultColumnName='_id'}}</th>
<th class="actions">Actions <span class="newProductButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
</tr>
<!--<button type="button" name="newProductButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
</thead>
<tbody>
{{#if displayNewProduct}}
{{> ProductEditor isNew=true}}
{{/if}}
{{#each products}}
{{> Product}}
{{/each}}
</tbody>
</table>
</div>
<div class="listRow">
<div class="listCell">
<div class="tableContainer">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="name">Name {{>ProductSearch columnName='name'}}</th>
<th class="tags">Tags {{>ProductSearch columnName='tags' collectionQueryColumnName='name' collection='ProductTags' collectionResultColumnName='_id'}}</th>
<th class="aliases">Aliases {{>ProductSearch columnName='aliases'}}</th>
<th class="measures">Measures {{>ProductSearch columnName='measures' collectionQueryColumnName='name' collection='Measures' collectionResultColumnName='_id'}}</th>
<th class="actions">Actions <span class="newProductButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
</tr>
<!--<button type="button" name="newProductButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
</thead>
<tbody>
{{#if displayNewProduct}}
{{> ProductEditor isNew=true}}
{{/if}}
{{#each products}}
{{> Product}}
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
{{else}}
{{/if}}
</div>
</template>

View File

@@ -1,6 +1,9 @@
#products
margin: 20px 20px
display: table
content-box: border-box
padding: 10px 20px
height: 100%
width: 100%
text-align: left
.tableControls
@@ -18,81 +21,93 @@
top: -4px
display: inline-block
.tableContainer
width: 100%
margin-bottom: 20px
border: 0
font-size: 12.5px
table
table-layout: fixed
.listRow
display: table-row
.listCell
display: table-cell
position: relative
height: 100%
width: 100%
.productSearch
margin: 3px 0 2px 1px
.productEditorTd
background: #deeac0
input[name="name"], .productTagsEditor, .productAliasesEditor, .productMeasuresEditor
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.tags
width: 220px
> th.aliases
width: 220px
> th.measures
width: 220px
> th.actions
width: 90px
text-align: center
.newProductButton
margin-top: 4px
padding: 0px 12px
.fa-plus-circle
display: inline-block
.fa-times-circle
display: none
.newProductButton.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
.tableContainer
position: absolute
top: 0
bottom: 0
left: 0
right: 0
width: auto
height: auto
border: 0
font-size: 12.5px
overflow-y: auto
table
table-layout: fixed
width: 100%
.productSearch
margin: 3px 0 2px 1px
.productEditorTd
background: #deeac0
input[name="name"], .productTagsEditor, .productAliasesEditor, .productMeasuresEditor
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.tags
width: 220px
> th.aliases
width: 220px
> th.measures
width: 220px
> th.actions
width: 90px
text-align: center
.newProductButton
margin-top: 4px
padding: 0px 12px
.fa-plus-circle
display: inline-block
.fa-times-circle
display: none
.newProductButton:active
background-color: #fb557b
color: black
.fa-times-circle
display: inline-block
.fa-plus-circle
display: none
> tbody
> tr
.actionRemove
color: #F77
.actionEdit
color: #44F
.editorApply
color: green
.editorCancel
color: red
> tr.deactivated
background-color: #fac0d1
.actionActivate
color: #158b18
.actionHide
color: #6a0707
.actionEdit
color: #0101e4
> tr.deactivated:hover
background-color: #ffcadb
> tr.hidden
background-color: #e995ff
.actionEdit
color: #0101e4
.actionShow
color: #027905
> tr.hidden:hover
background-color: #ffb5ff

View File

@@ -0,0 +1,61 @@
<template name="SaleDuplicates">
<div id="saleDuplicates">
<div class="controls">
<div class="pageControls">
<input class="duplicateScan btn btn-info" type="button" value="Scan For Duplicates"/>
</div>
<div class="tableControls">
<span class="controlLabel">Show Hidden</span>
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
<label>
<input type="checkbox" name="showHidden" {{showHidden}}><span></span>
</label>
</div>
</div>
</div>
<div class="listRow">
<div class="listCell">
<div class="tableContainer">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="amount noselect nonclickable">Duplicates</th>
<th class="amount noselect nonclickable">Amount</th>
<th class="product noselect nonclickable">Product <br/>{{>SaleDuplicateSearch columnName='productName' width='90%'}}</th>
<th class="price noselect nonclickable">Price</th>
<th class="measure noselect nonclickable">Measure</th>
<th class="saleDate noselect nonclickable">Date (Week)</th>
<th class="createdDate noselect nonclickable">Created On</th>
<th class="venue noselect nonclickable">Venue</th>
<th class="actions noselect nonclickable">Actions</th>
</tr>
</thead>
<tbody>
{{#each sales}}
{{> SaleDuplicate}}
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<template name="SaleDuplicate">
<tr class="{{duplicateClasses}}">
<td class="tdLarge noselect nonclickable center">{{duplicateCount}}</td>
<td class="tdLarge noselect nonclickable center">{{amount}}</td>
<td class="tdLarge noselect nonclickable left">{{productName}}</td>
<td class="tdLarge noselect nonclickable left">{{formatPrice price}}{{#if showTotalPrice amount}} ({{formatTotalPrice price amount}}){{/if}}</td>
<td class="tdLarge noselect nonclickable left">{{measureName}}</td> <!-- measureName measureId -->
<td class="tdLarge noselect nonclickable left">{{formatDateAndWeek date}}</td>
<td class="tdLarge noselect nonclickable left">{{formatDateTime createdAt}}</td>
<td class="tdLarge noselect nonclickable left">{{venueName}}</td>
<td class="tdLarge noselect left actions"><i class="fa fa-check fa-lg clickable ignoreDuplicatesButton {{#if ignoreDuplicates}}hidden{{/if}}" title="Ignore All Duplicates" aria-hidden="true"></i> <i class="fa fa-minus-circle fa-lg clickable removeAllDuplicatesButton" title="Remove All Duplicates" aria-hidden="true"></i> <span class="clickable removeOneDuplicateButton" title="Remove One Duplicate"><i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i><sup>1</sup></span></td>
</tr>
</template>
<template name="SaleDuplicateSearch">
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}" style="padding-right: 10px; width: {{width}}"/>
</template>

149
imports/ui/SaleDuplicates.import.styl vendored Normal file
View File

@@ -0,0 +1,149 @@
#saleDuplicates
display: table
content-box: border-box
padding: 10px 20px
height: 100%
width: 100%
text-align: left
.controls
text-align: left
display: table
width: 100%
.pageControls
padding: 4px 8px
margin: 4px 8px
display: table-cell
width: 240px
.tableControls
text-align: right
padding: 4px 8px
margin: 4px 12px 4px 8px
display: table-cell
.toggleShowHidden
margin: 0 40px 0 0
position: relative
top: -4px
display: inline-block
.listRow
display: table-row
.listCell
display: table-cell
position: relative
height: 100%
width: 100%
.tableContainer
position: absolute
top: 0
bottom: 0
left: 0
right: 0
width: auto
height: auto
//width: 100%
//margin-bottom: 20px
border: 0
font-size: 12.5px
overflow-y: auto
//height: 100%
label
font-size: 10px
font-weight: 800
table
table-layout: fixed
min-width: 100%
.saleRemove
color: red
margin-left: 8px
.saleEdit
color: darkblue
margin-right: 8px
.editorApply
color: green
.editorCancel
color: red
thead
> tr
> th.amount
width: 90px
> th.product
width: auto
min-width: 140px
> th.price
width: 140px
> th.measure
width: 100px
> th.saleDate
width: 140px
> th.createdDate
width: 100px
> th.venue
width: 160px
> th.actions
width: 90px
tbody
> tr
> td.actions
.ignoreDuplicatesButton
padding: 0 2px
color: green
.ignoreDuplicatesButton:hover
color: #00bb00
.ignoreDuplicatesButton:active
color: black
.ignoreDuplicatesButton.hidden
visibility: hidden
.removeAllDuplicatesButton, .removeOneDuplicateButton
padding: 0 2px
color: #a00000
.removeAllDuplicatesButton:hover, .removeOneDuplicateButton:hover
color: red
.removeAllDuplicatesButton:active, .removeOneDuplicateButton:active
color: black
> tr.hidden:nth-child(odd)
background-color: #f4f0ab
> tr.hidden:nth-child(even)
background-color: #fff6c0
> tr.hidden:hover
background-color: #ded
.editComment
color: grey
.hasComment
color: black
.actionEdit
margin-right: 6px
color: #44F
.saleEditor
.heading
font-size: 2em
font-family: verdana, arial, helvetica, sans-serif
text-transform: uppercase
font-weight: 800
margin: 6px 0 14px 0
.priceContainer
display: table
width: 100%
.price
display: table-cell
padding-right: 10px
.priceButtons
display: table-cell
width: 1.5em
.setDefaultPrice
font-size: 1.5em
padding: 6px 8px
margin-left: 8px
border-radius: 8px
.setDefaultPrice:hover
text-shadow: 0px 0px 6px #00b900
.setDefaultPrice:active
text-shadow: 0px 0px 6px grey
.insertSaleForm
.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

View File

@@ -0,0 +1,212 @@
import './SaleDuplicates.html';
import '/imports/util/selectize/selectize.js';
import swal from 'sweetalert2';
/**
* Notes:
* The Sale object has a date field which stores the date as a number in the format YYYYMMDD. Converting this number into a local date is done with moment(sale.date.toString(), "YYYYMMDD").toDate(), and converting it to a number from a date can be accomplished with ~~(moment(date).format("YYYYMMDD")), where the ~~ is a bitwise not and converts a string to a number quickly and reliably.
*/
let PREFIX = "SaleDuplicates.";
let DuplicateSales = new Meteor.Collection("duplicateSales");
let duplicateSalesSubscription;
Template.SaleDuplicates.onCreated(function() {
let template = Template.instance();
//Tracker.autorun(function() {
// let query = _.clone(Session.get(PREFIX + 'searchQuery'));
//
// duplicateSalesSubscription = template.subscribe("duplicateSales", query, Session.get(PREFIX + "showHidden"));
//});
Tracker.autorun(function() {
duplicateSalesSubscription = template.subscribe("duplicateSales", null, Session.get(PREFIX + "showHidden"));
});
});
Template.SaleDuplicates.onDestroyed(function() {
if(duplicateSalesSubscription) {
duplicateSalesSubscription.stop();
}
});
Template.SaleDuplicates.helpers({
sales: function() {
let dbQuery = [];
let query = _.clone(Session.get(PREFIX + 'searchQuery'));
if(query) {
// Add each query requirement sent by the client.
_.each(_.keys(query), function(key) {
//if(_.isObject(query[key])) dbQuery.push({[key]: query[key]});
if(_.isObject(query[key])) {
if(query[key].type === 'dateRange') {
if(query[key].start && query[key].end)
dbQuery.push({[key]: {$gte: query[key].start, $lte: query[key].end}});
else if(query[key].start)
dbQuery.push({[key]: {$gte: query[key].start}});
else if(query[key].end)
dbQuery.push({[key]: {$lte: query[key].end}});
// Do nothing if a start and/or end are not provided.
}
else {
dbQuery.push({[key]: query[key]});
}
}
else if(_.isNumber(query[key])) dbQuery.push({[key]: query[key]});
else {
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(dbQuery.length > 1) dbQuery = {$and: dbQuery};
else if(dbQuery.length == 1) dbQuery = dbQuery[0];
else dbQuery = {};
return DuplicateSales.find(dbQuery, {sort: {date: -1, productName: 1}});
},
showHidden: function() {
return Session.get(PREFIX + "showHidden") ? "checked": "";
}
});
Template.SaleDuplicates.events({
'click .duplicateScan': function(event, template) {
Meteor.call("markDuplicateSales", function(err, result) {
Meteor.log.error(err);
});
},
'change input[name="showHidden"]': function(event, template) {
//console.log("changed " + $(event.target).prop('checked'));
Session.set(PREFIX + "showHidden", $(event.target).prop('checked'));
}
});
Template.SaleDuplicate.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;
//},
formatDateAndWeek: function(date) {
return moment.utc(date.toString(), "YYYYMMDD").utc().format("MM/DD/YYYY (w)");
},
formatDateTime: function(date) {
return moment.utc(date).format("MM/DD/YYYY");
},
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;
},
duplicateClasses: function() {
return this.ignoreDuplicates ? "hidden" : "";
}
});
Template.SaleDuplicate.events({
"click .ignoreDuplicatesButton": function(event, template) {
Meteor.call('ignoreDuplicateSales', this._id, function(err, result) {
if(err) sAlert.error(err);
//else sAlert.success("Duplicates Ignored");
});
},
"click .removeAllDuplicatesButton": function(event, template) {
let _this = this;
swal({
title: "Are you sure?",
text: "This will permanently remove ALL duplicate sales.",
type: "question",
showCancelButton: true,
confirmButtonColor: "#DD6B55",
confirmButtonText: "Yes"
}).then(
function(isConfirm) {
if(isConfirm) {
Meteor.call('removeDuplicateSales', _this._id, function(err, result) {
if(err) sAlert.error(err);
//else sAlert.success("Duplicates Removed");
});
}
},
function(dismiss) {
}
);
},
"click .removeOneDuplicateButton": function(event, template) {
let _this = this;
swal({
title: "Are you sure?",
text: "This will permanently remove ONE duplicate sale.",
type: "question",
showCancelButton: true,
confirmButtonColor: "#DD6B55",
confirmButtonText: "Yes"
}).then(
function(isConfirm) {
if(isConfirm) {
Meteor.call('removeDuplicateSales', _this._id, true, function(err, result) {
if(err) sAlert.error(err);
//else sAlert.success("Duplicates Removed");
});
}
},
function(dismiss) {
}
);
}
});
Template.SaleDuplicateSearch.helpers({
searchValue: function() {
let searchFields = Session.get(PREFIX + 'searchFields');
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
}
});
Template.SaleDuplicateSearch.events({
"keyup .searchInput": _.throttle(function(event, template) {
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
let searchFields = Session.get(PREFIX + 'searchFields') || {};
let searchValue = template.$(event.target).val();
if(searchValue) {
if(this.number) searchValue = parseFloat(searchValue);
// A collection name will be provided if there is a related table of data that will contain the text provided and will map to an ID that is then searched for in the current table of data.
// For example we are displaying a table of Sales which has the ID of a Product. The Product table has a Name field and the search box searches for Product Names. The ID's of the Products found should be used to filter the Sales by Product ID.
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)
});

View File

@@ -1,18 +1,23 @@
<template name="Sales">
<div id="salesMain">
{{#if Template.subscriptionsReady}}
<div class="tableControls">
<select name="sortSelect">
<option value="date" selected>Sale Date</option>
<option value="createdAt">Data Entry Date</option>
</select>
<div class="pagination">
<span class="prevButton noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
<span class="nextButton noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
<div class="controls">
<div class="pageControls">
<input type="button" class="showDuplicates btn btn-info" style="margin-right: 30px" value="Duplicate Analysis"/>
</div>
<div class="tableControls">
<select name="sortSelect" class="form-control" style="width: auto; display: inline;">
<option value="date" selected>Sale Date</option>
<option value="createdAt">Data Entry Date</option>
</select>
<div class="pagination">
<span class="prevButton noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
<span class="nextButton noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
</div>
</div>
</div>
<div class="salesListRow">
<div class="salesListCell">
<div class="listRow">
<div class="listCell">
<div class="tableContainer">
<table class="table table-striped table-hover">
<thead>
@@ -21,7 +26,7 @@
<th class="product noselect nonclickable">Product <br/>{{>SaleSearch columnName='productId' collectionQueryColumnName='name' collection='Products' collectionResultColumnName='_id' width='90%'}}</th>
<th class="price noselect nonclickable">Price {{>SaleSearch columnName='price' width='90%'}}</th>
<th class="measure noselect nonclickable">Measure {{>SaleSearch columnName='measureId' collectionQueryColumnName='name' collection='Measures' collectionResultColumnName='_id' width='90%'}}</th>
<th class="saleDate noselect nonclickable">Date (Week)</th>
<th class="saleDate noselect nonclickable">Date (Week) {{>DateRangeSearch columnName='date' width='90%'}}</th>
<th class="createdDate noselect nonclickable">Created On</th>
<th class="venue noselect nonclickable">Venue {{>SaleSearch columnName='venueId' collectionQueryColumnName='name' collection='Venues' collectionResultColumnName='_id' width='90%'}}</th>
<th class="actions noselect nonclickable">Actions <span class="newSaleButton btn btn-success" title="Create Sale"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span> <i class="fa fa-commenting fa-lg showOnlyComments clickable" title="Show Commented Sales" aria-hidden="true"></i></th>
@@ -55,7 +60,7 @@
<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">{{formatDateAndWeek date}}</td>
<td class="tdLarge noselect nonclickable left">{{formatDate createdAt}}</td>
<td class="tdLarge noselect nonclickable left">{{formatDateTime createdAt}}</td>
<td class="tdLarge noselect nonclickable left">{{venueName venueId}}</td>
<td class="tdLarge noselect left"><i class="fa fa-pencil-square-o fa-lg actionEdit noselect clickable" title="Edit" aria-hidden="true"></i> <i class="fa fa-commenting fa-lg editComment noselect clickable {{commentClass}}" aria-hidden="true"></i> <i class="fa fa-times-circle fa-lg saleRemove noselect clickable" aria-hidden="true"></i></td>
</tr>
@@ -72,7 +77,7 @@
<div class="editorDiv"><label>Venue</label><input name="venue" class="form-control" type="text" required/></div>
</div>
<div class="col-6-12">
<div class="editorDiv"><label>Amount</label><input type="number" class="form-control amount" name="amount" min="0" step="0.01" data-schema-key='amount' value="{{amount}}" required></div>
<div class="editorDiv"><label>Amount</label><input type="number" class="form-control amount" name="amount" min="0" step="1" data-schema-key='amount' value="{{amount}}" required></div>
<div class="editorDiv"><label>Price</label><div class="priceContainer"><input type="number" class="form-control price" name="price" min="0" step="0.01" data-schema-key='currency' value="{{price}}" required><div class="priceButtons"><i class="fa fa-cogs setDefaultPrice noselect clickable" title="Calculate Default Price" aria-hidden="true"></i></div></div></div>
<div class="editorDiv"><label>Total</label><input type="number" class="form-control total" name="total" data-schema-key='currency' value="{{total}}" tabindex="-1" readonly></div>
</div>
@@ -87,6 +92,10 @@
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}" style="padding-right: 10px; width: {{width}}"/>
</template>
<template name="DateRangeSearch">
<div style="padding-right: 10px; width: {{width}};"><input type="date" class="searchDateStartInput" value="{{startDate}}" data-schema-key='date' required> - <input type="date" class="searchDateEndInput" value="{{endDate}}" data-schema-key='date' required></div>
</template>
<template name="InsertSale">
<tr>
<td colspan="8">

View File

@@ -5,12 +5,23 @@
height: 100%
width: 100%
text-align: left
.tableControls
text-align: right
margin-right: 20px
.salesListRow
.controls
text-align: left
display: table
width: 100%
.pageControls
padding: 4px 8px
margin: 4px 8px
display: table-cell
width: 240px
.tableControls
text-align: right
padding: 4px 8px
margin: 4px 12px 4px 8px
display: table-cell
.listRow
display: table-row
.salesListCell
.listCell
display: table-cell
position: relative
height: 100%
@@ -70,7 +81,7 @@
display: inline-block
.fa-times-circle
display: none
.newSaleButton.active
.newSaleButton:active
background-color: #fb557b
color: black
.fa-times-circle

View File

@@ -3,30 +3,41 @@ import './Sales.html';
import '/imports/util/selectize/selectize.js';
import swal from 'sweetalert2';
/**
* Notes:
* The Sale object has a date field which stores the date as a number in the format YYYYMMDD. Converting this number into a local date is done with moment(sale.date.toString(), "YYYYMMDD").toDate(), and converting it to a number from a date can be accomplished with ~~(moment(date).format("YYYYMMDD")), where the ~~ is a bitwise not and converts a string to a number quickly and reliably.
*/
let QUERY_LIMIT = 20;
let PREFIX = "Sales.";
Meteor.subscribe("products");
Session.set(PREFIX + "sortOption", "date");
Session.set(PREFIX + "showOnlyComments", false);
Tracker.autorun(function() {
let sortOption = Session.get(PREFIX + "sortOption");
let sort = sortOption == 'createdAt' ? {createdAt: -1} : {date: -1, createdAt: -1};
let showOnlyComments = Session.get(PREFIX + "showOnlyComments");
let query = _.clone(Session.get(PREFIX + 'searchQuery'));
if(showOnlyComments) {
if(!query) query = {};
query.comment = {$exists: true};
}
Meteor.subscribe("sales", query, sort, QUERY_LIMIT, Session.get(PREFIX + 'skipCount'));
Session.set(PREFIX + 'saleCount', Meteor.call('getSalesCount', Session.get(PREFIX + 'searchQuery')));
});
Template.Sales.onCreated(function() {
Session.set(PREFIX + "displayNewSale", false);
Meteor.subscribe("products");
Session.set(PREFIX + "sortOption", "date");
Session.set(PREFIX + "showOnlyComments", false);
Tracker.autorun(function() {
let sortOption = Session.get(PREFIX + "sortOption");
let sort = sortOption == 'createdAt' ? {createdAt: -1} : {date: -1, createdAt: -1};
let showOnlyComments = Session.get(PREFIX + "showOnlyComments");
let query = _.clone(Session.get(PREFIX + 'searchQuery'));
if(showOnlyComments) {
if(!query) query = {};
query.comment = {$exists: true};
}
//if(Template.Sales.salesSubscription) Template.Sales.salesSubscription.stop();
Template.Sales.salesSubscription = Meteor.subscribe("sales", query, sort, QUERY_LIMIT, Session.get(PREFIX + 'skipCount'));
Session.set(PREFIX + 'saleCount', Meteor.call('getSalesCount', Session.get(PREFIX + 'searchQuery')));
});
});
Template.Sales.onDestroyed(function() {
if(Template.Sales.salesSubscription) {
Template.Sales.salesSubscription.stop();
}
});
Template.Sales.helpers({
displayNewSale: function() {
@@ -77,6 +88,9 @@ Template.Sales.events({
Session.set(PREFIX + "showOnlyComments", !$button.hasClass('on'));
$button.toggleClass('on');
},
'click .showDuplicates': function(event, template) {
FlowRouter.go('SaleDuplicates');
}
});
@@ -93,10 +107,10 @@ Template.Sale.helpers({
return Meteor.collections.Products.findOne({_id: id}, {fields: {name: 1}}).name;
},
formatDateAndWeek: function(date) {
return moment(date).format("MM/DD/YYYY (w)");
return moment.utc(date.toString(), "YYYYMMDD").utc().format("MM/DD/YYYY (w)");
},
formatDate: function(date) {
return moment(date).format("MM/DD/YYYY");
formatDateTime: function(date) {
return moment.utc(date).format("MM/DD/YYYY");
},
formatPrice: function(price) {
return price.toLocaleString("en-US", {style: 'currency', currency: 'USD', minimumFractionDigits: 2});
@@ -162,10 +176,8 @@ Template.Sale.events({
});
Template.SaleEditor.onCreated(function() {
let _this = this;
this.product = Meteor.collections.Products.findOne({_id: this.data.productId});
this.selectedDate = new ReactiveVar(this.data.date);
this.selectedDate = new ReactiveVar(moment(this.data.date.toString(), "YYYYMMDD").toDate());
this.selectedVenue = new ReactiveVar(Meteor.collections.Venues.findOne({_id: this.data.venueId}));
this.price = new ReactiveVar(this.data.price);
this.amount = new ReactiveVar(this.data.amount);
@@ -207,7 +219,7 @@ Template.SaleEditor.events({
//If this product has pricing data for the given measure, then either use the price, or the previousPrice (if there is one and the effectiveDate is after the sale date).
if(priceData) {
if(priceData.effectiveDate && date && moment(priceData.effectiveDate).isAfter(date))
if(priceData.effectiveDate && date && moment.utc(priceData.effectiveDate.toString(), "YYYYMMDD").isAfter(date))
price = priceData.previousPrice;
else
price = priceData.price
@@ -231,7 +243,7 @@ Template.SaleEditor.events({
template.$('form[name="editSaleForm"]').data('bs.validator').validate(function(isValid) {
if(isValid) {
let id = template.data._id;
let date = template.selectedDate.get();
let date = ~~(moment(template.selectedDate.get()).format("YYYYMMDD")); // Note: The ~~ is a bitwise not that is a fast method of converting a string to a number.
let venue = template.selectedVenue.get();
let price = template.price.get();
let amount = template.amount.get();
@@ -245,36 +257,6 @@ Template.SaleEditor.events({
});
}
});
//let name = template.$("input[name='name']").val().trim();
//let tags = template.$(".productTagsEditor").select2('data');
//let aliases = template.$(".productAliasesEditor").select2('data');
//let measures = template.$(".productMeasuresEditor").select2('data');
//
//tags = tags.map((n)=>n.id);
//aliases = aliases.map((n)=>n.id);
//measures = measures.map((n)=>n.id);
//
//if(Session.get(PREFIX + 'displayNewProduct')) {
// Meteor.call("createProduct", name, tags, aliases, measures, function(error, result) {
// if(error) sAlert.error(error);
// else {
// sAlert.success("Product created.");
// Session.set(PREFIX + 'displayNewProduct', false);
// template.parentTemplate().$('.newProductButton').removeClass('active');
// }
// });
//}
//else {
// Meteor.call("updateProduct", this._id, name, tags, aliases, measures, function(error, result) {
// if(error) sAlert.error(error);
// else {
// sAlert.success("Product updated.");
// Session.set(PREFIX + "editedProduct", undefined);
// template.parentTemplate().$('.newProductButton').removeClass('active');
// }
// });
//}
}
});
@@ -289,11 +271,13 @@ Template.SaleSearch.events({
"keyup .searchInput": _.throttle(function(event, template) {
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
let searchFields = Session.get(PREFIX + 'searchFields') || {};
let searchValue = template.$('.searchInput').val();
let searchValue = template.$(event.target).val();
if(searchValue) {
if(this.number) searchValue = parseFloat(searchValue);
// A collection name will be provided if there is a related table of data that will contain the text provided and will map to an ID that is then searched for in the current table of data.
// For example we are displaying a table of Sales which has the ID of a Product. The Product table has a Name field and the search box searches for Product Names. The ID's of the Products found should be used to filter the Sales by Product ID.
if(this.collection) {
let ids = Meteor.collections[this.collection].find({[this.collectionQueryColumnName]: {$regex: searchValue, $options: 'i'}}, {fields: {[this.collectionResultColumnName]: 1}}).fetch();
@@ -313,11 +297,70 @@ 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)
});
Template.DateRangeSearch.helpers({
startDate: function() {
let searchFields = Session.get(PREFIX + 'searchFields');
let searchValue = (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : {};
return searchValue.start ? moment(searchValue.start.toString(), "YYYYMMDD").format("MM/DD/YYYY") : "";
},
endDate: function() {
let searchFields = Session.get(PREFIX + 'searchFields');
let searchValue = (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : {};
return searchValue.end ? moment(searchValue.end.toString(), "YYYYMMDD").format("MM/DD/YYYY") : "";
}
});
Template.DateRangeSearch.events({
"change .searchDateStartInput": function(event, template) {Template.DateRangeSearch.dateChanged(true, event, template)},
"keyup .searchDateStartInput": _.throttle(function(event, template) {Template.DateRangeSearch.dateChanged(true, event, template)}, 500),
"change .searchDateEndInput": function(event, template) {Template.DateRangeSearch.dateChanged(false, event, template)},
"keyup .searchDateEndInput": _.throttle(function(event, template) {Template.DateRangeSearch.dateChanged(false, event, template)}, 500)
});
Template.DateRangeSearch.dateChanged = function(isStart, event, template) {
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
let searchFields = Session.get(PREFIX + 'searchFields') || {};
let searchValue = template.$(event.target).val();
let columnName = template.data.columnName;
if(searchValue) {
let search = searchQuery[columnName];
// Create a search object and attach it to the searchFields and searchQuery objects if needed.
if(!search) {
search = {type: 'dateRange'};
searchFields[columnName] = searchQuery[columnName] = search;
}
// Use moment to parse date and convert it to YYYYMMDD for searching the database.
searchValue = ~~(moment(searchValue, searchValue.includes("-") ? "YYYY-MM-DD" : "MM/DD/YYYY").format("YYYYMMDD")); // Note: ~~ performs a bitwise not which is a fast method of converting a string to a number.
// Save the search ending date.
isStart ? search.start = searchValue : search.end = searchValue;
}
else {
if(searchQuery[columnName]) {
// Remove columns from the search query whose values are empty so we don't bother the database with them.
if(isStart) {
delete searchQuery[columnName].start;
delete searchFields[columnName].start;
}
else {
delete searchQuery[columnName].end;
delete searchFields[columnName].end;
}
}
}
Session.set(PREFIX + 'searchQuery', searchQuery);
Session.set(PREFIX + 'searchFields', searchFields);
Session.set(PREFIX + 'skipCount', 0); //Reset the paging of the results.
};
Template.InsertSale.onCreated(function() {
this.selectedDate = new ReactiveVar();
this.selectedProduct = new ReactiveVar();
@@ -351,7 +394,7 @@ Template.InsertSale.events({
let insertSaleMeasures = template.$(".insertSaleMeasure");
let sale = {
date: moment(template.find("[name='date']").value, "YYYY-MM-DD").toDate(),
date: ~~(moment(template.find("[name='date']").value, "YYYY-MM-DD").format("YYYYMMDD")), // Note: ~~ performs a bitwise not which is a fast method of converting a string to a number.
productId: template.selectedProduct.get()._id,
venueId: template.selectedVenue.get()._id
};
@@ -381,6 +424,7 @@ Template.InsertSale.events({
if(error) sAlert.error("Failed to insert the sale!\n" + error);
else {
sAlert.success("Sale Created");
nextMeasure.find(".amount").val(0);
//Clear the measure quantity fields so the user can enter another sale without the quantities already set.
for(let next = 0; next < insertSaleMeasures.length; next++) {

View File

@@ -1,37 +1,44 @@
<template name="Venues">
<div id="venues">
<div class="tableControls">
<span class="controlLabel">Show Hidden</span>
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
<label>
<input type="checkbox" name="showHidden"><span></span>
</label>
{{#if Template.subscriptionsReady}}
<div class="tableControls">
<span class="controlLabel">Show Hidden</span>
<div class="toggleShowHidden checkbox checkbox-slider--b-flat">
<label>
<input type="checkbox" name="showHidden"><span></span>
</label>
</div>
<span class="pagination">
<span class="prevVenues noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
<span class="nextVenues noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
</span>
</div>
<span class="pagination">
<span class="prevVenues noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
<span class="nextVenues noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
</span>
</div>
<div class="tableContainer">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="name">Name {{>VenueSearch columnName='name'}}</th>
<th class="type">Type {{>VenueSearch columnName='type'}}</th>
<th class="actions">Actions <span class="newVenueButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
</tr>
<!--<button type="button" name="newVenueButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
</thead>
<tbody>
{{#if displayNewVenue}}
{{> VenueEditor isNew=true}}
{{/if}}
{{#each venues}}
{{> Venue}}
{{/each}}
</tbody>
</table>
</div>
<div class="listRow">
<div class="listCell">
<div class="tableContainer">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="name">Name {{>VenueSearch columnName='name'}}</th>
<th class="type">Type {{>VenueSearch columnName='type'}}</th>
<th class="actions">Actions <span class="newVenueButton btn btn-success"><i class="fa fa-plus-circle" aria-hidden="true"></i><i class="fa fa-times-circle" aria-hidden="true"></i></span></th>
</tr>
<!--<button type="button" name="newVenueButton"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>-->
</thead>
<tbody>
{{#if displayNewVenue}}
{{> VenueEditor isNew=true}}
{{/if}}
{{#each venues}}
{{> Venue}}
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
{{else}}
{{/if}}
</div>
</template>

View File

@@ -1,6 +1,9 @@
#venues
margin: 20px 20px
display: table
content-box: border-box
padding: 10px 20px
height: 100%
width: 100%
text-align: left
.tableControls
@@ -18,77 +21,89 @@
top: -4px
display: inline-block
.tableContainer
width: 100%
margin-bottom: 20px
border: 0
font-size: 12.5px
table
table-layout: fixed
.listRow
display: table-row
.listCell
display: table-cell
position: relative
height: 100%
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
.tableContainer
position: absolute
top: 0
bottom: 0
left: 0
right: 0
width: auto
height: auto
border: 0
font-size: 12.5px
overflow-y: auto
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

View File

@@ -17,6 +17,11 @@
User Management
</a>
</li>
<li class="{{isActiveRoute 'MiscManagement'}}">
<a href="{{pathFor 'MiscManagement'}}">
Misc Management
</a>
</li>
{{/if}}
<li class="{{isActiveRoute 'Sales'}}">
<a href="{{pathFor 'Sales'}}">