Added a reports page with a set of links to download CSV reports.
This commit is contained in:
265
imports/api/Reports.js
Normal file
265
imports/api/Reports.js
Normal file
@@ -0,0 +1,265 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
if(Meteor.isServer) {
|
||||
WebApp.connectHandlers.use("/reports/AnnualTotals", (req, res, next) => {
|
||||
try {
|
||||
let result = Meteor.collections.Sales.aggregate([{$group: {_id: {$substr: ['$date', 0, 4]}, total: {$sum: {$multiply: ["$price", "$amount"]}}}}]);
|
||||
|
||||
result.toArray().then(function(result) {
|
||||
res.writeHead(200, {'Content-Type': 'text/csv'});
|
||||
|
||||
res.write("Year,Sales Total\n");
|
||||
|
||||
for(let i = 0; i < result.length; i++) {
|
||||
res.write(result[i]._id);
|
||||
res.write(",");
|
||||
res.write("" + result[i].total);
|
||||
res.write("\n");
|
||||
}
|
||||
|
||||
res.end();
|
||||
});
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
WebApp.connectHandlers.use("/reports/MonthlyTotals", (req, res, next) => {
|
||||
try {
|
||||
let result = Meteor.collections.Sales.aggregate([{$group: {_id: {$substr: ['$date', 0, 6]}, total: {$sum: {$multiply: ["$price", "$amount"]}}}}]);
|
||||
|
||||
result.toArray().then(function(result) {
|
||||
res.writeHead(200, {'Content-Type': 'text/csv'});
|
||||
|
||||
res.write("Date,Sales Total\n");
|
||||
|
||||
result.sort(function(a, b) {
|
||||
return parseInt(a._id) - parseInt(b._id);
|
||||
});
|
||||
|
||||
for(let i = 0; i < result.length; i++) {
|
||||
res.write(result[i]._id.substr(4, 2) + "/" + result[i]._id.substr(0, 4));
|
||||
res.write(",");
|
||||
res.write("" + result[i].total);
|
||||
res.write("\n");
|
||||
}
|
||||
|
||||
res.end();
|
||||
});
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
WebApp.connectHandlers.use("/reports/TagTotals", (req, res, next) => {
|
||||
try {
|
||||
//Aggregate all the sales by product id & year, then later create a map between products and tags to create tag totals.
|
||||
let result = Meteor.collections.Sales.aggregate([{
|
||||
$group: {
|
||||
_id: {$concat: ["$productId", "-", {$substr: ['$date', 0, 4]}]},
|
||||
productId: {$first: "$productId"},
|
||||
year: {$first: {$substr: ['$date', 0, 4]}},
|
||||
total: {$sum: {$multiply: ["$price", "$amount"]}}
|
||||
}
|
||||
}]);
|
||||
|
||||
result.toArray().then(function(result) {
|
||||
let productSalesTotalsMapByYear = {};
|
||||
|
||||
//Create a map of maps: year -> product id -> sales totals.
|
||||
for(let next of result) {
|
||||
let productSalesTotalsMap = productSalesTotalsMapByYear[next.year];
|
||||
|
||||
//Create the map if necessary.
|
||||
if(productSalesTotalsMap === undefined) productSalesTotalsMap = productSalesTotalsMapByYear[next.year] = {};
|
||||
|
||||
productSalesTotalsMap[next.productId] = next.total;
|
||||
}
|
||||
|
||||
//Now create a mapping between the tag id's and tag names for later use.
|
||||
let tagIdToTagNameMap = {};
|
||||
let tags = Meteor.collections.ProductTags.find({}, {fields: {_id: 1, name: 1}}).fetch();
|
||||
|
||||
for(let tag of tags) {
|
||||
tagIdToTagNameMap[tag._id] = tag.name;
|
||||
}
|
||||
|
||||
//Now create a map between tag names -> [product ids] so that we can build a table below.
|
||||
let tagProductIdsMap = {};
|
||||
let products = Meteor.collections.Products.find({}, {fields: {_id: 1, tags: 1}}).fetch();
|
||||
|
||||
for(let product of products) {
|
||||
for(let tagId of product.tags) {
|
||||
let tagName = tagIdToTagNameMap[tagId];
|
||||
let productIds = tagProductIdsMap[tagName];
|
||||
|
||||
//Initialize the array if undefined, and add to the mapping.
|
||||
if(productIds === undefined) productIds = tagProductIdsMap[tagName] = [];
|
||||
|
||||
productIds.push(product._id);
|
||||
}
|
||||
}
|
||||
|
||||
//Collect the years in ascending oder.
|
||||
let years = Object.keys(productSalesTotalsMapByYear).sort(function(a, b) {return parseInt(a) - parseInt(b);});
|
||||
|
||||
//Write the response table.
|
||||
res.writeHead(200, {'Content-Type': 'text/csv'});
|
||||
|
||||
//Start with the table headers: Tag, Year1, Year2, ..
|
||||
res.write("Tag");
|
||||
|
||||
//Iterate over the years and add them to the headers.
|
||||
for(let year of years) {
|
||||
res.write("," + year);
|
||||
}
|
||||
|
||||
res.write('\n');
|
||||
|
||||
//Now write the tag name, and the annual totals for each tag as a row.
|
||||
for(let tagName in tagProductIdsMap) {
|
||||
let productIds = tagProductIdsMap[tagName];
|
||||
|
||||
//console.log(result[i].name + "::" + result[i].total);
|
||||
res.write(tagName);
|
||||
|
||||
//Iterate over each year to add its total as a column.
|
||||
for(let year of years) {
|
||||
let productSalesTotalsMap = productSalesTotalsMapByYear[year];
|
||||
let annualTotal = 0;
|
||||
|
||||
//Iterate over all the product id's for the tag and sum the annual totals for each product that is tagged.
|
||||
for(productId of productIds) {
|
||||
let productAnnualTotal = productSalesTotalsMap[productId];
|
||||
|
||||
if(productAnnualTotal) annualTotal += productAnnualTotal;
|
||||
}
|
||||
|
||||
res.write("," + annualTotal);
|
||||
}
|
||||
|
||||
res.write('\n');
|
||||
}
|
||||
|
||||
res.end();
|
||||
|
||||
|
||||
//let tagSalesTotals = [];
|
||||
//let tagIdToNameMap = [];
|
||||
|
||||
////Find all products and collect sales totals by tag id.
|
||||
//let products = Meteor.collections.Products.find({}, {fields: {_id: 1, tags: 1}}).fetch();
|
||||
//
|
||||
//for(let product of products) {
|
||||
// for(let tag of product.tags) {
|
||||
// let tagSalesTotal = tagSalesTotals[tag];
|
||||
// let productSalesTotal = productSalesTotalsMap[product._id];
|
||||
//
|
||||
// if(tagSalesTotal === undefined) tagSalesTotal = 0;
|
||||
// if(productSalesTotal === undefined) productSalesTotal = 0;
|
||||
//
|
||||
// tagSalesTotals[tag] = tagSalesTotal + productSalesTotal;
|
||||
// }
|
||||
//}
|
||||
//
|
||||
////Find all tags and convert the tag id's to tag names.
|
||||
//let tags = Meteor.collections.ProductTags.find({}, {fields: {_id: 1, name: 1}}).fetch();
|
||||
//
|
||||
//for(let tag of tags) {
|
||||
// tagIdToNameMap[tag._id] = tag.name;
|
||||
//}
|
||||
|
||||
////Write the response table.
|
||||
//res.writeHead(200, {'Content-Type': 'text/csv'});
|
||||
//
|
||||
//res.write("Tag,Sales Total\n");
|
||||
//
|
||||
////result.sort(function(a, b) {
|
||||
//// return a.name > b.name ? 1 : -1;
|
||||
////});
|
||||
//
|
||||
//for(let tagId in tagSalesTotals) {
|
||||
// let tagTotal = tagSalesTotals[tagId];
|
||||
// let tagName = tagIdToNameMap[tagId];
|
||||
//
|
||||
// //console.log(result[i].name + "::" + result[i].total);
|
||||
// res.write(tagName + ',' + tagTotal + '\n');
|
||||
//}
|
||||
//
|
||||
//res.end();
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
res.end();
|
||||
});
|
||||
|
||||
//Get all tags
|
||||
//Get all products for each tag
|
||||
//Total all sales for each product
|
||||
//let tags = Meteor.collections.ProductTags.find({},{fields: {_id: 1, name: 1}}).fetch();
|
||||
//let result = [];
|
||||
//
|
||||
//for(let tag of tags) {
|
||||
// let tagTotal = 0;
|
||||
// let products = Meteor.collections.Products.find({tags: tag._id}, {fields: {_id: 1}}).fetch();
|
||||
//
|
||||
// for(let product of products) {
|
||||
// let sales = Meteor.collections.Sales.find({productId: product._id}, {fields: {amount: 1, price: 1, _id: 0}}).fetch();
|
||||
//
|
||||
// for(let sale of sales) {
|
||||
// tagTotal += sale.amount * sale.price;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// result.push({name: tag.name, total: tagTotal});
|
||||
//}
|
||||
|
||||
//res.writeHead(200, {'Content-Type': 'text/csv'});
|
||||
//
|
||||
//res.write("Tag,Sales Total\n");
|
||||
//
|
||||
//result.sort(function(a, b) {
|
||||
// return a.name > b.name ? 1 : -1;
|
||||
//});
|
||||
//
|
||||
//for(let i = 0; i < result.length; i++) {
|
||||
// console.log(result[i].name + "::" + result[i].total);
|
||||
// res.write(result[i].name);
|
||||
// res.write(",");
|
||||
// res.write("" + result[i].total);
|
||||
// res.write("\n");
|
||||
//}
|
||||
//
|
||||
//res.end();
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
WebApp.connectHandlers.use("/reports/WeekYearTable", (req, res, next) => {
|
||||
try {
|
||||
let result = Meteor.collections.Sales.aggregate([{$group: {_id: {$substr: ['$date', 0, 6]}, total: {$sum: {$multiply: ["$price", "$amount"]}}}}]);
|
||||
|
||||
result.toArray().then(function(result) {
|
||||
res.writeHead(200, {'Content-Type': 'text/csv'});
|
||||
|
||||
res.write("Date,Sales Total\n");
|
||||
|
||||
result.sort(function(a, b) {
|
||||
return parseInt(a._id) - parseInt(b._id);
|
||||
});
|
||||
|
||||
for(let i = 0; i < result.length; i++) {
|
||||
res.write(result[i]._id.substr(4, 2) + "/" + result[i]._id.substr(0, 4));
|
||||
res.write(",");
|
||||
res.write("" + result[i].total);
|
||||
res.write("\n");
|
||||
}
|
||||
|
||||
res.end();
|
||||
});
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -349,7 +349,7 @@ if(Meteor.isServer) {
|
||||
|
||||
let dateString = date.toString();
|
||||
let timestamp = new Date(dateString.substring(0, 4) + "-" + dateString.substring(4, 6) + "-" + dateString.substring(6, 8) + "T00:00:00Z");
|
||||
let weekOfYear = sale.timestamp.getWeek().toString();
|
||||
let weekOfYear = timestamp.getWeek().toString();
|
||||
|
||||
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
|
||||
Sales.update(id, {$set: {date, venueId, price, amount, timestamp, weekOfYear}}, function(err, id) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import Logs from "./Logs.js";
|
||||
import Users from "./User.js";
|
||||
import UserRoles from "./Roles.js";
|
||||
import Workers from "./Worker.js";
|
||||
import './Reports.js';
|
||||
|
||||
//Save the collections in the Meteor.collections property for easy access without name conflicts.
|
||||
Meteor.collections = {Measures, Venues, Products, ProductTags, Sales, SalesSheets, Logs, Users, UserRoles, Workers};
|
||||
|
||||
@@ -102,6 +102,13 @@ pri.route('/graphs', {
|
||||
BlazeLayout.render('Body', {content: 'Graphs'});
|
||||
}
|
||||
});
|
||||
pri.route('/reports', {
|
||||
name: 'Reports',
|
||||
action: function(params, queryParams) {
|
||||
require("/imports/ui/Reports.js");
|
||||
BlazeLayout.render('Body', {content: 'Reports'});
|
||||
}
|
||||
});
|
||||
pri.route('/graphTest', {
|
||||
name: 'GraphTest',
|
||||
action: function(params, queryParams) {
|
||||
|
||||
7
imports/ui/Reports.html
Normal file
7
imports/ui/Reports.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<template name="Reports">
|
||||
<div id="reports">
|
||||
<a download="AnnualTotals.csv" href="/reports/AnnualTotals">Annual Totals (csv)</a><br/>
|
||||
<a download="MonthlyTotals.csv" href="/reports/MonthlyTotals">Monthly Totals (csv)</a><br/>
|
||||
<a download="TagTotals.csv" href="/reports/TagTotals">Tag Totals (csv)</a><br/>
|
||||
</div>
|
||||
</template>
|
||||
8
imports/ui/Reports.import.styl
vendored
Normal file
8
imports/ui/Reports.import.styl
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
#reports
|
||||
display: table
|
||||
content-box: border-box
|
||||
padding: 10px 20px
|
||||
height: 100%
|
||||
width: 100%
|
||||
text-align: left
|
||||
margin-left:20px
|
||||
54
imports/ui/Reports.js
Normal file
54
imports/ui/Reports.js
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
import './Reports.html';
|
||||
|
||||
let PREFIX = "reports.";
|
||||
|
||||
//let SalesTotals = new Meteor.Collection("salesTotals");
|
||||
//
|
||||
//Meteor.subscribe("venues");
|
||||
//Meteor.subscribe("productTags");
|
||||
|
||||
let salesTotalsSubscription;
|
||||
|
||||
|
||||
Template.Reports.onCreated(function() {
|
||||
//let template = Template.instance();
|
||||
|
||||
//Tracker.autorun(function() {
|
||||
// salesTotalsSubscription = template.subscribe("salesTotals", Session.get(PREFIX + "time"), Session.get(PREFIX + "options"));
|
||||
//});
|
||||
});
|
||||
Template.Reports.onRendered(function() {
|
||||
//Template.instance()
|
||||
//Tracker.autorun(function() {
|
||||
// if(salesTotalsSubscription.ready()) {
|
||||
//
|
||||
// }
|
||||
//});
|
||||
});
|
||||
//Template.Reports.helpers({
|
||||
// sales: function() {
|
||||
// let sort = [];
|
||||
//
|
||||
// sort.push(['year', 'asc']);// year = 1;
|
||||
// if(Session.get(PREFIX + "time") === 'weekly') sort.push(['week', 'asc']); // .week = 1;
|
||||
// if(Session.get(PREFIX + "time") === 'monthly') sort.push(['month', 'asc']); // .month = 1;
|
||||
// if(Session.get(PREFIX + "options") === 'markets') sort.push(['venue', 'asc']); // .month = 1;
|
||||
//
|
||||
// return SalesTotals.find({}, {sort: sort});
|
||||
// },
|
||||
// showTime: function(time) {
|
||||
// return Session.get(PREFIX + "time") === time;
|
||||
// },
|
||||
// showOption: function(option) {
|
||||
// return Session.get(PREFIX + "options") === option;
|
||||
// },
|
||||
// formatTotal: function(total) {
|
||||
// return "$" + total.toFixed(2);
|
||||
// }
|
||||
//});
|
||||
//Template.Reports.events({
|
||||
// 'click annualNumbers': function(event, template) {
|
||||
//
|
||||
// }
|
||||
//});
|
||||
@@ -51,6 +51,13 @@
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="{{isActiveRoute 'Reports'}}">
|
||||
<a href="{{pathFor 'Reports'}}">
|
||||
Reports
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer">
|
||||
© Petit Teton LLC 2017
|
||||
|
||||
Reference in New Issue
Block a user