diff --git a/imports/api/Reports.js b/imports/api/Reports.js index 6aa4c80..93f222b 100644 --- a/imports/api/Reports.js +++ b/imports/api/Reports.js @@ -142,119 +142,311 @@ if(Meteor.isServer) { } 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) => { + WebApp.connectHandlers.use("/reports/ProductSalesByMeasure", (req, res, next) => { + const separator = ";"; + try { - let result = Meteor.collections.Sales.aggregate([{$group: {_id: {$substr: ['$date', 0, 6]}, total: {$sum: {$multiply: ["$price", "$amount"]}}}}]); + //Aggregate all the sale counts by product id, measure id, & year, then later create a map between products and tags to create tag totals, and measures to measure names to label them. + let result = Meteor.collections.Sales.aggregate([{ + $group: { + _id: {$concat: ["$productId", "-", {$substr: ['$date', 0, 4]}, "-", "$measureId"]}, + productId: {$first: "$productId"}, + measureId: {$first: "$measureId"}, + year: {$first: {$substr: ['$date', 0, 4]}}, + count: {$sum: "$amount"}, + total: {$sum: {$multiply: ["$price", "$amount"]}} + } + }]); result.toArray().then(function(result) { - res.writeHead(200, {'Content-Type': 'text/csv'}); + let totalByYear = {}; - 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"); + //Create a map of maps: year -> product id -> measure id -> sales count. + for(let next of result) { + let totalByProduct = totalByYear[next.year]; + //Create the map if necessary. + if(totalByProduct === undefined) totalByProduct = totalByYear[next.year] = {}; + + //Get the map of totals by product for the year. + let totalByMeasure = totalByProduct[next.productId]; + //Create the map if necessary. + if(totalByMeasure === undefined) totalByMeasure = totalByProduct[next.productId] = {}; + + totalByMeasure[next.measureId] = {count: next.count, total: next.total}; } + //Create a list of ordered measures. We could use a map, but then getting the ordering correct would be difficult. + let measures = Meteor.collections.Measures.find({}, {fields: {_id: 1, name: 1}, sort: {order: 1}}).fetch(); + + //Now create a mapping between the product id's and product names for later use. + let products = Meteor.collections.Products.find({}, {fields: {_id: 1, name: 1}, sort: {name: 1}}).fetch(); + + //Collect the years in ascending oder. + let years = Object.keys(totalByYear).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: Product, Year1 Count, Year1 Total, Year2 Count, .. + res.write("Product"); + + //Iterate over the years and add them to the headers. + for(let year of years) { + res.write(separator + year + " Count" + separator + year + " Total"); + } + + res.write('\n'); + + for(let product of products) { + let includeProduct = false; + let productMeasureIds = {}; + + //Collect the measureIds for the product in all years of data (it may only exist in some years). + for(let year of years) { + let productTotals = totalByYear[year][product._id]; + + if(productTotals) { + for(let measureId of Object.keys(productTotals)) { + productMeasureIds[measureId] = 1; + includeProduct = true; + } + } + } + + //If we have any data at all for the product then we should include it in the table. + if(includeProduct) { + res.write(product.name); + + //First include the summation of all measures for each year. + for(let year of years) { + let productTotals = totalByYear[year][product._id]; + let annualCount = 0; + let annualTotal = 0; + + //There may be no information for some years on some products. + if(productTotals) { + for(let totals of Object.values(productTotals)) { + annualCount += totals.count; + annualTotal += totals.total; + } + } + + res.write(separator + annualCount + separator + annualTotal); + } + + res.write("\n"); + + //Iterate over all measures in the system (they are ordered), and write out the rows for each one that has data for this product. Use zeros for years without data. + for(let measure of measures) { + //If this measure has any data for any year then include it in the table. + if(productMeasureIds[measure._id]) { + res.write(product.name + " " + measure.name); + + for(let year of years) { + let productTotals = totalByYear[year][product._id]; + + if(productTotals) { + let totals = productTotals[measure._id]; + + if(totals) { + res.write(separator + totals.count + separator + totals.total); + } + else { + res.write(separator + "0" + separator + "0"); + } + } + else { + res.write(separator + "0" + separator + "0"); + } + } + + res.write("\n"); + } + } + } + } + + res.end(); + }).catch((err) => { + console.log(err); + res.end(); + }); + } catch(err) { + console.log(err); + res.end(); + } + }); + WebApp.connectHandlers.use("/reports/TagSalesByMeasure", (req, res, next) => { + const separator = ";"; + try { + //Aggregate all the sale counts by product id, measure id, & year, then later create a map between products and tags to create tag totals, and measures to measure names to label them. + let result = Meteor.collections.Sales.aggregate([{ + $group: { + _id: {$concat: ["$productId", "-", {$substr: ['$date', 0, 4]}, "-", "$measureId"]}, + productId: {$first: "$productId"}, + measureId: {$first: "$measureId"}, + year: {$first: {$substr: ['$date', 0, 4]}}, + count: {$sum: "$amount"}, + total: {$sum: {$multiply: ["$price", "$amount"]}} + } + }]); + + result.toArray().then(function(result) { + let totalByYear = {}; + + //Create a map of maps: year -> product id -> measure id -> sales count. + for(let next of result) { + let totalByProduct = totalByYear[next.year]; + //Create the map if necessary. + if(totalByProduct === undefined) totalByProduct = totalByYear[next.year] = {}; + + //Get the map of totals by product for the year. + let totalByMeasure = totalByProduct[next.productId]; + //Create the map if necessary. + if(totalByMeasure === undefined) totalByMeasure = totalByProduct[next.productId] = {}; + + totalByMeasure[next.measureId] = {count: next.count, total: next.total}; + } + + //Create a list of ordered measures. We could use a map, but then getting the ordering correct would be difficult. + let measures = Meteor.collections.Measures.find({}, {fields: {_id: 1, name: 1}, sort: {order: 1}}).fetch(); + + //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(totalByYear).sort(function(a, b) {return parseInt(a) - parseInt(b);}); + + //A map of tag names -> measure id -> 1. The measureId's are not in an array, but rather an object setup as a HashSet. So tagMeasureIdMap['tagname']['measureId'] === 1 if the measure is related to the tag via sales for any year. + let tagMeasureIdMap = {}; + + //For each tag, collect measure id's that have data in the relevant years. + for(let tagName of Object.keys(tagProductIdsMap)) { + let productIds = tagProductIdsMap[tagName]; + let measureIdMap = {}; + + //Iterate over the products related to the tag, and the years we are querying to find all measures with sales data. + for(let productId of productIds) { + for(let year of years) { + let totalsByMeasureIdMap = totalByYear[year][productId]; + + if(totalsByMeasureIdMap) { + for(let measureId in totalsByMeasureIdMap) { + measureIdMap[measureId] = 1; + } + } + } + } + + tagMeasureIdMap[tagName] = measureIdMap; + } + + //Write the response table. + res.writeHead(200, {'Content-Type': 'text/csv'}); + + //Start with the table headers: Product, Year1 Count, Year1 Total, Year2 Count, .. + res.write("Tag"); + + //Iterate over the years and add them to the headers. + for(let year of years) { + res.write(separator + year + " Count" + separator + year + " Total"); + } + + res.write('\n'); + + //Iterate over each tag and add rows for summation and each measure with data associated with it. + for(let tagName of Object.keys(tagProductIdsMap)) { + let productIds = tagProductIdsMap[tagName]; + + if(productIds) { + //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 totalByProduct = totalByYear[year]; + let annualCount = 0; + 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 totalByMeasure = totalByProduct[productId]; + + if(totalByMeasure) { + for(let totals of Object.values(totalByMeasure)) { + annualCount += totals.count; + annualTotal += totals.total; + } + } + } + + res.write(separator + annualCount + separator + annualTotal); + } + + res.write('\n'); + + //Write a row for each measure associated with the tag's products in the sales data. + for(let measure of measures) { + //If there is data for the given measure for any year and this tag, then write a row for it. + if(tagMeasureIdMap[tagName][measure._id]) { + res.write(tagName + " - " + measure.name); + + //Write the sum of sales counts and $ amounts for each year for the tag and measure. + for(let year of years) { + let annualCount = 0; + let annualTotal = 0; + + for(productId of productIds) { + let totals = totalByYear[year][productId] ? totalByYear[year][productId][measure._id] : null; + + if(totals) { + annualCount += totals.count; + annualTotal += totals.total; + } + } + + res.write(separator + annualCount + separator + annualTotal); + } + + res.write('\n'); + } + } + } + } + + res.end(); + }).catch((err) => { + console.log(err); res.end(); }); } catch(err) { diff --git a/imports/ui/Reports.html b/imports/ui/Reports.html index 42591da..8fe4853 100644 --- a/imports/ui/Reports.html +++ b/imports/ui/Reports.html @@ -2,6 +2,8 @@