import './SalesSheetEditor.html';
import swal from 'sweetalert2';
import dragula from 'dragula';
let PREFIX = "SalesSheetEditor.";
//******************************************************************
//** The parent template for editing a sheet. Has two children which allow picking products, and organizing products.
//******************************************************************
Template.SalesSheetEditor.onCreated(function() {
// Default the currently displayed form to the product selection form.
let currentFormName = Session.get(PREFIX + "currentFormName");
if(currentFormName != "SalesSheetEditorProductSelection" && currentFormName != "SalesSheetEditorConfiguration") Session.set(PREFIX + "currentFormName", "SalesSheetEditorProductSelection");
//this.currentFormName = new ReactiveVar("SalesSheetEditorProductSelection");
// Save the data as the sheet. This is easier to read in the code, and it avoids the problem in onDestroyed() where our data is being changed out from under us.
this.sheet = Meteor.collections.SalesSheets.findOne(this.data);
});
Template.SalesSheetEditor.onRendered(function() {
});
Template.SalesSheetEditor.onDestroyed(function() {
let sheet = this.sheet; //Note: this.data does not refer to the SAME sheet instance, but rather another copy of the same sheet. So any changes would be lost if we referenced `this.data`.
swal({
title: "Save Changes",
text: "Would you like to save any changes you have made to this sheet?",
type: "question",
showCancelButton: true,
confirmButtonColor: "#7cdd7f",
confirmButtonText: "Yes"
}).then(
function(isConfirm) {
if(isConfirm) {
Meteor.call("updateSalesSheet", sheet._id, sheet.name, sheet.products, function(error) {
if(error) sAlert.error("Failed to update the sheet!\n" + error);
else {
sAlert.success("Updated the Sales Sheet.");
}
});
}
},
function(dismiss) {}
);
});
Template.SalesSheetEditor.events({
'click .productSelection': function(event, template) {
// Toggle which form template is active.
if(Session.get(PREFIX + "currentFormName") != "SalesSheetEditorProductSelection") {
$(event.target).addClass('selected').siblings().removeClass('selected');
Session.set(PREFIX + "currentFormName", "SalesSheetEditorProductSelection");
}
},
'click .sheetConfiguration': function(event, template) {
// Toggle which form template is active.
if(Session.get(PREFIX + "currentFormName") != "SalesSheetEditorConfiguration") {
$(event.target).addClass('selected').siblings().removeClass('selected');
Session.set(PREFIX + "currentFormName", "SalesSheetEditorConfiguration");
}
}
});
Template.SalesSheetEditor.helpers({
salesSheetEditorForm: function() {
return Session.get(PREFIX + "currentFormName");
},
salesSheetEditorData: function() {
return {parentTemplate: Template.instance(), salesSheet: Template.instance().sheet};
},
productSelectionSelected: function() {
return Session.get(PREFIX + "currentFormName") == 'SalesSheetEditorProductSelection' ? "selected" : "";
},
sheetConfigurationSelected: function() {
return Session.get(PREFIX + "currentFormName") == 'SalesSheetEditorConfiguration' ? "selected" : "";
}
});
//******************************************************************
//** Lets the user pick the products on the sheet.
//******************************************************************
Template.SalesSheetEditorProductSelection.onCreated(function() {
//Here, this is the template, and this.data is an object containing the 'parentTemplate' and 'salesSheet' properties.
//Save the sales sheet as a property of this template to make the code later easier to read.
//Note: This is not reactive because we don't expect the sales sheet to change without closing the editor (re-editing would open a new template instance). Also, the sales sheet is a clone of the real one, so any changes will be lost if not saved.
this.salesSheet = this.data.salesSheet;
this.productNameFilter = new ReactiveVar("");
Session.set(PREFIX + "showHidden", false);
});
Template.SalesSheetEditorProductSelection.events({
'keyup input[name="productFilter"]': _.throttle(function(event, template) {
template.productNameFilter.set($(event.target).val());
}),
'click .clearFilter': function(event, template) {
template.$('input[name="productFilter"]').val('');
template.productNameFilter.set('');
},
'change input[name="showHidden"]': function(event, template) {
//console.log("changed " + $(event.target).prop('checked'));
Session.set(PREFIX + "showHidden", $(event.target).prop('checked'));
}
});
Template.SalesSheetEditorProductSelection.helpers({
products: function() {
let salesSheet = this.salesSheet;
let filter = Template.instance().productNameFilter.get();
let products = salesSheet.products ? salesSheet.products : [];
let productMap = {};
let dbQuery = [];
let showHidden = Session.get(PREFIX + "showHidden");
//Map the products in the sales sheet by their id so we can later only add products to the list that are not on the sheet.
for(next of products) {
if(next.productId) //Ignore any elements that don't have an id since they may be headers that have no associated product.
productMap[next.productId] = next;
}
//If we have a filter, split the filter (space delimited) and use each piece to match against the name, and the alternate names for the product.
//Only match where the word in the name starts with the characters in the filter. So "ra j" would match "raspberry jam".
if(filter && filter.trim().length > 0) {
let searches = filter.trim().split(/\s+/);
let regex = "";
for(let search of searches) {
search = RegExp.escape(search);
regex += '(?=.*\\b' + search + ')'
}
regex += '.*';
dbQuery.push({name: {$regex: regex, $options: 'i'}});
}
if(!showHidden) {
//Ignore any hidden elements by showing those not hidden, or those without the hidden field.
dbQuery.push({$or: [{hidden: false}, {hidden: {$exists:false}}]});
}
if(dbQuery.length > 0) dbQuery = {$and: dbQuery};
else dbQuery = {};
let allProducts = Meteor.collections.Products.find(dbQuery).fetch();
//Mark all the products that are currently included in the sheet and note the name they are included as.
for(next of allProducts) {
//Attach the sheet data for the product to the actual product model if it is in the sheet.
if(productMap[next._id]) {
//Add the sheet product data to the product for those products that are on the sheet. Use this to determine if a product is on the sheet, and to remove it from the sheet.
next.sheetProduct = productMap[next._id];
}
else next.sheetProduct = undefined;
}
return allProducts;
}
});
Template.SalesSheetEditorProductSelectionRow.onCreated(function() {
//Here, this refers to the template, and this.data is the Product object which has been modified to reference a Sheet object via the 'sheetProduct' property if the product is on the sheet.
//We are creating a reactive variable to hold the sheet's product data, if the product is on the sheet, otherwise it is empty.
//Note: The Product's sheetProduct references the product data for the sheet, but it must also be referenced in the sheet object held by the parent.
this.sheetProduct = new ReactiveVar(this.data.sheetProduct);
});
Template.SalesSheetEditorProductSelectionRow.events({
'click .include': function(event, template) {
let sheet = template.parentTemplate(1).salesSheet;
if(template.sheetProduct.get()) { //Remove the product from the sheet, or rename it (if the names don't match and the user clicked on the name instead of the checkbox).
// TODO: The commented code fails to detect newly added (to the sheet) products and instead of removing them, it seems to be changing the name when unchecking. This should also keep the element checked in the list instead of unchecking.
//If the click was on the product's name, and the product name is different from the name used for it in the sheet, then use the product name as the name in the sheet instead of removing it from the sheet.
//if($(event.target).closest('.productName') && this.name !== template.sheetProduct.get().name) {
// template.sheetProduct.get().name = this.name;
//}
//else {
let index = sheet.products.indexOf(template.sheetProduct.get());
//Remove the product data from the sheet first. Template.parentData(1) is the sheet.
if(index >= 0) sheet.products.splice(index, 1);
//Clear the sheet product data from the actual product.
template.sheetProduct.set(undefined);
//}
}
else {
let sheetProduct = {name: this.name, productId: this._id, measureIds: this.measures.length > 2 ? this.measures.slice(0,2) : this.measures};
//Save the sheet product in the sheet. Template.parentData(1) is the sheet.
sheet.products.push(sheetProduct);
//Attach the sheet product data to the actual product.
template.sheetProduct.set(sheetProduct);
}
}
});
Template.SalesSheetEditorProductSelectionRow.helpers({
sheetProduct: function() {
return Template.instance().sheetProduct.get();
},
hidden: function() {
return this.hidden;
},
deactivated: function() {
return this.deactivated;
},
sheetProductName: function() {
return Template.instance().sheetProduct.get().name;
},
showAlternateName: function() {
let sheetProduct = Template.instance().sheetProduct.get();
return sheetProduct && sheetProduct.name !== this.name;
}
});
//******************************************************************
//** Lets the user configure the products on the sheet.
//******************************************************************
Template.SalesSheetEditorConfiguration.onCreated(function() {
let template = this;
//Here, this is the template, and this.data is an object containing the 'parentTemplate' and 'salesSheet' properties.
//Save the sales sheet as a property of this template to make the code later easier to read.
//Note: This is not reactive because we don't expect the sales sheet to change without closing the editor (re-editing would open a new template instance). Also, the sales sheet is a clone of the real one, so any changes will be lost if not saved.
this.salesSheet = this.data.salesSheet;
this.measures = new ReactiveDict();
this.productsDependency = new Tracker.Dependency;
Tracker.autorun(function() {
let measures = Meteor.collections.Measures.find({}).fetch();
template.measures.clear();
for(let measure of measures) {
template.measures.set(measure._id, measure);
}
});
Session.set(PREFIX + "showMeasures", false);
});
Template.SalesSheetEditorConfiguration.onRendered(function() {
let template = this;
//Setup the drag and drop for the view.
this.drake = dragula([this.$('.configurationProductsListing')[0], this.$('.tableControls')[0]], {
moves: function(el, container, handle, sibling) {
//Don't allow drag and drop of buttons - we want them to be clickable.
return !$(handle).hasClass("button");
},
//Checks whether the element `el` can be moved from the container `target`, to the container `source`, above the `sibling` element.
accepts: function(el, target, source, sibling) {
return (!sibling || !$(sibling).hasClass('newHeading'));
},
copy: function(el, source) {
return $(el).hasClass('heading') && $(source).hasClass('tableControls');
},
ignoreInputTextSelection: true
}).on('drop', function(el, target, source, sibling) {
if($(el).hasClass('heading')) {
if(el.parentNode) {
let array = template.salesSheet.products;
//Add the heading to the product array.
array.add({name: "New Heading"}, $(el).index());
//Remove the element that was just added by the D&D. The element will be re-added by the template in just a moment. We need the template to add the element so that events will be properly handled for it by meteor.
el.parentNode.removeChild(el);
//Notify the template engine that the products list has changed so it can be re-rendered.
template.productsDependency.changed();
}
}
else {
//Get the item from the DOM using the blaze data structure. We could make this more blaze agnostic by attaching the object as data to the DOM in the view, but we really can't escape blaze, so why bother.
//let item = el.$blaze_range.view._templateInstance.data;
let productId = $(el).data('model');
let array = template.salesSheet.products;
let item = undefined;
for(let product of array) {
if(productId == product.productId) {
item = product;
break;
}
}
if(item) {
//Rearrange the array of products on the sheet.
array.move(array.indexOf(item), $(el).index());
}
else {
console.log("ERROR: Unable to locate the moved item.");
}
}
});
});
Template.SalesSheetEditorConfiguration.onDestroyed(function() {
//Clean up after the drag and drop.
this.drake.destroy();
});
Template.SalesSheetEditorConfiguration.events({
'change input[name="showMeasures"]': function(event, template) {
Session.set(PREFIX + "showMeasures", $(event.target).prop('checked'));
}
});
Template.SalesSheetEditorConfiguration.helpers({
products: function() {
let template = Template.instance();
let products = template.salesSheet.products;
//Mark this call as depending on the products array. When we change the array later, we will call changed() on the dependency and it will trigger this function (and the calling template setup) to be re-run.
template.productsDependency.depend();
return products;
}
});
//Note: The data to this template is a product metadata object that is part of a sheet and wrappers (by ID association) a product in the system. See the schema in SalesSheet.js, look for 'products.$' to see the type definition for this data.
Template.SalesSheetEditorConfigurationRow.onCreated(function() {
let template = this;
this.handleHeaderEditorCancelAndClose = function() {
let $inputField = template.$("input[name='name']");
let index = template.$('.heading').index();
//Reset the text field.
$inputField.val(template.parentTemplate(1).salesSheet.products[index].name);
template.$('.heading .nameEditor, .heading .headingNameRow').removeClass('edit');
};
this.handleHeaderEditorApplyAndClose = function() {
let $inputField = template.$("input[name='name']");
let name = $inputField.val();
let index = template.$('.heading').index();
if(name) name = name.trim();
if(name && name.length > 0) {
template.parentTemplate(1).salesSheet.products[index].name = name;
template.$('.heading .name').text(name);
}
else {
template.parentTemplate(1).salesSheet.products.splice(index, 1);
template.parentTemplate(1).productsDependency.changed();
}
template.$('.heading .nameEditor, .heading .headingNameRow').removeClass('edit');
};
this.handleProductEditorCancelAndClose = function() {
let $inputField = template.$("input[name='name']");
let index = template.$('.product').index();
//Reset the text field.
$inputField.val(template.parentTemplate(1).salesSheet.products[index].name);
template.$('.product .nameEditor, .product .name').removeClass('edit');
};
this.handleProductEditorApplyAndClose = function() {
let $inputField = template.$("input[name='name']");
let name = $inputField.val();
let index = template.$('.product').index();
template.parentTemplate(1).salesSheet.products[index].name = name;
template.$('.product .name').text(name);
template.$('.product .nameEditor, .product .name').removeClass('edit');
};
});
Template.SalesSheetEditorConfigurationRow.helpers({
measureName: function(measureId) {
return Template.instance().parentTemplate(1).measures.get(measureId).name;
},
measures: function() {
let product = Meteor.collections.Products.findOne(this.productId);
return product.measures;
},
isSelected: function(measureId) {
return this.measureIds.includes(measureId);
},
isProduct: function() {
return !!this.productId;
},
showMeasures: function() {
return Session.get(PREFIX + "showMeasures");
}
});
Template.SalesSheetEditorConfigurationRow.events({
'dblclick .heading .name': function(event, template) {
template.$('.nameEditor, .headingNameRow').addClass('edit');
template.$('input[name="name"]').select();
},
'blur .heading input[name="name"]': function(event, template) {
template.handleHeaderEditorApplyAndClose();
},
'keyup .heading input[name="name"]': function(event, template) {
if(event.which === 13 || event.which === 9) { //Enter or Tab
template.handleHeaderEditorApplyAndClose();
event.stopPropagation();
return false;
}
else if(event.which === 27) { //Escape
template.handleHeaderEditorCancelAndClose();
event.stopPropagation();
return false;
}
},
'click .heading .accept': function(event, template) {
template.handleHeaderEditorApplyAndClose();
},
'click .heading .reject': function(event, template) {
template.handleHeaderEditorCancelAndClose();
},
'dblclick .product .name': function(event, template) {
template.$('.nameEditor, .name').addClass('edit');
template.$('input[name="name"]').select();
},
'blur .product input[name="name"]': function(event, template) {
template.handleProductEditorApplyAndClose();
},
'keyup .product input[name="name"]': function(event, template) {
if(event.which === 13 || event.which === 9) { //Enter or Tab
template.handleProductEditorApplyAndClose();
event.stopPropagation();
return false;
}
else if(event.which === 27) { //Escape
template.handleProductEditorCancelAndClose();
event.stopPropagation();
return false;
}
},
'click .product .accept': function(event, template) {
template.handleProductEditorApplyAndClose();
},
'click .product .reject': function(event, template) {
template.handleProductEditorCancelAndClose();
},
'click .measureButton': function(event, template) {
let measureId = $(event.target).data("model");
$(event.target).toggleClass("selected");
if(this.measureIds.includes(measureId))
this.measureIds.remove(measureId);
else
this.measureIds.add(measureId);
},
'click .heading .sort': function(event, template) {
let width = event.currentTarget.offsetWidth;
let x = event.pageX - event.currentTarget.offsetLeft;
let sortAlphabetical = x <= (width / 2);
let headingIndex = template.$(event.target).closest(".heading").index();
let firstIndex = headingIndex + 1;
let products = template.parentTemplate(1).salesSheet.products;
let length = 0;
while(firstIndex + length < products.length && products[firstIndex + length].productId) {
length++;
}
//Sort the part of the array that contains products under the sorted heading.
products.partialSort(firstIndex, length, function(a, b) {
return sortAlphabetical ? (a.name < b.name ? -1 : 1) : (a.name > b.name ? -1 : 1);
});
//Notify anything depending on the products list that they have been modified.
template.parentTemplate(1).productsDependency.changed();
}
});