Added a lot of functionality; Fixed a large number of bugs; Removed Bootstrap from the mix and replaced it with SimpleGrid and some choice bits from the bootstrap system; Pricing, Sales, and Product management all now function at basic levels.
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
<template name="Intro">
|
||||
<div id="intro">Intro</div>
|
||||
</template>
|
||||
4
imports/ui/Intro.import.styl
vendored
4
imports/ui/Intro.import.styl
vendored
@@ -1,4 +0,0 @@
|
||||
#intro
|
||||
text-align: center
|
||||
font-size: 4em
|
||||
font-family: sans-serif
|
||||
@@ -1,2 +0,0 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import './Intro.html';
|
||||
@@ -1,20 +0,0 @@
|
||||
<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
56
imports/ui/Menu.import.styl
vendored
@@ -1,56 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import './Menu.html';
|
||||
@@ -1,23 +1,7 @@
|
||||
<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">
|
||||
<div class="measureGroup" style="vertical-align: bottom">
|
||||
<label class='controlLabel'>Selected Measure: </label>
|
||||
<select name="measures">
|
||||
{{#each measures}}
|
||||
@@ -25,30 +9,54 @@
|
||||
{{/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="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>
|
||||
<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 class="clickable noselect {{rowClass}}">
|
||||
<td class="tdLarge noselect nonclickable left">{{name}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{currentPrice}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{priceChangeDate}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{previousPrice}}</td>
|
||||
</tr>
|
||||
</template>
|
||||
98
imports/ui/Pricing.import.styl
vendored
98
imports/ui/Pricing.import.styl
vendored
@@ -1,11 +1,29 @@
|
||||
#pricing
|
||||
margin: 10px 20px
|
||||
margin: 20px 20px
|
||||
height: 100%
|
||||
text-align: left
|
||||
|
||||
.controls
|
||||
text-align: left
|
||||
display: table
|
||||
width: 100%
|
||||
|
||||
.measureGroup
|
||||
padding: 4px 8px
|
||||
margin: 4px 8px
|
||||
display: table-cell
|
||||
width: 240px
|
||||
select
|
||||
width: 100%
|
||||
.controlGroup
|
||||
padding: 4px 8px
|
||||
margin: 4px 8px
|
||||
display: table-cell
|
||||
.pagination
|
||||
display: table-cell
|
||||
width: 240px
|
||||
vertical-align: bottom
|
||||
.previousSettings
|
||||
padding: 4px 8px
|
||||
margin: 4px 8px
|
||||
display: inline-block
|
||||
@@ -17,8 +35,9 @@
|
||||
.floatRight
|
||||
float: right
|
||||
.controlLabel
|
||||
font-size: 1.5em
|
||||
font-size: .9em
|
||||
font-weight: 700
|
||||
margin-top: -2px
|
||||
select[name="measures"]
|
||||
padding: 4px 8px
|
||||
font-size: 1.5em
|
||||
@@ -30,66 +49,37 @@
|
||||
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
|
||||
.resetButton
|
||||
margin-left: 20px
|
||||
|
||||
.tableContainer
|
||||
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%)
|
||||
font-size: 12.5px
|
||||
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
|
||||
@@ -1,13 +1,26 @@
|
||||
|
||||
import './Pricing.html';
|
||||
|
||||
let QUERY_LIMIT = 20;
|
||||
let PREFIX = "Pricing.";
|
||||
|
||||
Meteor.subscribe("products");
|
||||
let measuresSubscription = Meteor.subscribe("measures");
|
||||
Tracker.autorun(function() {
|
||||
Meteor.subscribe("products");
|
||||
Meteor.subscribe("measures");
|
||||
let ready = measuresSubscription.ready();
|
||||
|
||||
if(ready) {
|
||||
console.log("setting selected measure session var");
|
||||
let firstMeasure = Meteor.collections.Measures.findOne({}, {sort: {order: 1}, fields: {_id: 1}});
|
||||
Session.set(PREFIX + "selectedMeasure", firstMeasure._id);
|
||||
}
|
||||
});
|
||||
|
||||
Template.Pricing.onCreated(function() {
|
||||
});
|
||||
Template.Pricing.onRendered(function() {
|
||||
this.$('input[name="date"]').val(new Date().toDateInputValue());
|
||||
// this>$('select[name="measures"]').val()
|
||||
});
|
||||
Template.Pricing.helpers({
|
||||
measures: function() {
|
||||
@@ -22,66 +35,95 @@ Template.Pricing.helpers({
|
||||
return measures;
|
||||
},
|
||||
product: function() {
|
||||
let measureId = Session.get("selectedMeasure");
|
||||
let skipCount = Session.get(PREFIX + 'skipCount') || 0;
|
||||
let measureId = Session.get(PREFIX + "selectedMeasure");
|
||||
let dbQuery = {measures: {$all: [measureId]}, $or: [{hidden: false}, {hidden: {$exists:false}}]};
|
||||
|
||||
return Meteor.collections.Products.find({measures: {$all: [measureId]}}, {sort: {name: 1}});
|
||||
Session.set(PREFIX + 'productCount', Meteor.collections.Products.find(dbQuery).count()); //Always get a full count.
|
||||
return Meteor.collections.Products.find(dbQuery, {limit: QUERY_LIMIT, skip: skipCount, sort: {name: 1}});
|
||||
},
|
||||
disablePrev: function() {
|
||||
return (Session.get(PREFIX + 'skipCount') || 0) == 0;
|
||||
},
|
||||
disableNext: function() {
|
||||
return Session.get(PREFIX + 'productCount') - (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT <= 0;
|
||||
}
|
||||
});
|
||||
Template.Pricing.events({
|
||||
'change select[name="measures"]': function(event, template) {
|
||||
Session.set("selectedMeasure", $(event.target).val());
|
||||
Session.get(PREFIX + 'skipCount', 0);
|
||||
Session.set(PREFIX + "selectedMeasure", $(event.target).val());
|
||||
},
|
||||
'click .applyButton': function(event, template) {
|
||||
let measureId = Session.get("selectedMeasure");
|
||||
let measureId = template.$('select[name="measures"]').val();
|
||||
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.");
|
||||
}
|
||||
let productIds = [];
|
||||
|
||||
for(let i = 0; i < $selectedRows.length; i++) {
|
||||
let product = $($selectedRows[i]).data('product');
|
||||
|
||||
Meteor.call("setProductPrice", product._id, measureId, price, setPrevious, date);
|
||||
productIds.push(product._id);
|
||||
}
|
||||
|
||||
if(!price) {
|
||||
Meteor.call("clearProductPrice", productIds, measureId)
|
||||
}
|
||||
else {
|
||||
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.");
|
||||
}
|
||||
|
||||
Meteor.call("setProductPrice", productIds, measureId, price, setPrevious, date);
|
||||
}
|
||||
},
|
||||
'click .resetButton': function(event, template) {
|
||||
template.$('input.price').val(0);
|
||||
template.$('input.date').val(new Date().toDateInputValue());
|
||||
template.$('input[name="setPrevious"]').removeProp('checked');
|
||||
},
|
||||
'click .prevProducts': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', Math.max(0, (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT));
|
||||
},
|
||||
'click .nextProducts': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', (Session.get(PREFIX + 'skipCount') || 0) + QUERY_LIMIT);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 measureId = Session.get(PREFIX + "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 measureId = Session.get(PREFIX + "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 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)") : "-";
|
||||
},
|
||||
rowClass: function() {
|
||||
return this.deactivated ? "deactivated" : "";
|
||||
}
|
||||
});
|
||||
Template.PricingForProduct.events({
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
<template name="ProductTags">
|
||||
<div id="productTags">
|
||||
{{#if Template.subscriptionsReady}}
|
||||
<div class="insert">
|
||||
{{>ProductTagInsert}}
|
||||
<div class="tagInfo">
|
||||
<div class="floatRight">
|
||||
<label class="switch">
|
||||
<input class="editTagsSwitchCheckbox" type="checkbox">
|
||||
<div class="slider round"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="tagControls">
|
||||
<label class='control-label'>Name</label>
|
||||
<input name="tagName" type="text" class="form-control" required>
|
||||
<input name="addTag" type="button" class="btn btn-success" value="+">
|
||||
</div>
|
||||
<div class="tableTop">
|
||||
<span class="tagDisplay">
|
||||
{{#each productTagsByUse}}
|
||||
{{> ProductTag}}
|
||||
{{/each}}
|
||||
</span>
|
||||
<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>
|
||||
<div class="grid">
|
||||
<table class="dataTable table table-striped table-hover">
|
||||
<div class="tableContainer">
|
||||
<table class="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>
|
||||
<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 productTags}}
|
||||
{{> ProductTag}}
|
||||
{{#each products}}
|
||||
{{> ProductTag_Product}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -29,38 +47,18 @@
|
||||
</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> / <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> / <i class="tagEdit fa fa-pencil-square-o fa-lg noselect clickable" aria-hidden="true"></i></td>
|
||||
{{/if}}
|
||||
<span class="productTagExp" style="{{tagcolor}}"><span class="productTagName">{{name}}</span><input class="productTagEditor" style="display: none" type="text" value={{name}}></span>
|
||||
</template>
|
||||
|
||||
<template name="ProductTag_Product">
|
||||
<tr class="{{rowClass}}">
|
||||
<td class="tdLarge noselect nonclickable left">{{name}}</td>
|
||||
<td class="tdLarge noselect nonclickable left">{{tags}}</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template name="ProductTagSearch">
|
||||
<template name="ProductTag_ProductSearch">
|
||||
<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>
|
||||
172
imports/ui/ProductTags.import.styl
vendored
172
imports/ui/ProductTags.import.styl
vendored
@@ -1,67 +1,131 @@
|
||||
#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
|
||||
.tagInfo
|
||||
display: block
|
||||
margin-bottom: 20px
|
||||
|
||||
.insert
|
||||
flex: none
|
||||
.tagControls
|
||||
display: block
|
||||
width: 100%
|
||||
label
|
||||
display: inline-block
|
||||
input[name="tagName"]
|
||||
width: 140px;
|
||||
display: inline-block
|
||||
input[name="addTag"]
|
||||
margin-top: -4px
|
||||
.floatRight
|
||||
float: right
|
||||
.editTags
|
||||
font-size: 2em
|
||||
border: 1px solid #999
|
||||
background: #f7f3a0
|
||||
border-radius: 4px
|
||||
padding: 2px 3px
|
||||
.switch
|
||||
position: relative
|
||||
display: inline-block
|
||||
width: 60px
|
||||
height: 34px
|
||||
/* Hide default HTML checkbox */
|
||||
input
|
||||
display:none
|
||||
/* The slider */
|
||||
.slider
|
||||
position: absolute
|
||||
cursor: pointer
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
background-color: #ccc
|
||||
-webkit-transition: .4s
|
||||
transition: .4s
|
||||
.slider:before
|
||||
position: absolute
|
||||
font-family: FontAwesome
|
||||
padding-left: 4px
|
||||
font-size: 20px
|
||||
line-height: 26px
|
||||
color: #ffa398
|
||||
content: "\f040"
|
||||
height: 26px
|
||||
width: 26px
|
||||
left: 4px
|
||||
bottom: 4px
|
||||
background-color: white
|
||||
-webkit-transition: .4s
|
||||
transition: .4s
|
||||
input:checked + .slider
|
||||
background-color: #5cb85c
|
||||
input:focus + .slider
|
||||
box-shadow: 0 0 1px #2196F3
|
||||
input:checked + .slider:before
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
/* Rounded sliders */
|
||||
.slider.round
|
||||
border-radius: 34px
|
||||
.slider.round:before
|
||||
border-radius: 50%
|
||||
.tableTop
|
||||
display: table
|
||||
width: 100%
|
||||
margin-top: 10px
|
||||
.tagDisplay
|
||||
display: table-cell
|
||||
width: auto
|
||||
.productTagExp
|
||||
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif
|
||||
font-size: 14px
|
||||
line-height: 34px
|
||||
margin: 0
|
||||
padding: 4px 14px
|
||||
border: 1px solid #999
|
||||
border-radius: 4px
|
||||
white-space: nowrap
|
||||
cursor: pointer
|
||||
.hidden
|
||||
display: none;
|
||||
.productTagEditor
|
||||
display: inline-block
|
||||
font-size: 1em
|
||||
height: 20px
|
||||
background: transparent
|
||||
border: 0
|
||||
padding: 0
|
||||
margin: 0
|
||||
.productTagName
|
||||
border: 0
|
||||
padding: 0
|
||||
margin: 0
|
||||
.pagination
|
||||
display: table-cell
|
||||
width: 240px
|
||||
vertical-align: bottom;
|
||||
.tableContainer
|
||||
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
|
||||
font-size: 12.5px
|
||||
table
|
||||
table-layout: fixed
|
||||
width: auto
|
||||
|
||||
.tdLarge
|
||||
font-size: 1.5em
|
||||
.tagRemove
|
||||
color: red
|
||||
.tagEdit
|
||||
color: darkblue
|
||||
.editorApply
|
||||
color: green
|
||||
.editorCancel
|
||||
color: red
|
||||
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
|
||||
|
||||
@@ -1,86 +1,228 @@
|
||||
|
||||
import './ProductTags.html';
|
||||
|
||||
let QUERY_LIMIT = 20;
|
||||
let PREFIX = "ProductTags.";
|
||||
|
||||
Tracker.autorun(function() {
|
||||
Meteor.subscribe("products");
|
||||
Meteor.subscribe("productTags");
|
||||
});
|
||||
|
||||
Template.ProductTags.onCreated(function() {
|
||||
Session.set(PREFIX + "editTags", false);
|
||||
});
|
||||
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);
|
||||
return Meteor.collections.ProductTags.find(Session.get(PREFIX + 'searchQuery') || {}, {sort: {name: 1}});
|
||||
},
|
||||
"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();
|
||||
productTagsByUse: function() {
|
||||
let productTags = Meteor.collections.ProductTags.find({}, {sort: {name: 1}}).fetch();
|
||||
let totalCount = Meteor.collections.Products.find({}).count();
|
||||
|
||||
//Basic validation.
|
||||
if(name) {
|
||||
Meteor.call("updateProductTag", {_id: this._id, name: name}, function(error, result) {
|
||||
if(error) {
|
||||
sAlert.error(error);
|
||||
}
|
||||
for(let tag of productTags) {
|
||||
tag.useRatio = totalCount > 0 ? Meteor.collections.Products.find({tags: {$all: [tag._id]}}).count() / totalCount : 0;
|
||||
}
|
||||
|
||||
//Turned off sorting by use count since it caused re-ordering problems when applying tags to products.
|
||||
//productTags.sort(function(a, b) {
|
||||
// return b.useRatio - a.useRatio;
|
||||
//});
|
||||
|
||||
return productTags;
|
||||
},
|
||||
products: function() {
|
||||
let skipCount = Session.get(PREFIX + 'skipCount') || 0;
|
||||
let query = Session.get(PREFIX + 'searchQuery');
|
||||
let dbQuery = [];
|
||||
|
||||
if(query) {
|
||||
_.each(_.keys(query), function(key) {
|
||||
if(_.isFunction(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key]();
|
||||
else if(_.isObject(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key]; //Will look something like: {$in: [xxx,xxx,xxx]}
|
||||
else if(_.isNumber(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key];
|
||||
else {
|
||||
sAlert.success("Product tag updated.");
|
||||
//dbQuery[key] = {$regex: query[key], $options: 'i'};
|
||||
let searchValue = query[key];
|
||||
let searches = searchValue && searchValue.length > 0 ? searchValue.split(/\s+/) : undefined;
|
||||
|
||||
for(let search of searches) {
|
||||
dbQuery.push({[key]: {$regex: '\\b' + search, $options: 'i'}});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
template.edited.set(undefined);
|
||||
//Don't show hidden products.
|
||||
dbQuery.push({$or: [{hidden: false}, {hidden: {$exists:false}}]});
|
||||
//Join the query pieces together with an $and
|
||||
dbQuery = dbQuery.length > 1 ? {$and: dbQuery} : dbQuery[0];
|
||||
//Collect a count of the products first, and store it in the session.
|
||||
Session.set(PREFIX + 'productCount', Meteor.collections.Products.find(dbQuery).count()); //Always get a full count.
|
||||
|
||||
return Meteor.collections.Products.find(dbQuery, {limit: QUERY_LIMIT, skip: skipCount, sort: {name: 1}});
|
||||
},
|
||||
"click .role": function(event, template) {
|
||||
$(event.target).toggleClass("selected");
|
||||
disablePrev: function() {
|
||||
return (Session.get(PREFIX + 'skipCount') || 0) == 0;
|
||||
},
|
||||
disableNext: function() {
|
||||
return Session.get(PREFIX + 'productCount') - (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT <= 0;
|
||||
}
|
||||
});
|
||||
Template.ProductTag.helpers({
|
||||
editing: function() {
|
||||
return Template.instance().edited.get() == this;
|
||||
Template.ProductTags.events({
|
||||
'change .editTagsSwitchCheckbox': function(event, template) {
|
||||
Session.set(PREFIX + "editTags", $(event.target).is(":checked"));
|
||||
},
|
||||
'click input[name="addTag"]': function(event, template) {
|
||||
let $tagName = template.$('input[name="tagName"]');
|
||||
let name = $tagName.val();
|
||||
|
||||
Meteor.call('insertProductTag', name, function(error) {
|
||||
if(error) sAlert.error(error);
|
||||
else {
|
||||
sAlert.success("Tag Created");
|
||||
$tagName.val("");
|
||||
$tagName.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
'click .prevProducts': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', Math.max(0, (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT));
|
||||
},
|
||||
'click .nextProducts': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', (Session.get(PREFIX + 'skipCount') || 0) + QUERY_LIMIT);
|
||||
}
|
||||
});
|
||||
|
||||
Template.ProductTagSearch.events({
|
||||
Template.ProductTag.onCreated(function() {
|
||||
//this.showEditorVar = new ReactiveVar(false);
|
||||
});
|
||||
Template.ProductTag.helpers({
|
||||
// tagsize: function() {
|
||||
// return ((2 * this.useRatio) + 1) + "em";
|
||||
// },
|
||||
// showEditor: function() {
|
||||
// return Template.instance().showEditorVar.get();
|
||||
// },
|
||||
tagcolor: function() {
|
||||
let color = "rgba(" + (Math.round(155 * this.useRatio) + 100) + ",80," + (Math.round(155 * (1-this.useRatio)) + 100) + ", 1)";
|
||||
|
||||
return "-webkit-box-shadow: inset 0px 0px 6px 1px " + color + "; -moz-box-shadow: inset 0px 0px 6px 1px " + color + "; box-shadow: inset 0px 0px 6px 1px " + color + ";";
|
||||
}
|
||||
});
|
||||
Template.ProductTag.events({
|
||||
'click .productTagExp': function(event, template) {
|
||||
if(Session.get(PREFIX + "editTags")) {
|
||||
let width = template.$('.productTagName').innerWidth();
|
||||
template.$('.productTagEditor').css('width', width + "px");
|
||||
|
||||
template.$('.productTagName').hide();
|
||||
template.$('.productTagEditor').show();
|
||||
template.$('.productTagEditor').focus();
|
||||
}
|
||||
else {
|
||||
let tagId = this._id;
|
||||
let $selectedRows = template.parentTemplate(1).$('tr.selected');
|
||||
let productIds = [];
|
||||
|
||||
for(let i = 0; i < $selectedRows.length; i++) {
|
||||
let selectedRow = $selectedRows[i];
|
||||
|
||||
productIds.push($(selectedRow).data('product')._id);
|
||||
}
|
||||
|
||||
//Should either tag or un-tag the products (if any products don't have the tag then add it to all, otherwise remove it).
|
||||
Meteor.call("tagProducts", productIds, tagId);
|
||||
}
|
||||
},
|
||||
'blur .productTagEditor': function(event, template) {
|
||||
let name = template.$('.productTagEditor').val();
|
||||
|
||||
template.$('.productTagName').show();
|
||||
template.$('.productTagEditor').hide();
|
||||
|
||||
if(name && name.trim().length > 0) {
|
||||
Meteor.call('updateProductTag', {_id: this._id, name: name.trim()}, function(error) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Tag Updated");
|
||||
});
|
||||
}
|
||||
else {
|
||||
Meteor.call('deleteProductTag', this._id, function(error) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Tag Deleted");
|
||||
});
|
||||
}
|
||||
},
|
||||
'keydown .productTagEditor': function(event, template) {
|
||||
if(event.which === 13) { //Enter
|
||||
let name = template.$('.productTagEditor').val();
|
||||
|
||||
event.preventDefault();
|
||||
template.$('.productTagName').show();
|
||||
template.$('.productTagEditor').hide();
|
||||
|
||||
if(name && name.trim().length > 0) {
|
||||
Meteor.call('updateProductTag', {_id: this._id, name: name.trim()}, function(error) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Tag Updated");
|
||||
});
|
||||
}
|
||||
else {
|
||||
Meteor.call('deleteProductTag', this._id, function(error) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Tag Deleted");
|
||||
});
|
||||
}
|
||||
}
|
||||
else if(event.which === 27) { //ESC
|
||||
event.preventDefault();
|
||||
template.$('.productTagEditor').val(this.name);
|
||||
template.$('.productTagName').show();
|
||||
template.$('.productTagEditor').hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.ProductTag_ProductSearch.events({
|
||||
"keyup .searchInput": _.throttle(function(event, template) {
|
||||
let searchQuery = Session.get('searchQuery') || {};
|
||||
let searchFields = Session.get('searchFields') || {};
|
||||
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
|
||||
let searchFields = Session.get(PREFIX + 'searchFields') || {}; //searchFields allows the view to retain the search values in the inputs across page refreshes.
|
||||
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();
|
||||
let ids = [];
|
||||
//Split the search by the spaces. Each word will be a separate search which must being a word in the matches.
|
||||
let searches = searchValue && searchValue.length > 0 ? searchValue.split(/\s+/) : undefined;
|
||||
let regexs = searches ? [] : undefined;
|
||||
|
||||
if(searches) {
|
||||
for(let search of searches) {
|
||||
regexs.push(new RegExp("\\b" + search));
|
||||
}
|
||||
}
|
||||
|
||||
// let results = Meteor.collections[this.collection].find({[this.collectionQueryColumnName]: {$regex: searchValue, $options: 'i'}}, {fields: {[this.collectionResultColumnName]: 1}}).fetch();
|
||||
//
|
||||
// for(let result of results) {
|
||||
// ids.push(result._id);
|
||||
// }
|
||||
|
||||
for(let regex of regexs) {
|
||||
let results = Meteor.collections[this.collection].find({[this.collectionQueryColumnName]: {$regex: searchValue, $options: 'i'}}, {fields: {[this.collectionResultColumnName]: 1}}).fetch();
|
||||
|
||||
for(let result of results) {
|
||||
ids.push(result._id);
|
||||
}
|
||||
}
|
||||
|
||||
//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;}
|
||||
//for(let i = 0; i < ids.length; i++) {ids[i] = ids[i]._id;}
|
||||
searchQuery[this.columnName] = {$in: ids};
|
||||
searchFields[this.columnName] = searchValue;
|
||||
}
|
||||
@@ -94,39 +236,92 @@ Template.ProductTagSearch.events({
|
||||
delete searchFields[this.columnName];
|
||||
}
|
||||
|
||||
Session.set('searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchFields', searchFields);
|
||||
}, 500)
|
||||
});
|
||||
Template.ProductTagSearch.helpers({
|
||||
Template.ProductTag_ProductSearch.helpers({
|
||||
searchValue: function() {
|
||||
let searchFields = Session.get('searchFields');
|
||||
let searchFields = Session.get(PREFIX + 'searchFields');
|
||||
|
||||
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
|
||||
}
|
||||
});
|
||||
|
||||
Template.ProductTagInsert.onRendered(function() {
|
||||
this.$('form[name="insert"]').validator();
|
||||
Template.ProductTag_Product.onRendered(function() {
|
||||
this.$('tr').data("product", this.data);
|
||||
});
|
||||
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();
|
||||
Template.ProductTag_Product.helpers({
|
||||
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]);
|
||||
|
||||
Meteor.call('insertProductTag', name, function(error, result) {
|
||||
if(error) {
|
||||
sAlert.error(error);
|
||||
}
|
||||
else {
|
||||
sAlert.success("Product tag created.");
|
||||
}
|
||||
});
|
||||
if(obj && obj.name)
|
||||
tagNames.push(obj.name);
|
||||
}
|
||||
});
|
||||
|
||||
result = tagNames.join(", ");
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
"click .role": function(event, template) {
|
||||
$(event.target).toggleClass("selected");
|
||||
name: function() {
|
||||
let result = this.name;
|
||||
|
||||
if(this.aliases && this.aliases.length > 0) {
|
||||
let first = true;
|
||||
|
||||
for(let alias of this.aliases) {
|
||||
if(first) {
|
||||
result += " ['";
|
||||
first = false;
|
||||
}
|
||||
else {
|
||||
result += "', '";
|
||||
}
|
||||
|
||||
result += alias;
|
||||
}
|
||||
|
||||
result += "']";
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
rowClass: function() {
|
||||
return this.deactivated ? "deactivated" : "";
|
||||
}
|
||||
});
|
||||
Template.ProductTag_Product.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;
|
||||
}
|
||||
});
|
||||
@@ -1,38 +1,94 @@
|
||||
<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 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>
|
||||
<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>
|
||||
</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 class="{{getRowClass}}">
|
||||
{{#if editing}}
|
||||
{{> ProductEditor}}
|
||||
{{else}}
|
||||
<td class="noselect nonclickable left">{{name}}</td>
|
||||
<td class="noselect nonclickable left">{{tags}}</td>
|
||||
<td class="noselect nonclickable left">{{aliases}}</td>
|
||||
<td class="noselect nonclickable left">{{measures}}</td>
|
||||
{{#if hidden}}
|
||||
<td class="center"><i class="actionEdit fa fa-pencil-square-o fa-lg noselect clickable" title="Edit" aria-hidden="true"></i> / <i class="actionShow fa fa-eye fa-lg noselect clickable" title="Show" aria-hidden="true"></i></td>
|
||||
{{else}}
|
||||
{{#if deactivated}}
|
||||
<td class="center"><i class="actionEdit fa fa-pencil-square-o fa-lg noselect clickable" title="Edit" aria-hidden="true"></i> / <i class="actionActivate fa fa-toggle-on fa-lg noselect clickable" title="Activate" aria-hidden="true"></i> / <i class="actionHide fa fa-eye-slash fa-lg noselect clickable" title="Hide" aria-hidden="true"></i></td>
|
||||
{{else}}
|
||||
<td class="center"><i class="actionEdit fa fa-pencil-square-o fa-lg noselect clickable" title="Edit" aria-hidden="true"></i> / <i class="actionRemove fa fa-times-circle fa-lg noselect clickable" title="Deactivate" aria-hidden="true"></i></td>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template name="ProductEditor">
|
||||
<td colspan="4" class="productEditorTd">
|
||||
<div class="editorDiv"><label>Name:</label><input name="name" class="form-control" type="text" value="{{name}}" autocomplete="off" required></div>
|
||||
<div class="editorDiv"><label>Tags:</label>
|
||||
<select class="productTagsEditor" multiple="multiple">
|
||||
{{#each tags}}
|
||||
<option value="{{_id}}" {{tagSelected}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="editorDiv"><label>Aliases:</label>
|
||||
<select class="productAliasesEditor" multiple="multiple">
|
||||
{{#each aliases}}
|
||||
<option value="{{this}}" selected>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="editorDiv"><label>Measures:</label>
|
||||
<select class="productMeasuresEditor" multiple="multiple">
|
||||
{{#each measures}}
|
||||
<option value="{{_id}}" {{measureSelected}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td class="center productEditorTd"><i class="editorApply fa fa-check-square-o fa-lg noselect clickable" aria-hidden="true"></i> / <i class="editorCancel fa fa-times-circle fa-lg noselect clickable" aria-hidden="true"></i></td>
|
||||
</template>
|
||||
|
||||
<template name="ProductSearch">
|
||||
<div class="">
|
||||
<div class="productSearch">
|
||||
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}"/>
|
||||
</div>
|
||||
</template>
|
||||
142
imports/ui/Products.import.styl
vendored
142
imports/ui/Products.import.styl
vendored
@@ -1,52 +1,98 @@
|
||||
#products
|
||||
height: 100%;
|
||||
margin: 20px 20px
|
||||
height: 100%
|
||||
text-align: left
|
||||
|
||||
.editor
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
.tableControls
|
||||
text-align: right
|
||||
margin-right: 20px
|
||||
.controlLabel
|
||||
font-size: 9px
|
||||
font-weight: 700
|
||||
color: #5a5a5a
|
||||
position: relative
|
||||
top: -2px
|
||||
.toggleShowHidden
|
||||
margin: 0 40px 0 0
|
||||
position: relative
|
||||
top: -4px
|
||||
display: inline-block
|
||||
|
||||
.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;
|
||||
.tableContainer
|
||||
width: 100%
|
||||
margin-bottom: 20px
|
||||
border: 0
|
||||
font-size: 12.5px
|
||||
|
||||
.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);
|
||||
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
|
||||
@@ -1,33 +1,90 @@
|
||||
|
||||
import './Products.html';
|
||||
|
||||
let QUERY_LIMIT = 20;
|
||||
let PREFIX = "Products.";
|
||||
|
||||
Tracker.autorun(function() {
|
||||
Meteor.subscribe("products");
|
||||
Meteor.subscribe("productTags");
|
||||
Meteor.subscribe("measures");
|
||||
});
|
||||
|
||||
Template.Products.onCreated(function() {
|
||||
Session.set(PREFIX + "displayNewProduct", false);
|
||||
Session.set(PREFIX + "showHidden", false);
|
||||
});
|
||||
Template.Products.helpers({
|
||||
displayNewProduct: function() {
|
||||
return Session.get(PREFIX + "displayNewProduct");
|
||||
},
|
||||
products: function() {
|
||||
let query = Session.get('searchQuery');
|
||||
let dbQuery = {};
|
||||
let skipCount = Session.get(PREFIX + 'skipCount') || 0;
|
||||
let query = Session.get(PREFIX + 'searchQuery');
|
||||
let dbQuery = [];
|
||||
|
||||
if(query) {
|
||||
_.each(_.keys(query), function(key) {
|
||||
if(_.isFunction(query[key])) dbQuery[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'};
|
||||
if(_.isFunction(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key]();
|
||||
else if(_.isObject(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key]; //Will look something like: {$in: [xxx,xxx,xxx]}
|
||||
else if(_.isNumber(query[key])) dbQuery.push({[key]: query[key]}); //dbQuery[key] = query[key];
|
||||
else {
|
||||
//dbQuery[key] = {$regex: query[key], $options: 'i'};
|
||||
let searchValue = query[key];
|
||||
let searches = searchValue && searchValue.length > 0 ? searchValue.split(/\s+/) : undefined;
|
||||
|
||||
for(let search of searches) {
|
||||
dbQuery.push({[key]: {$regex: '\\b' + search, $options: 'i'}});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Meteor.collections.Products.find(dbQuery, {limit: 20, sort: {name: 1}});
|
||||
if(!Session.get(PREFIX + "showHidden")) {
|
||||
//Ignore any hidden elements by showing those not hidden, or those without the hidden field.
|
||||
dbQuery.push({$or: [{hidden: false}, {hidden: {$exists:false}}]});
|
||||
}
|
||||
|
||||
dbQuery = dbQuery.length > 0 ? {$and: dbQuery} : {};
|
||||
Session.set(PREFIX + 'productCount', Meteor.collections.Products.find(dbQuery).count()); //Always get a full count.
|
||||
return Meteor.collections.Products.find(dbQuery, {limit: QUERY_LIMIT, skip: skipCount, sort: {name: 1}});
|
||||
},
|
||||
disablePrev: function() {
|
||||
return (Session.get(PREFIX + 'skipCount') || 0) == 0;
|
||||
},
|
||||
disableNext: function() {
|
||||
return Session.get(PREFIX + 'productCount') - (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT <= 0;
|
||||
}
|
||||
});
|
||||
Template.Products.events({
|
||||
'click .prevProducts': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', Math.max(0, (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT));
|
||||
},
|
||||
'click .nextProducts': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', (Session.get(PREFIX + 'skipCount') || 0) + QUERY_LIMIT);
|
||||
},
|
||||
'click .newProductButton': function(event, template) {
|
||||
if(template.$('.newProductButton').hasClass('active')) {
|
||||
Session.set(PREFIX + 'displayNewProduct', false);
|
||||
}
|
||||
else {
|
||||
Session.set(PREFIX + 'displayNewProduct', true);
|
||||
Session.set(PREFIX + "editedProduct", undefined); //Clear the edited product so that only one editor is open at a time.
|
||||
}
|
||||
template.$('.newProductButton').toggleClass('active');
|
||||
},
|
||||
'change input[name="showHidden"]': function(event, template) {
|
||||
//console.log("changed " + $(event.target).prop('checked'));
|
||||
Session.set(PREFIX + "showHidden", $(event.target).prop('checked'));
|
||||
}
|
||||
});
|
||||
|
||||
Template.ProductSearch.events({
|
||||
"keyup .searchInput": _.throttle(function(event, template) {
|
||||
let searchQuery = Session.get('searchQuery') || {};
|
||||
let searchFields = Session.get('searchFields') || {};
|
||||
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
|
||||
let searchFields = Session.get(PREFIX + 'searchFields') || {};
|
||||
let searchValue = template.$('.searchInput').val();
|
||||
|
||||
if(searchValue) {
|
||||
@@ -51,13 +108,13 @@ Template.ProductSearch.events({
|
||||
delete searchFields[this.columnName];
|
||||
}
|
||||
|
||||
Session.set('searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchFields', searchFields);
|
||||
}, 500)
|
||||
});
|
||||
|
||||
Template.ProductSearch.helpers({
|
||||
searchValue: function() {
|
||||
let searchFields = Session.get('searchFields');
|
||||
let searchFields = Session.get(PREFIX + 'searchFields');
|
||||
|
||||
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
|
||||
}
|
||||
@@ -82,6 +139,9 @@ Template.Product.helpers({
|
||||
|
||||
return result;
|
||||
},
|
||||
aliases: function() {
|
||||
return this.aliases.join(', ');
|
||||
},
|
||||
tags: function() {
|
||||
let result = "";
|
||||
|
||||
@@ -99,5 +159,112 @@ Template.Product.helpers({
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
editing: function() {
|
||||
let editedProduct = Session.get(PREFIX + "editedProduct");
|
||||
|
||||
return editedProduct == this._id;
|
||||
},
|
||||
getRowClass: function() {
|
||||
return this.hidden ? "hidden" : this.deactivated ? "deactivated" : "";
|
||||
}
|
||||
});
|
||||
Template.Product.events({
|
||||
"click .actionEdit": function(event, template) {
|
||||
Session.set(PREFIX + "editedProduct", this._id);
|
||||
Session.set(PREFIX + 'displayNewProduct', false); //Ensure the new product editor is closed.
|
||||
template.$('.newProductButton').removeClass('active');
|
||||
},
|
||||
"click .actionRemove": function(event, template) {
|
||||
Meteor.call('deactivateProduct', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Product Deactivated");
|
||||
});
|
||||
},
|
||||
'click .actionActivate': function(event, template) {
|
||||
Meteor.call('reactivateProduct', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Product Reactivated");
|
||||
});
|
||||
},
|
||||
"click .actionShow": function(event, template) {
|
||||
Meteor.call('showProduct', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Product Visibility Enabled");
|
||||
});
|
||||
},
|
||||
'click .actionHide': function(event, template) {
|
||||
Meteor.call('hideProduct', this._id, function(error, result) {
|
||||
if(error) sAlert.error(error);
|
||||
else sAlert.success("Product Visibility Disabled");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Template.ProductEditor.onRendered(function() {
|
||||
this.$(".productTagsEditor").select2();
|
||||
this.$(".productAliasesEditor").select2({tags: true, tokenSeparators: [';', '.']});
|
||||
this.$(".productMeasuresEditor").select2();
|
||||
});
|
||||
Template.ProductEditor.helpers({
|
||||
measures: function() {
|
||||
return Meteor.collections.Measures.find({});
|
||||
},
|
||||
measureSelected: function() {
|
||||
let measure = this;
|
||||
let product = Template.parentData();
|
||||
|
||||
return product.measures && product.measures.includes(measure._id) ? "selected" : "";
|
||||
},
|
||||
aliases: function() {
|
||||
return this.aliases;
|
||||
},
|
||||
tags: function() {
|
||||
return Meteor.collections.ProductTags.find({});
|
||||
},
|
||||
tagSelected: function() {
|
||||
let tag = this;
|
||||
let product = Template.parentData();
|
||||
|
||||
return product.tags && product.tags.includes(tag._id) ? "selected" : "";
|
||||
}
|
||||
});
|
||||
Template.ProductEditor.events({
|
||||
"click .editorCancel": function(event, template) {
|
||||
Session.set(PREFIX + "editedProduct", undefined);
|
||||
Session.set(PREFIX + 'displayNewProduct', false);
|
||||
template.$('.newProductButton').removeClass('active');
|
||||
},
|
||||
"click .editorApply": function(event, template) {
|
||||
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.$('.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
@@ -5,16 +5,16 @@
|
||||
{{>InsertSale}}
|
||||
</div>
|
||||
<div class="grid">
|
||||
<table class="dataTable table table-striped table-hover">
|
||||
<table class="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>
|
||||
<th class="amount noselect nonclickable">Amount</th>
|
||||
<th class="product noselect nonclickable">Product</th>
|
||||
<th class="price noselect nonclickable">Price</th>
|
||||
<th class="measure noselect nonclickable">Measure</th>
|
||||
<th class="date noselect nonclickable">Date (Week)</th>
|
||||
<th class="venue noselect nonclickable">Venue</th>
|
||||
<th class="actions noselect nonclickable">Actions</th>
|
||||
</tr>
|
||||
<tr class="footers">
|
||||
<th>{{>SaleSearch columnName='amount' width='90%'}}</th>
|
||||
@@ -33,6 +33,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<span class="prevButton noselect {{#if disablePrev}}disabled{{/if}}"><i class="fa fa-long-arrow-left" aria-hidden="true"></i> Prev</span>
|
||||
<span class="nextButton noselect {{#if disableNext}}disabled{{/if}}">Next <i class="fa fa-long-arrow-right" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
{{else}}
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -40,38 +44,13 @@
|
||||
|
||||
<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}}-->
|
||||
<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>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
@@ -80,27 +59,27 @@
|
||||
</template>
|
||||
|
||||
<template name="InsertSale">
|
||||
<form id="insertSale" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<form class="insertSaleForm" autocomplete="off">
|
||||
<div class="grid">
|
||||
<div class="col-4-12">
|
||||
<div class="formGroupHeading">New Sale</div>
|
||||
<div class="form-group">
|
||||
<label for='InsertSaleDate' class='control-label'>Date</label>
|
||||
<label 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>
|
||||
<label 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>
|
||||
<label 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="col-1-1">
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-success" value="Save Sale">
|
||||
</div>
|
||||
@@ -110,7 +89,7 @@
|
||||
</template>
|
||||
|
||||
<template name="InsertSaleMeasure">
|
||||
<div class="col-md-4 col-sm-6 insertSaleMeasure">
|
||||
<div class="col-2-12 insertSaleMeasure">
|
||||
<div class="formGroupHeading">{{name}}</div>
|
||||
<input type="hidden" class="measureId" value="{{this._id}}">
|
||||
<div class="form-group">
|
||||
@@ -123,7 +102,7 @@
|
||||
</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>
|
||||
<input type="number" class="form-control total" name="total" data-schema-key='currency' value="{{total}}" tabindex="-1" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
61
imports/ui/Sales.import.styl
vendored
61
imports/ui/Sales.import.styl
vendored
@@ -1,24 +1,17 @@
|
||||
#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
|
||||
text-align: left
|
||||
|
||||
.comboList .deactivated
|
||||
color: red
|
||||
background: #ffdbd9
|
||||
|
||||
.editor
|
||||
height: 100%
|
||||
overflow-y: auto
|
||||
|
||||
.insertSale
|
||||
flex: none
|
||||
width: 100%
|
||||
|
||||
.form-group, label
|
||||
@@ -30,29 +23,41 @@
|
||||
font-style: normal
|
||||
font-variant: normal
|
||||
font-weight: 500
|
||||
|
||||
.grid
|
||||
flex: auto
|
||||
align-self: stretch
|
||||
overflow-y: auto
|
||||
overflow-x: auto
|
||||
width: 100%
|
||||
margin-bottom: 20px
|
||||
border: 0
|
||||
padding-top: 20px
|
||||
|
||||
.table > thead > tr > th
|
||||
border: 0
|
||||
padding-top: 0
|
||||
padding-bottom: 6px
|
||||
|
||||
.dataTable
|
||||
font-size: 12.5px
|
||||
label
|
||||
font-size: 10px
|
||||
font-weight: 800
|
||||
table
|
||||
table-layout: fixed
|
||||
|
||||
.tdLarge
|
||||
font-size: 1.3em
|
||||
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: 90px
|
||||
> th.date
|
||||
width: 140px
|
||||
> th.venue
|
||||
width: 160px
|
||||
> th.actions
|
||||
width: 90px
|
||||
|
||||
@@ -3,79 +3,40 @@ 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'));
|
||||
});
|
||||
let QUERY_LIMIT = 20;
|
||||
let PREFIX = "Sales.";
|
||||
|
||||
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);
|
||||
// }
|
||||
Meteor.subscribe("products");
|
||||
|
||||
Tracker.autorun(function() {
|
||||
Meteor.subscribe("sales", Session.get(PREFIX + 'searchQuery'), QUERY_LIMIT, Session.get(PREFIX + 'skipCount'));
|
||||
Session.set(PREFIX + 'saleCount', Meteor.call('getSalesCount', Session.get(PREFIX + 'searchQuery')));
|
||||
});
|
||||
|
||||
Template.Sales.helpers({
|
||||
sales: function() {
|
||||
return Meteor.collections.Sales.find({}, {sort: {date: -1}});
|
||||
return Meteor.collections.Sales.find({}, {sort: {date: -1, createdAt: -1}});
|
||||
},
|
||||
disablePrev: function() {
|
||||
return (Session.get(PREFIX + 'skipCount') || 0) == 0;
|
||||
},
|
||||
disableNext: function() {
|
||||
return Session.get(PREFIX + 'saleCount') - (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT <= 0;
|
||||
}
|
||||
});
|
||||
Template.Sales.events({
|
||||
'click .prevButton': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', Math.max(0, (Session.get(PREFIX + 'skipCount') || 0) - QUERY_LIMIT));
|
||||
},
|
||||
'click .nextButton': function(event, template) {
|
||||
if(!$(event.target).hasClass('disabled'))
|
||||
Session.set(PREFIX + 'skipCount', (Session.get(PREFIX + 'skipCount') || 0) + QUERY_LIMIT);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -99,11 +60,33 @@ Template.Sale.helpers({
|
||||
return amount > 1;
|
||||
}
|
||||
});
|
||||
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.SaleSearch.helpers({
|
||||
searchValue: function() {
|
||||
let searchFields = Session.get(PREFIX + 'searchFields');
|
||||
|
||||
return (searchFields && searchFields[this.columnName]) ? searchFields[this.columnName] : '';
|
||||
}
|
||||
});
|
||||
Template.SaleSearch.events({
|
||||
"keyup .searchInput": _.throttle(function(event, template) {
|
||||
let searchQuery = Session.get('searchQuery') || {};
|
||||
let searchFields = Session.get('searchFields') || {};
|
||||
let searchQuery = Session.get(PREFIX + 'searchQuery') || {};
|
||||
let searchFields = Session.get(PREFIX + 'searchFields') || {};
|
||||
let searchValue = template.$('.searchInput').val();
|
||||
|
||||
if(searchValue) {
|
||||
@@ -127,32 +110,31 @@ Template.SaleSearch.events({
|
||||
delete searchFields[this.columnName];
|
||||
}
|
||||
|
||||
Session.set('searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchQuery', searchQuery);
|
||||
Session.set(PREFIX + 'searchFields', searchFields);
|
||||
}, 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.selectedDate = new ReactiveVar();
|
||||
this.selectedProduct = new ReactiveVar();
|
||||
this.selectedVenue = new ReactiveVar();
|
||||
});
|
||||
Template.InsertSale.onRendered(function() {
|
||||
$('#insertSale').validator();
|
||||
this.$('.insertSaleForm').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'});
|
||||
|
||||
//TODO: Highlight deactivated products in combo
|
||||
//TODO: Default the price for each size product based on the date.
|
||||
this.$('[name="product"]').buildCombo({cursor: Meteor.collections.Products.find({$or: [{hidden: false}, {hidden: {$exists:false}}]}), selection: this.selectedProduct, textAttr: 'name', listClass: 'comboList', getClasses: function(data) {
|
||||
return (data && data.deactivated) ? "deactivated" : "";
|
||||
}});
|
||||
this.$('[name="venue"]').buildCombo({cursor: Meteor.collections.Venues.find({}), selection: this.selectedVenue, textAttr: 'name', listClass: 'comboList'});
|
||||
|
||||
// this.autorun(function(){
|
||||
@@ -160,14 +142,17 @@ Template.InsertSale.onRendered(function() {
|
||||
// }.bind(this));
|
||||
});
|
||||
Template.InsertSale.events({
|
||||
'change #InsertSaleProduct': function(event, template) {
|
||||
let selectedId = $('#InsertSaleProduct').val();
|
||||
'change input[name="product"]': function(event, template) {
|
||||
let selectedId = template.$('input[name="product"]').val();
|
||||
let selected = Meteor.collections.Products.findOne(selectedId);
|
||||
template.selectedProduct.set(selected);
|
||||
},
|
||||
'change input[name="date"]': function(event, template) {
|
||||
template.selectedDate.set(moment(event.target.value, "YYYY-MM-DD").toDate());
|
||||
},
|
||||
'click input[type="submit"]': function(event, template) {
|
||||
event.preventDefault();
|
||||
$('#insertSale').data('bs.validator').validate(function(isValid) {
|
||||
template.$('.insertSaleForm').data('bs.validator').validate(function(isValid) {
|
||||
if(isValid) {
|
||||
let sales = [];
|
||||
let sale = {
|
||||
@@ -192,30 +177,20 @@ Template.InsertSale.events({
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
//console.log("Inserting: " + JSON.stringify(next));
|
||||
Meteor.call('insertSale', next, function(error) {
|
||||
if(error) sAlert.error("Failed to insert the sale!\n" + error);
|
||||
else sAlert.success("Sale Created");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
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() {
|
||||
@@ -226,8 +201,8 @@ Template.InsertSale.helpers({
|
||||
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!");
|
||||
// if(product) console.log("Found " + result.length + " measures for the product " + product.name);
|
||||
// else console.log("No product!");
|
||||
|
||||
return result;
|
||||
},
|
||||
@@ -237,29 +212,45 @@ Template.InsertSale.helpers({
|
||||
});
|
||||
|
||||
Template.InsertSaleMeasure.onCreated(function() {
|
||||
let prices = this.parentTemplate().selectedProduct.get().prices;
|
||||
let price = 0;
|
||||
let _this = this;
|
||||
|
||||
if(prices) price = prices[this._id];
|
||||
|
||||
this.price = new ReactiveVar(price);
|
||||
this.price = new ReactiveVar(0);
|
||||
this.amount = new ReactiveVar(0);
|
||||
|
||||
Tracker.autorun(function() {
|
||||
let date = _this.parentTemplate().selectedDate.get();
|
||||
let prices = _this.parentTemplate().selectedProduct.get().prices;
|
||||
let priceData;
|
||||
let price = 0;
|
||||
|
||||
if(prices) priceData = prices[_this.data._id];
|
||||
|
||||
//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))
|
||||
price = priceData.previousPrice;
|
||||
else
|
||||
price = priceData.price
|
||||
}
|
||||
|
||||
_this.price.set(price);
|
||||
});
|
||||
});
|
||||
Template.InsertSaleMeasure.events({
|
||||
'change .price': function(event, template) {
|
||||
template.price.set(parseFloat($(event.target).val()).toFixed(2));
|
||||
template.price.set(parseFloat($(event.target).val()));
|
||||
},
|
||||
'change .amount': function(event, template) {
|
||||
template.amount.set(parseFloat($(event.target).val()).toFixed(2));
|
||||
template.amount.set(parseFloat($(event.target).val()));
|
||||
}
|
||||
});
|
||||
Template.InsertSaleMeasure.helpers({
|
||||
price: function() {
|
||||
return Template.instance().price.get();
|
||||
return Template.instance().price.get().toFixed(2);
|
||||
},
|
||||
total: function() {
|
||||
let template = Template.instance();
|
||||
return template.price.get() * template.amount.get();
|
||||
return (template.price.get() * template.amount.get()).toFixed(2);
|
||||
},
|
||||
amount: function() {
|
||||
return Template.instance().amount.get();
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<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
52
imports/ui/Subcategories.import.styl
vendored
@@ -1,52 +0,0 @@
|
||||
#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);
|
||||
@@ -1,76 +0,0 @@
|
||||
|
||||
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] : '';
|
||||
}
|
||||
});
|
||||
@@ -4,18 +4,18 @@
|
||||
<div class="insert">
|
||||
{{>UserInsert}}
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="tableContainer">
|
||||
<table class="dataTable table table-striped table-hover">
|
||||
<thead>
|
||||
<tr class="headers">
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Roles</th>
|
||||
<th>Actions</th>
|
||||
<th class="username">Username</th>
|
||||
<th class="email">Email</th>
|
||||
<th class="roles">Roles</th>
|
||||
<th class="actions">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>{{>UserSearch columnName='username'}}</th>
|
||||
<th>{{>UserSearch columnName='email' collectionQueryColumnName='name' collection='Items' collectionResultColumnName='_id'}}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -54,15 +54,15 @@
|
||||
|
||||
<template name="UserSearch">
|
||||
<div class="">
|
||||
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}" style="max-width: {{maxWidth}}px; min-width: {{minWidth}}px;"/>
|
||||
<input type="text" class="searchInput" placeholder="Filter..." value="{{searchValue}}" style="width: 90%"/>
|
||||
</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="grid">
|
||||
<div class="col-3-12"></div>
|
||||
<div class="col-6-12">
|
||||
<div class="formGroupHeading">New User</div>
|
||||
<div class="form-group">
|
||||
<label class='control-label'>User Name</label>
|
||||
@@ -84,7 +84,7 @@
|
||||
<input type="submit" class="btn btn-success" value="Create">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-0"></div>
|
||||
<div class="col-3-12"></div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
34
imports/ui/UserManagement.import.styl
vendored
34
imports/ui/UserManagement.import.styl
vendored
@@ -33,26 +33,26 @@
|
||||
font-style: normal
|
||||
font-variant: normal
|
||||
font-weight: 500
|
||||
|
||||
.grid
|
||||
flex: auto
|
||||
align-self: stretch
|
||||
overflow-y: auto
|
||||
overflow-x: auto
|
||||
.tableContainer
|
||||
width: 100%
|
||||
height: 100%
|
||||
margin-top: 20px
|
||||
margin-bottom: 20px
|
||||
border: 0
|
||||
padding-top: 20px
|
||||
|
||||
.table > thead > tr > th
|
||||
border: 0
|
||||
padding-top: 0
|
||||
padding-bottom: 6px
|
||||
|
||||
.dataTable
|
||||
font-size: 12.5px
|
||||
table
|
||||
table-layout: fixed
|
||||
|
||||
.tdLarge
|
||||
font-size: 1.5em
|
||||
width: 100%
|
||||
thead
|
||||
> tr
|
||||
> th.username
|
||||
width: auto
|
||||
> th.email
|
||||
width: auto
|
||||
> th.roles
|
||||
width: 260px
|
||||
> th.actions
|
||||
width: 80px
|
||||
.userRemove
|
||||
color: red
|
||||
.userEdit
|
||||
|
||||
@@ -55,12 +55,12 @@
|
||||
<div class="content">
|
||||
{{> Template.dynamic template=content}}
|
||||
</div>
|
||||
<div class="footer">
|
||||
© Petit Teton LLC 2017
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
© Petit Teton LLC 2017
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
12
imports/ui/layouts/Body.import.styl
vendored
12
imports/ui/layouts/Body.import.styl
vendored
@@ -106,12 +106,12 @@
|
||||
-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
|
||||
.footer
|
||||
display: table-row
|
||||
height: 1px
|
||||
text-align: center
|
||||
background: #4d7727
|
||||
color: white
|
||||
|
||||
|
||||
//#layoutBody
|
||||
|
||||
Reference in New Issue
Block a user