diff --git a/.meteor/packages b/.meteor/packages index 74d4414..4fb7e8e 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -53,3 +53,5 @@ vsivsi:job-collection aldeed:moment-timezone nimble:restivus shell-server +lepozepo:accounting +okgrow:analytics diff --git a/.meteor/versions b/.meteor/versions index 296a7b7..6117367 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -47,6 +47,7 @@ jquery@1.11.9 kadira:flow-router@2.12.1 kadira:login-state@1.3.1 launch-screen@1.0.12 +lepozepo:accounting@1.0.0 less@2.7.5 livedata@1.0.18 localstorage@1.0.11 @@ -72,6 +73,7 @@ nimble:restivus@0.8.11 npm-bcrypt@0.9.1 npm-mongo@1.5.48 observe-sequence@1.0.12 +okgrow:analytics@2.0.0 ordered-dict@1.0.8 percolate:migrations@0.9.8 promise@0.8.4 diff --git a/imports/api/cache/index.js b/imports/api/cache/index.js new file mode 100644 index 0000000..4c6a148 --- /dev/null +++ b/imports/api/cache/index.js @@ -0,0 +1,3 @@ +import {Mongo} from 'meteor/mongo'; + +export const MiniMongo = new Mongo.Collection(null); \ No newline at end of file diff --git a/imports/api/email/functions.js b/imports/api/email/functions.js index 7a1b604..0965840 100644 --- a/imports/api/email/functions.js +++ b/imports/api/email/functions.js @@ -64,7 +64,6 @@ export const getRecipientInfo = ({recipient, sender}) => { return false; } const recipientElements = recipient.split("-"); - // console.log(recipientElements) const planId = recipientElements[0], organizationId = recipientElements[1] @@ -114,7 +113,6 @@ export const getSurveyEmailOptions = ({template, data}) => { siteName: "", leaderProfileUrl: "", leaderName: "", - leaderGender: "", alias: "", orgName: "", employeeName: "", @@ -161,7 +159,6 @@ export const getSurveyEmailOptions = ({template, data}) => { mailData.siteName = SITE_NAME; mailData.leaderProfileUrl = `http://${mailData.alias}.${domain}`; mailData.leaderName = `${capitalize(leader.firstName)} ${capitalize(leader.lastName)}`; - mailData.leaderGender = ""; mailData.orgName = `${capitalize(organization.name)}`; mailData.employeeName = `${capitalize(employee.firstName)}`; mailData.metric = capitalize(metric); @@ -172,10 +169,10 @@ export const getSurveyEmailOptions = ({template, data}) => { { mailData.replyGuideHeader = EMAIL_TEMPLATE_CONTENT.metrics.replyGuideHeader; mailData.replyGuideMessage = EMAIL_TEMPLATE_CONTENT.metrics.replyGuideMessage; - mailData.message = `Please help your leader "${mailData.leaderName}" to improve "${mailData.metric}" management by giving ${mailData.leaderGender} a score.`; + mailData.message = `Please help your leader "${mailData.leaderName}" to improve "${mailData.metric}" management by giving a score.`; mailData.description = EMAIL_TEMPLATE_CONTENT.metrics[metric]; senderSuffix = template; - subject = `${mailData.employeeName}, How "${mailData.leaderName}" can improve ${mailData.leaderGender} score on ${mailData.metric} Management?`; + subject = `${mailData.employeeName}, How "${mailData.leaderName}" can improve ${mailData.metric} Management?`; break; } @@ -183,31 +180,31 @@ export const getSurveyEmailOptions = ({template, data}) => { { mailData.replyGuideHeader = EMAIL_TEMPLATE_CONTENT.metrics.replyGuideHeader; mailData.replyGuideMessage = EMAIL_TEMPLATE_CONTENT.metrics.replyGuideMessage; - mailData.message = `Please help your leader "${mailData.leaderName}" to improve "${mailData.metric}" management by giving ${mailData.leaderGender} a score.`; + mailData.message = `Please help "${mailData.leaderName}" to improve "${mailData.metric}" management by giving accurate score. The score should be a number from 1 to 5. If you think ${mailData.leaderName} is doing a great job, just reply the email by sending 5. If you think ${mailData.leaderName} is doing moderate job reply the email by sending 3 and if ${mailData.leaderName} is doing very bad then send 1.`; mailData.description = EMAIL_TEMPLATE_CONTENT.metrics[metric]; senderSuffix = "survey"; - subject = `Please correct the score about "${mailData.metric}" for ${mailData.leaderName} in ${mailData.orgName}.`; + subject = `${mailData.employeeName}, seems the score of "${mailData.metric}" has some issues.`; break; } case "feedback": { mailData.replyGuideHeader = EMAIL_TEMPLATE_CONTENT[template].replyGuideHeader; - mailData.replyGuideMessage = `Simply reply this email with your suggestion for ${mailData.leaderName} to improve ${mailData.leaderGender} ${mailData.metric} Management.`; - mailData.message = `Help your leader "${mailData.leaderName}" to improve "${mailData.metric}" Management.`; - mailData.description = EMAIL_TEMPLATE_CONTENT[template].description; + mailData.replyGuideMessage = `Simply reply this email with your suggestion and write whatever you think is good.`; + mailData.message = `"${mailData.leaderName}" needs your help to improve "${mailData.metric}" Management.`; + mailData.description = `Your feedback is very important and it will be kept CONFIDENTIAL, it means ${mailData.leaderName} won’t be able to know who submitted the feedback.`; senderSuffix = template; - subject = `How could "${mailData.leaderName}" improve the ${mailData.metric} for higher score?`; + subject = `${mailData.employeeName}, "${mailData.leaderName}" wants to improve ${mailData.metric}, how?`; break; } case "thankyou": { const {type} = data; - mailData.message = `Thank you very much for your ${type} to "${mailData.leaderName}" about ${mailData.leaderGender} "${mailData.metric}" Management.`; - mailData.description = `Your ${type} will help the leader to improve ${mailData.leaderGender} ability.`; - mailData.viewLeaderProfileHeader = `Want to view ${mailData.leaderName} profile?`; + mailData.message = `I appreciate your contribution on "${mailData.metric}" Management. I believe better leadership can help all of us to enjoy our daily life even more. Besides, it is very effective for you as an individual. The better the leader, happier employee.\nBest Regards,\ntheLeader.io on behalf of "${mailData.leaderName}"`; + mailData.description = `Your ${type} will help ${mailData.leaderName} to improve.`; + mailData.viewLeaderProfileHeader = `View ${mailData.leaderName} public profile?`; senderSuffix = template; - subject = `Thank you for your ${type}`; + subject = `${mailData.employeeName}, You’ve been heard. Leadership matters.`; break; } } diff --git a/imports/api/email/methods.js b/imports/api/email/methods.js index 60e5f63..ce02674 100644 --- a/imports/api/email/methods.js +++ b/imports/api/email/methods.js @@ -110,7 +110,6 @@ export const send = new ValidatedMethod({ case 'survey': { const options = EmailFunctions.getSurveyEmailOptions({template, data}); - console.log(options) Email.send(options); break; } diff --git a/imports/api/employees/register.server.js b/imports/api/employees/register.server.js new file mode 100644 index 0000000..9f5de42 --- /dev/null +++ b/imports/api/employees/register.server.js @@ -0,0 +1,2 @@ +import './methods'; +import './server/publications'; \ No newline at end of file diff --git a/imports/api/employees/server/publications.js b/imports/api/employees/server/publications.js new file mode 100644 index 0000000..1603899 --- /dev/null +++ b/imports/api/employees/server/publications.js @@ -0,0 +1,13 @@ +import {Meteor} from 'meteor/meteor'; +import {Employees} from '../index'; + +Meteor.publish('employees', function() { + if(!this.userId) { + return this.ready(); + } + + return Employees.find({leaderId: this.userId}, { + fields: Employees.publicFields + }); + +}); diff --git a/imports/api/feedbacks/register.server.js b/imports/api/feedbacks/register.server.js index e627d75..9f5de42 100644 --- a/imports/api/feedbacks/register.server.js +++ b/imports/api/feedbacks/register.server.js @@ -1 +1,2 @@ -import './methods'; \ No newline at end of file +import './methods'; +import './server/publications'; \ No newline at end of file diff --git a/imports/api/jobs/jobs.js b/imports/api/jobs/jobs.js index dd83468..60cadff 100644 --- a/imports/api/jobs/jobs.js +++ b/imports/api/jobs/jobs.js @@ -15,10 +15,17 @@ function createJob(type, attributes, data) { job = new Job(DailyJobs, type, data); break; } + case "measure_metric": { + job = new Job(DailyJobs, type, data); + break; + } case "send_surveys": { job = new Job(QueueJobs, type, data); break; } + default: { + return `Unknown job type: ${type}` + } } const currentDate = new Date(); const { diff --git a/imports/api/jobs/workers.js b/imports/api/jobs/workers.js index 777f62e..0882709 100644 --- a/imports/api/jobs/workers.js +++ b/imports/api/jobs/workers.js @@ -1,5 +1,7 @@ +import {Mongo} from 'meteor/mongo'; import {DailyJobs, QueueJobs} from './collections'; -import {words as capitalize} from 'capitalize'; +import moment from 'moment'; +import _ from 'lodash'; // collections import {Organizations} from '/imports/api/organizations/index'; @@ -11,8 +13,7 @@ import * as EmailActions from '/imports/api/email/methods'; import {getSendingPlans} from '/imports/api/sending_plans/methods'; import {getLocalDate} from '/imports/api/time/functions'; import {setStatus as setSendingPlanStatus} from '/imports/api/sending_plans/methods'; - - +import {measureMonthlyMetricScore} from '/imports/api/measures/methods'; // constants const LOG_LEVEL = { @@ -75,10 +76,11 @@ const enqueueSurveys = function (job, cb) { }; const attributes = { priority: "normal", - after: new Date(getLocalDate(date, timezone)) + after: new Date(getLocalDate(sendDate, timezone)) }; enqueue.call({type: "send_surveys", attributes, data: queueData}, (error) => { if (_.isEmpty(error)) { + setSendingPlanStatus.call({_id: planId, status: "QUEUED"}); jobMessage = `Enqueue mail ${metric} to ${employee.email} on ${date}`; job.log(jobMessage, {level: LOG_LEVEL.INFO}); } else { @@ -116,7 +118,6 @@ const sendSurveys = function (job, cb) { job.log(jobMessage, {level: LOG_LEVEL.WARNING}); job.done(); } else { - // console.log({employeeId, leaderId, organizationId, metric}); const template = 'survey'; const data = { planId, @@ -126,12 +127,11 @@ const sendSurveys = function (job, cb) { metric }; EmailActions.send.call({template, data}, (error) => { - if(_.isEmpty(error)) { + if (_.isEmpty(error)) { setSendingPlanStatus.call({_id: planId, status: "SENT"}); job.done(); } else { setSendingPlanStatus.call({_id: planId, status: "FAILED"}); - console.log(error); jobMessage = error.reason; job.log(jobMessage, {level: LOG_LEVEL.WARNING}); job.done(); @@ -145,13 +145,40 @@ const sendSurveys = function (job, cb) { } +const measureMetrics = (job, cb) => { + measureMonthlyMetricScore.call({params: {}}, (error, measure) => { + if (!error) { + if (!_.isEmpty(measure)) { + jobMessage = `measured metrics for ${measure.noOfLeader} leaders and ${measure.noOfOrg} organizations done`; + job.log(jobMessage, {level: LOG_LEVEL.INFO}); + job.done(); + } else { + jobMessage = `No data to measure for job: ${job}`; + job.log(jobMessage, {level: LOG_LEVEL.WARNING}); + job.done(); + } + } else { + job.log(error.reason, {level: LOG_LEVEL.CRITICAL}); + job.fail(); + } + }); +} + // Start Job function startJob(type) { - switch(type) { - case "enqueue_surveys": { + switch (type) { + // daily jobs + case "enqueue_surveys": + { DailyJobs.processJobs(type, enqueueSurveys); } - case "send_surveys": { + case "measure_metric": + { + DailyJobs.processJobs(type, measureMetrics); + } + // queue jobs + case "send_surveys": + { QueueJobs.processJobs(type, sendSurveys); } } diff --git a/imports/api/measures/collections.js b/imports/api/measures/collections.js new file mode 100644 index 0000000..f5b516c --- /dev/null +++ b/imports/api/measures/collections.js @@ -0,0 +1,5 @@ +import {Mongo} from 'meteor/mongo'; + +export default class MeasuresCollection extends Mongo.Collection { + +} \ No newline at end of file diff --git a/imports/api/measures/functions.js b/imports/api/measures/functions.js new file mode 100644 index 0000000..5eb9179 --- /dev/null +++ b/imports/api/measures/functions.js @@ -0,0 +1,168 @@ +import moment from 'moment'; +import _ from 'lodash'; + +// collections +import {Measures} from './index'; +import {Metrics} from '/imports/api/metrics/index'; + +// functions +import {arraySum} from '/imports/utils/index'; + +/** + * @summary this function used to insert/update the measure data which collect data for measurement + * @param {Object} data include leaderId, organizationId, type, interval, year, month, day, key, value + * @return {Number} the result of updating data into Measures Collection + */ +export const measure = ({data}) => { + const {leaderId, organizationId, type, interval, year, month, day, key, value} = data; + let + query = {}, + update = {}, + options = {} + ; + + switch (type) { + case "metric": + { + query = {leaderId, organizationId, type, interval, year, month, key}; + update = {$set: {value}}; + options = {upsert: true}; + break; + } + case "feedback": + { + + break; + } + default: + { + return `unknown type: ${type}`; + } + } + + return Measures.update(query, update, options); +}; + +/** + * @summary collect measure data of a month for metric + * @description collect all score in current month, + * @description get average score for every metric in every organization of every leader + * @return true if success, false if failed + */ +export const measureMonthlyMetricScore = () => { + // create mini mongo collection + const MiniMongo = new Mongo.Collection(null); + MiniMongo.remove({}); + const + // {} = job.data, + runDate = new Date(), + year = runDate.getFullYear(), + month = runDate.getMonth(), + nextMonth = month + 1 + ; + let + jobMessage = "", + selector = {}, + modifier = {}, + leaderList = [], + result = false + ; + + // Get list of leaders + selector = { + date: { + $gte: new Date(year, month, 1), + $lt: new Date(year, nextMonth, 1) + } + }; // only get data in current month + modifier = { + fields: { + _id: 0, + leaderId: 1, + organizationId: 1, + metric: 1, + score: 1 + } + }; // only return necessary fields + + // get leaders data in current month + const docs = Metrics.find(selector, modifier).fetch(); + if (!_.isEmpty(docs)) { + + docs.map(doc => { + MiniMongo.insert(doc); + leaderList.push(doc.leaderId); + }); + leaderList = _.uniq(leaderList); // get unique leader only + + // get average score for every leader + leaderList.map(leaderId => { + const leaderDocs = MiniMongo.find({leaderId}).fetch(); + let orgList = []; + + //get list of organization for specific leader + leaderDocs.map(leaderDoc => { + orgList.push(leaderDoc.organizationId); + orgList = _.uniq(orgList); + }); + // get list of metric for specific organization + orgList.map(organizationId => { + let + metricList = [], + orgDocs = MiniMongo.find({leaderId, organizationId}).fetch() + ; + orgDocs.map(orgDoc => { + metricList.push(orgDoc.metric); + metricList = _.uniq(metricList); + }); + // get list of score for specific metric + metricList.map(metric => { + const metricDocs = MiniMongo.find({leaderId, organizationId, metric}).fetch(); + let + scoreList = [], + averageScore = 0, + noOfScores = 0, + noOfGoodScores = 0, // count the number of score from 4 to 5 + noOfBadScores = 0, // count the number of score from 1 to 3 + measureDoc = {} // data of measure for leader + ; + + metricDocs.map(metricDoc => { + const {score} = metricDoc; + if(score > 3) { + noOfGoodScores++; + } else { + noOfBadScores++; + } + scoreList.push(score); + }); + + noOfScores = scoreList.length; + if(noOfScores > 0) { + averageScore = Number(arraySum(scoreList) / scoreList.length).toFixed(1); + } + + measureDoc = { + leaderId, + organizationId, + type: "metric", + interval: "monthly", + year, + month, + key: metric, + value: { + averageScore, + noOfScores, + noOfGoodScores, + noOfBadScores + } + }; + measure({data: measureDoc}); + }); + }); + }); + return true; + } else { + return false; + } +} diff --git a/imports/api/measures/index.js b/imports/api/measures/index.js new file mode 100644 index 0000000..f3e4daa --- /dev/null +++ b/imports/api/measures/index.js @@ -0,0 +1,62 @@ +import {SimpleSchema} from 'meteor/aldeed:simple-schema'; + +import MeasuresCollection from './collections'; + +export const Measures = new MeasuresCollection('measures'); + +Measures.publicFields = {}; + +Measures.schema = new SimpleSchema({ + leaderId: { + type: String + }, + organizationId: { + type: String + }, + type: { + type: String, + allowedValues: ["metric", "feedback"] + }, + interval: { + type: String, + allowedValues: ["daily", "weekly", "monthly"], + defaultValue: "monthly" + }, + year: { + type: Number, + optional: true + }, + month: { + type: Number, + optional: true + }, + day: { + type: Number, + optional: true + }, + key: { + type: String + }, + value: { + type: Object, + optional: true + }, + "value.averageScore": { + type: String, + optional: true + }, + "value.noOfScores": { + type: Number, + optional: true + }, + "value.noOfGoodScores": { + type: Number, + optional: true + }, + "value.noOfBadScores": { + type: Number, + optional: true + } +}); + +Measures.attachSchema(Measures.schema); \ No newline at end of file diff --git a/imports/api/measures/methods.js b/imports/api/measures/methods.js new file mode 100644 index 0000000..7450e4e --- /dev/null +++ b/imports/api/measures/methods.js @@ -0,0 +1,299 @@ +import {ValidatedMethod} from 'meteor/mdg:validated-method'; +import _ from 'lodash'; + +// collections +import {Measures} from './index'; +import {Metrics} from '/imports/api/metrics/index'; +import {MiniMongo} from '/imports/api/cache/index'; + +// functions +import {arraySum} from '/imports/utils/index'; +import {measure} from './functions'; + + +/** + * @summary get data for chart + * @description collect the last 6 months chart data + * @param {String} leaderId + * @param {String} organizationId + * @param {Date} date the date which is the last month of data + * @param {Number} noOfMonths the number of months which data will be returned + * @return {Object} data the chart data for 11 metrics and overall of them + */ +export const getChartData = new ValidatedMethod({ + name: "measure.getChartData", + validate: null, + run({leaderId, organizationId, date, noOfMonths}) { + const + months = [ + { + month: date.getMonth(), + name: moment(date).format('MMMM'), + year: date.getFullYear() + } + ] + ; + let + result = {}, + countChartData = 0 // used to check the final result of chart data + ; + + // Chart Data + result = { + label: [], + overall: [], + purpose: [], + mettings: [], + rules: [], + communications: [], + leadership: [], + workload: [], + energy: [], + stress: [], + decision: [], + respect: [], + conflict: [] + }; + + // chart data + for (var i = 0; i < noOfMonths; i++) { + const + previousMonth = new Date(moment().subtract(i, 'month')), + month = { + month: previousMonth.getMonth(), + name: moment(previousMonth).format('MMMM'), + year: previousMonth.getFullYear() + }; + let + overall = [], // overall data of metrics + totalScore = 0, // score for overall + noOfMetrics = 0, // the number of metrics in 1 month + metrics = { + label: "", + overall: 0, + purpose: 0, + mettings: 0, + rules: 0, + communications: 0, + leadership: 0, + workload: 0, + energy: 0, + stress: 0, + decision: 0, + respect: 0, + conflict: 0 + }, // current score of metrics + selector = {}, // conditions for query database + fields = {}, // fields will receive from database + MeasuresData = [] + ; + + // get labels + metrics.label = month.name; + + // get data + selector = { + leaderId, + organizationId, + type: "metric", + interval: "monthly", + year: month.year, + month: month.month + }; + fields = { + key: true, + value: true + }; + MeasuresData = Measures.find(selector, {fields}).fetch(); + totalScore = 0; + noOfMetrics = 0; + MeasuresData.map(measure => { + const + metric = measure.key, + score = Number(measure.value.averageScore) + ; + noOfMetrics++; + totalScore += score; + metrics[metric] = score; + }); + metrics.overall = (totalScore === 0) ? totalScore : totalScore / noOfMetrics; + metrics.overall = metrics.overall.toFixed(1); + + // add metrics value into result + for (var metric in metrics) { + result[metric].push(metrics[metric]); + } + } + // return empty if no chart data + countChartData = arraySum(result.overall); + if(countChartData === 0) { + result = []; + // these data used for testing + // result.label = ["April", "May", "June", "July", "August", "September"]; + // result.overall = [3.2, 4.0, 3.9, 4.9, 4.5, 4]; + // result.purpose = [2.2, 3.0, 4.9, 3.9, 5, 3]; + // result.mettings = [3.2, 3.0, 3.9, 4.9, 4, 4.3]; + // result.rules = [2.7, 4.6, 3.9, 3.2, 4, 3]; + // result.communications = [4.2, 2.0, 3.9, 4.9, 4, 4]; + // result.leadership = [3.2, 4.0, 3.9, 4.9, 4, 4]; + // result.workload = [3.2, 2.0, 3.9, 4.9, 2.3, 3]; + // result.energy = [2.7, 3.3, 4.6, 3.7, 4.5, 3.6]; + // result.stress = [3.3, 3.5, 4.2, 4.9, 5, 4]; + // result.decision = [2.6, 3.8, 4.2, 3.4, 3.4, 3.7]; + // result.respect = [4.2, 5.0, 3.9, 2.9, 4.5, 4]; + // result.conflict = [2.8, 2.0, 4.9, 4.9, 4.7, 4.4]; + } else { + // reorder the values + for (var e in result) { + result[e] = _.reverse(result[e]); + } + } + return result; + } +}); + +/** + * @summary Method measure monthly metric score + * @param {Object} params + * @param {String} params.leaderId + * @param {String} params.organizationId + * @param {Date} params.date - the date which is the last month of data + * @return {Number} the number of docs had been upsert + */ +export const measureMonthlyMetricScore = new ValidatedMethod({ + name: "measures.measureMonthlyMetricScore", + validate: null, + run({params}) { + const + MiniMongo = new Mongo.Collection(null), + runDate = (!!params.date ? params.date : new Date()), + year = runDate.getFullYear(), + month = runDate.getMonth(), + nextMonth = month + 1, + haveLeaderId = !!params.leaderId, + haveOrgId = !!params.organizationId + ; + let + jobMessage = "", + selector = {}, + modifier = {}, + leaderList = [], + leaderDocs = [], + orgList = [], + metricList = [], + orgDocs = [], + metricDocs = [], + scoreList = [], + averageScore = 0, + noOfScores = 0, + noOfGoodScores = 0, // count the number of score from 4 to 5 + noOfBadScores = 0, // count the number of score from 1 to 3 + measureDoc = {}, // data of measure for leader + result = false + ; + + // Get list of leaders + if(haveLeaderId) { + selector.leaderId = params.leaderId; + } + if(haveOrgId) { + selector.organizationId = params.organizationId; + } + selector.date = { + $gte: new Date(year, month, 1), + $lt: new Date(year, nextMonth, 1) + } + ; // only get data in current month + modifier = { + fields: { + _id: 0, + leaderId: 1, + organizationId: 1, + metric: 1, + score: 1 + } + }; // only return necessary fields + + // get leaders data in current month + const docs = Metrics.find(selector, modifier).fetch(); + if (!_.isEmpty(docs)) { + MiniMongo.remove({}); + docs.map(doc => { + MiniMongo.insert(doc); + if(!haveLeaderId) { + leaderList.push(doc.leaderId); + } + }); + + if(haveLeaderId) { + leaderList.push(params.leaderId); + } else { + leaderList = _.uniq(leaderList); // get unique leader only + } + + // get average score for every leader + leaderList.map(leaderId => { + //get list of organization for specific leader + if(haveOrgId) { + orgList.push(params.organizationId); + } else { + leaderDocs = MiniMongo.find({leaderId}).fetch(); + leaderDocs.map(leaderDoc => { + orgList.push(leaderDoc.organizationId); + }); + orgList = _.uniq(orgList); + } + + // get list of metric for specific organization + orgList.map(organizationId => { + orgDocs = MiniMongo.find({leaderId, organizationId}).fetch(); + orgDocs.map(orgDoc => { + metricList.push(orgDoc.metric); + }); + metricList = _.uniq(metricList); + // get list of score for specific metric + metricList.map(metric => { + metricDocs = MiniMongo.find({leaderId, organizationId, metric}).fetch(); + metricDocs.map(metricDoc => { + const {score} = metricDoc; + if(score > 3) { + noOfGoodScores++; + } else { + noOfBadScores++; + } + scoreList.push(score); + }); + + noOfScores = scoreList.length; + if(noOfScores > 0) { + averageScore = Number(arraySum(scoreList) / scoreList.length).toFixed(1); + } + + measureDoc = { + leaderId, + organizationId, + type: "metric", + interval: "monthly", + year, + month, + key: metric, + value: { + averageScore, + noOfScores, + noOfGoodScores, + noOfBadScores + } + }; + measure({data: measureDoc}); + }); + }); + }); + return { + noOfLeader: leaderList.length, + noOfOrg: orgList.length + }; + } else { + return {}; + } + } +}); \ No newline at end of file diff --git a/imports/api/measures/register.server.js b/imports/api/measures/register.server.js new file mode 100644 index 0000000..246aeff --- /dev/null +++ b/imports/api/measures/register.server.js @@ -0,0 +1,3 @@ +import './methods'; +import './functions'; +import './server/publications'; \ No newline at end of file diff --git a/imports/api/measures/server/publications.js b/imports/api/measures/server/publications.js new file mode 100644 index 0000000..e4eb538 --- /dev/null +++ b/imports/api/measures/server/publications.js @@ -0,0 +1,13 @@ +import {Meteor} from 'meteor/meteor'; + +// collections +import {Measures} from '../index'; + +Meteor.publish('measures', function() { + if(!this.userId) { + return this.ready(); + } + return Measures.find({leaderId: this.userId}, { + fields: Measures.publicFields + }); +}); \ No newline at end of file diff --git a/imports/api/metrics/functions.js b/imports/api/metrics/functions.js index 55c1981..899984e 100644 --- a/imports/api/metrics/functions.js +++ b/imports/api/metrics/functions.js @@ -7,6 +7,9 @@ import {Defaults} from '/imports/api/defaults/index'; import {send as sendEmail} from '/imports/api/email/methods'; import {add as addScore} from './methods'; +// functions +import {arrayAverage} from '/imports/utils/index'; + function onScoringFailed({planId, employeeId, leaderId, organizationId, metric}) { const template = 'survey_error'; const data = { @@ -82,3 +85,44 @@ export const scoringLeader = ({planId, employeeId, leaderId, organizationId, met return onScoringFailed({planId, employeeId, leaderId, organizationId, metric}); } } + +/** + * Function get average value of metrics for metric box + * @param {Array} data - an array of metrics object + * @return {Object} average value of metrics + */ +export const getAverageMetrics = (data) => { + let + metrics = { + overall: null, + purpose: null, + mettings: null, + rules: null, + communications: null, + leadership: null, + workload: null, + energy: null, + stress: null, + decision: null, + respect: null, + conflict: null + } + ; + + metrics = { + overall: arrayAverage(data.overall), + purpose: arrayAverage(data.purpose), + mettings: arrayAverage(data.mettings), + rules: arrayAverage(data.rules), + communications: arrayAverage(data.communications), + leadership: arrayAverage(data.leadership), + workload: arrayAverage(data.workload), + energy: arrayAverage(data.energy), + stress: arrayAverage(data.stress), + decision: arrayAverage(data.decision), + respect: arrayAverage(data.respect), + conflict: arrayAverage(data.conflict) + } + + return metrics; +} \ No newline at end of file diff --git a/imports/api/metrics/methods.js b/imports/api/metrics/methods.js index bed283a..70b9986 100644 --- a/imports/api/metrics/methods.js +++ b/imports/api/metrics/methods.js @@ -41,46 +41,4 @@ export const checkExists = new ValidatedMethod({ return false; } } -}); - -/** - * @summary Get metrics score of month - * @params leaderId, month, year - * @return {metrics, month, year} - */ -export const getMetricsOfMonth = new ValidatedMethod({ - name: "metrics.getMetrics", - validate: null, - run({leaderId, month, year}) { - // const year = date.getFullYear(); - // const month = date.getMonth(); - // const day = date.getDate(); - const nextMonth = month + 1; - const selector = {leaderId, date: {$gte: new Date(year, month, 1), $lt: new Date(year, nextMonth, 1)}}; - const modifier = {}; - - const metrics = Metrics.find(selector).fetch(); - let result = {}; - let metricsScores = {}; - if(!_.isEmpty(metrics)) { - // initiate result - for(var i in DEFAULT_METRICS) { - metricsScores[DEFAULT_METRICS[i]] = []; - } - // get result - metrics.map(metric => { - metricsScores[metric.name].push(metric.score); - }); - for(var i in DEFAULT_METRICS) { - var metric = DEFAULT_METRICS[i]; - if(!_.isEmpty(metricsScores[metric])) { - const averageScore = arraySum(metricsScores[metric]) / metricsScores[metric].length; - result[metric] = Number(averageScore.toFixed(1)); - } - } - } else { - return []; - } - return result; - } -}); +}); \ No newline at end of file diff --git a/imports/api/metrics/register.server.js b/imports/api/metrics/register.server.js index bc7bdbf..246aeff 100644 --- a/imports/api/metrics/register.server.js +++ b/imports/api/metrics/register.server.js @@ -1,2 +1,3 @@ import './methods'; -import './functions'; \ No newline at end of file +import './functions'; +import './server/publications'; \ No newline at end of file diff --git a/imports/api/organizations/methods.js b/imports/api/organizations/methods.js index 37d83d5..6ca522c 100644 --- a/imports/api/organizations/methods.js +++ b/imports/api/organizations/methods.js @@ -3,7 +3,6 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidationError } from 'meteor/mdg:validation-error'; import { SimpleSchema } from 'meteor/aldeed:simple-schema'; import _ from 'lodash'; -import moment from 'moment'; import { Organizations } from './index'; import { Employees, STATUS_ACTIVE, STATUS_DEACTIVE } from '/imports/api/employees'; @@ -458,3 +457,20 @@ export const removeEmployee = new ValidatedMethod({ return Employees.remove({ _id: employeeId }); } }); + +/** + * Method get present organizations + * @return {Array} list of present organizationId ordered by the latest startTime + */ +export const getPresentOrganizations = new ValidatedMethod({ + name: "organization.getPresentOrganizations", + validate: null, + run({leaderId, isPresent}) { + const + query = {leaderId, isPresent, status: "ACTIVE"}, + projection = {name: 1, jobTitle: 1, description: 1} + ; + + return Organizations.find(query, {projection}).fetch(); + } +}); diff --git a/imports/api/profiles/methods.js b/imports/api/profiles/methods.js index 4678425..b1b08e6 100644 --- a/imports/api/profiles/methods.js +++ b/imports/api/profiles/methods.js @@ -7,22 +7,23 @@ import _ from 'lodash'; // collections import {Profiles, STATUS_ACTIVE, STATUS_INACTIVE} from './index'; import {Organizations} from '/imports/api/organizations/index'; -import {Employees} from '/imports/api/employees/index'; import {Industries} from '/imports/api/industries/index'; import {Preferences} from '/imports/api/users/index'; +import {Feedbacks} from '/imports/api/feedbacks/index'; import {IDValidator} from '/imports/utils'; import {DEFAULT_PUBLIC_INFO_PREFERENCES} from '/imports/utils/defaults'; // methods import {addPreferences} from '/imports/api/users/methods'; -import {getMetricsOfMonth} from '/imports/api/metrics/methods'; +import {getChartData} from '/imports/api/measures/methods'; // functions -import {addMonths} from '/imports/utils/index'; +import {getAverageMetrics} from '/imports/api/metrics/functions'; // constants import * as ERROR_CODE from '/imports/utils/error_code'; +import {DEFAULT_PROFILE_PHOTO} from '/imports/utils/defaults'; /** * CUD user profiles (Create, Update, Deactivate) @@ -52,8 +53,8 @@ export const create = new ValidatedMethod({ } }).validator(), run({userId, firstName, lastName, timezone}) { - // console.log({userId, firstName, lastName}); - return Profiles.insert({userId, firstName, lastName, timezone}); + const imageUrl = DEFAULT_PROFILE_PHOTO; + return Profiles.insert({userId, firstName, lastName, timezone, imageUrl}); } }); @@ -208,9 +209,14 @@ export const setStatus = new ValidatedMethod({ } }); -// get public information +/** + * @summary collect public data with the customization from user + * @param {String} alias + * @param {Boolean} isGetAll flag for getting all data or just the customization + * @return {Object} all data for public profile + */ export const getPublicData = new ValidatedMethod({ - name: 'profiles.getPublicData', + name: "profiles.getPublicData", validate: new SimpleSchema({ alias: { type: String @@ -221,252 +227,181 @@ export const getPublicData = new ValidatedMethod({ }).validator(), run({alias, isGetAll}) { if (!this.isSimulation) { - const user = Accounts.findUserByUsername(alias); - if (!_.isEmpty(user)) { - if (Preferences.find({userId: user._id, name: 'publicInfo'}).count() == 0) { + const User = Accounts.findUserByUsername(alias); + if (!_.isEmpty(User)) { + const + userId = User._id, + leaderId = User._id, + noOfPreferences = Preferences.find({userId, name: 'publicInfo'}).count(), + ProfileData = Profiles.findOne({userId}), + OrganizationsData = _.orderBy(Organizations.find({leaderId}).fetch(), ['isPresent','startTime'], ['desc','desc']), + FeedbacksData = Feedbacks.find({leaderId}).fetch(), + date = new Date(), + months = [ + { + month: date.getMonth(), + name: moment(date).format('MMMM'), + year: date.getFullYear() + } + ] + ; + let + result = { + basic: { + name: null, + industry: null + }, + headline: { + title: null + }, + contact: { + phone: null, + email: null + }, + summary: { + noOrg: null, + noEmployees: null, + noFeedbacks: null + }, + picture: { + imageUrl: null + }, + about: { + aboutMe: null + }, + organizations: [], + chart: {}, + metrics: { + overall: null, + purpose: null, + mettings: null, + rules: null, + communications: null, + leadership: null, + workload: null, + energy: null, + stress: null, + decision: null, + respect: null, + conflict: null + }, + preferences: {} + }, + PreferencesData = {} + ; + + // add default preferences for user if don't have + // + if (noOfPreferences == 0) { addPreferences.call({name: 'publicInfo', preferences: DEFAULT_PUBLIC_INFO_PREFERENCES}); } - let result = { - basic: { - name: null, - industry: null - }, - headline: { - title: null - }, - contact: { - phone: null, - email: null - }, - summary: { - noOrg: null, - noEmployees: null, - noFeedbacks: null - }, - picture: { - imageUrl: null - }, - about: { - aboutMe: null - }, - organizations: [], - chart: { - label: [], - overall: [], - purpose: [], - mettings: [], - rules: [], - communications: [], - leadership: [], - workload: [], - energy: [], - stress: [], - decision: [], - respect: [], - conflict: [] - }, - metrics: { - overall: null, - purpose: null, - mettings: null, - rules: null, - communications: null, - leadership: null, - workload: null, - energy: null, - stress: null, - decision: null, - respect: null, - conflict: null - }, - preferences: {} - }; - // Get basic info - always show + // Get customize info, + // which could be showed all (isGetAll = true) + // or customized data only + if(isGetAll) { + PreferencesData = DEFAULT_PUBLIC_INFO_PREFERENCES; + } else { + PreferencesData = Preferences.find({userId: User._id}).fetch()[0].preferences; + result.preferences = PreferencesData; + } + // Get preferences data + const {headline, contact, summary, picture, about, organizations, metrics} = PreferencesData; + + // Get basic info, which always are showed // name - const profile = Profiles.findOne({userId: user._id}); - if (!!profile.firstName || !!profile.lastName) { - result.basic.name = `${profile.firstName} ${profile.lastName}`; + if (!!ProfileData.firstName || !!ProfileData.lastName) { + result.basic.name = `${ProfileData.firstName} ${ProfileData.lastName}`; } // industry - if (!!profile.industries) { - result.basic.industry = Industries.findOne({_id: {$in: profile.industries}}).name; + if (!!ProfileData.industries) { + result.basic.industry = Industries.findOne({_id: {$in: ProfileData.industries}}).name; } - // others - if (Preferences.find({userId: user._id}).count() > 0) { - let preferences = {}; - if (isGetAll) { - preferences = DEFAULT_PUBLIC_INFO_PREFERENCES; - } else { - preferences = Preferences.find({userId: user._id}).fetch()[0].preferences; - result.preferences = preferences; - } - // Get preferences - const {headline, contact, summary, picture, about, organizations} = preferences; - // Get headline info - // title - if (headline.title && typeof headline.title !== 'undefined') { - result.headline.title = !!profile.title ? profile.title : null; - } + // get public data base on preferences + // Headline + if (headline.title && typeof headline.title !== 'undefined') { + result.headline.title = !!ProfileData.title ? ProfileData.title : null; + } - // Get contact info - // phoneNumber - if (contact.phone && typeof contact.phone !== 'undefined') { - result.contact.phone = !!profile.phoneNumber ? profile.phoneNumber : null; - } - // email - if (contact.email && typeof contact.email !== 'undefined') { - result.contact.email = user.emails[0].address; - } + // Contact + // phoneNumber + if (contact.phone && typeof contact.phone !== 'undefined') { + result.contact.phone = !!ProfileData.phoneNumber ? ProfileData.phoneNumber : null; + } + // email + if (contact.email && typeof contact.email !== 'undefined') { + result.contact.email = User.emails[0].address; + } - // Get summary info - // noOrg - if (summary.noOrg && typeof summary.noOrg !== 'undefined') { - const noOrg = Organizations.find({leaderId: user._id}).count(); - result.summary.noOrg = !!noOrg ? noOrg : null; - } - // noEmployees - if (summary.noEmployees && typeof summary.noEmployees !== 'undefined') { - if (Organizations.find({leaderId: user._id}).count() > 0) { - const modifier = { - fields: {employees: true} - }; - const employeesList = Organizations.find({leaderId: user._id}, modifier).fetch(); - let noEmployees = 0; - employeesList.map(employees => { - noEmployees += employees.employees.length; - }); - result.summary.noEmployees = noEmployees; - } - } - // noFeedbacks - if (summary.noFeedbacks && typeof summary.noFeedbacks !== 'undefined') { - result.summary.noFeedbacks = 240; + // Summary + // noOrg + if (summary.noOrg && typeof summary.noOrg !== 'undefined') { + const noOrg = OrganizationsData.length; + result.summary.noOrg = (noOrg > 0) ? noOrg : null; + } + // noEmployees + if (summary.noEmployees && typeof summary.noEmployees !== 'undefined') { + if (OrganizationsData.length > 0) { + let noEmployees = 0; + OrganizationsData.map(organization => { + noEmployees += organization.employees.length; + }); + result.summary.noEmployees = (noEmployees > 0) ? noEmployees : null; } + } + // noFeedbacks + if (summary.noFeedbacks && typeof summary.noFeedbacks !== 'undefined') { + const noFeedbacks = FeedbacksData.length; + result.summary.noFeedbacks = (noFeedbacks > 0) ? noFeedbacks : null; + } - // Get picture - if (picture.imageUrl && typeof picture.imageUrl !== 'undefined') { - result.picture.imageUrl = !!profile.imageUrl ? profile.imageUrl : null; - } + // Picture + if (picture.imageUrl && typeof picture.imageUrl !== 'undefined') { + result.picture.imageUrl = !!ProfileData.imageUrl ? ProfileData.imageUrl : null; + } - // Get about info - // AboutMe - if (about.aboutMe && typeof about.aboutMe !== 'undefined') { - result.about.aboutMe = !!profile.aboutMe ? profile.aboutMe : null; - } + // About + // AboutMe + if (about.aboutMe && typeof about.aboutMe !== 'undefined') { + result.about.aboutMe = !!ProfileData.aboutMe ? ProfileData.aboutMe : null; + } - // Get Organizations - if (organizations.show) { - if (Organizations.find({leaderId: user._id}).count() > 0) { - const modifier = { - fields: { - name: true, - startTime: true, - endTime: true, - jobTitle: true, - isPresent: true, - employees: true, - imageUrl: true - }, - sort: {startTime: -1} - }; - const orgInfo = Organizations.find({leaderId: user._id}, modifier).fetch(); - result.organizations = !_.isEmpty(orgInfo) ? orgInfo : []; - } + // Organizations + if (organizations.show) { + if (OrganizationsData.length > 0) { + result.organizations = !_.isEmpty(OrganizationsData) ? OrganizationsData : []; } + } else { + result.organizations = []; + } - // Get chart info - // result.chart = { - // label: [], - // overall: [], - // purpose: [], - // mettings: [], - // rules: [], - // communications: [], - // leadership: [], - // workload: [], - // energy: [], - // stress: [], - // decision: [], - // respect: [], - // conflict: [] - // }; - // const date = new Date(); - // const months = [ - // { - // month: date.getMonth(), - // name: moment(date).format('MMMM'), - // year: date.getFullYear() - // } - // ]; - // for (var i = 1; i < 6; i++) { - // var previousMonth = new Date(moment().subtract(i, 'month')); - // var element = { - // month: previousMonth.getMonth(), - // name: moment(previousMonth).format('MMMM'), - // year: previousMonth.getFullYear() - // }; - // months.push(element); - // } - // - // for(var i in months) { - // let data = []; - // result.chart.label.push(months[i].name); - // getMetricsOfMonth.call({leaderId, month, year}, (error, result) => { - // if(!error) { - // console.log(result) - // } - // }); - // } - // months.map(monthData => { - // const leaderId = user._id; - // const {month, year} = monthData; - // getMetricsOfMonth.call({leaderId, month, year}, (error, result) => { - // if(!error) { - // console.log(result) - // } - // }); - // }); - - result.chart.label = ["February", "March", "April", "May", "June", "July"]; - result.chart.overall = [3.2, 4.0, 3.9, 4.9, 4.5, 4]; - result.chart.purpose = [2.2, 3.0, 4.9, 3.9, 5, 3]; - result.chart.mettings = [3.2, 3.0, 3.9, 4.9, 4, 4.3]; - result.chart.rules = [2.7, 4.6, 3.9, 3.2, 4, 3]; - result.chart.communications = [4.2, 2.0, 3.9, 4.9, 4, 4]; - result.chart.leadership = [3.2, 4.0, 3.9, 4.9, 4, 4]; - result.chart.workload = [3.2, 2.0, 3.9, 4.9, 2.3, 3]; - result.chart.energy = [2.7, 3.3, 4.6, 3.7, 4.5, 3.6]; - result.chart.stress = [3.3, 3.5, 4.2, 4.9, 5, 4]; - result.chart.decision = [2.6, 3.8, 4.2, 3.4, 3.4, 3.7]; - result.chart.respect = [4.2, 5.0, 3.9, 2.9, 4.5, 4]; - result.chart.conflict = [2.8, 2.0, 4.9, 4.9, 4.7, 4.4]; + // Chart + if(OrganizationsData.length > 0) { + getChartData.call({ + leaderId, + organizationId: OrganizationsData[0]._id, + date: new Date(), noOfMonths: 6 + }, (error, chartData) => { + if(!error) { + result.chart = chartData; + } else { + console.log(error) + } + }); + } else { + result.chart = []; + } - // Get metrics - const userId = user._id; - // modifier for finding public metrics - const metricsModifier = { - fields: preferences.metrics - }; - result.metrics = { - overall: 4.4, - purpose: 3.6, - mettings: 4.7, - rules: 5, - communications: 4.2, - leadership: 3.9, - workload: 2.5, - energy: 3.8, - stress: 3.7, - decision: 4.2, - respect: 4, - conflict: 4.9 - }; + // Metrics + if(!_.isEmpty(result.chart)) { + result.metrics = getAverageMetrics(result.chart); } - // console.log(result) + return result; + } else { + return Meteor.Error(ERROR_CODE.RESOURCE_NOT_FOUND); } } } diff --git a/imports/api/scheduler/collection.js b/imports/api/scheduler/collection.js index 4793b3d..e618336 100644 --- a/imports/api/scheduler/collection.js +++ b/imports/api/scheduler/collection.js @@ -3,7 +3,16 @@ import { SendingPlans } from '/imports/api/sending_plans'; import * as schedulerUtils from '/imports/utils/scheduler'; export default class SchedulerCollection extends Mongo.Collection { - + insert(doc, callback) { + const + _id = super.insert(doc, callback), + data = this.findOne({_id}) + ; + if(_id) { + this.updateSendingPlan(data) + } + return _id; + } update(selector, modifier) { const before = this.findOne(selector); @@ -27,6 +36,8 @@ export default class SchedulerCollection extends Mongo.Collection { status: 'READY' }); + if(_.isEmpty(doc.metrics)) return; + // generate new sending plan const leaderId = Meteor.userId(); const schedulerId = doc._id; diff --git a/imports/api/scheduler/index.js b/imports/api/scheduler/index.js index 0c7ee27..6f0c904 100644 --- a/imports/api/scheduler/index.js +++ b/imports/api/scheduler/index.js @@ -111,13 +111,12 @@ Scheduler.schema = new SimpleSchema({ }, metrics: { type: [String], - minCount: 1, - maxCount: 3, allowedValues: [ METRICS.PURPOSE, METRICS.MEETINGS, METRICS.RULES, METRICS.COMMUNICATIONS, METRICS.LEADERSHIP, METRICS.WORKLOAD, METRICS.ENERGY, METRICS.STRESS, METRICS.DECISION, METRICS.RESPECT, METRICS.CONFLICT ], + optional: true }, year: { type: Number diff --git a/imports/api/sending_plans/index.js b/imports/api/sending_plans/index.js index 55590f6..05b1d8a 100644 --- a/imports/api/sending_plans/index.js +++ b/imports/api/sending_plans/index.js @@ -43,7 +43,7 @@ SendingPlans.schema = new SimpleSchema({ }, status: { type: String, - allowedValues: ['READY', 'SENT', 'FAILED'] + allowedValues: ['READY', 'SENT', 'FAILED', 'QUEUED'] } }); diff --git a/imports/api/sending_plans/methods.js b/imports/api/sending_plans/methods.js index c852bf8..70f625f 100644 --- a/imports/api/sending_plans/methods.js +++ b/imports/api/sending_plans/methods.js @@ -60,12 +60,24 @@ export const getSendingPlans = new ValidatedMethod({ name: "sendingPlans.getSendingPlans", validate: null, run({date}) { - const year = date.getFullYear(); - const month = date.getMonth(); - const day = date.getDate(); - const nextDay = date.getDate() + 1; - const selector = {sendDate: {$gte: new Date(year, month, day), $lt: new Date(year, month, nextDay)}, status: "READY"}; - const modifier = {}; + const year = date.getFullYear(), + month = date.getMonth(), + day = date.getDate(), + nextDay = date.getDate() + 1, + selector = {sendDate: {$gte: new Date(year, month, day), $lt: new Date(year, month, nextDay)}, status: "READY"}, + modifier = {}; return SendingPlans.find(selector).fetch(); } +}); + +export const getLeaderPlans = new ValidatedMethod({ + name: "sendingPlans.getLeaderPlans", + validate: null, + run() { + const + leaderId = Meteor.userId() + ; + + return SendingPlans.find({leaderId}).fetch(); + } }); \ No newline at end of file diff --git a/imports/api/users/methods.js b/imports/api/users/methods.js index 4a09b38..31f1cad 100644 --- a/imports/api/users/methods.js +++ b/imports/api/users/methods.js @@ -153,13 +153,14 @@ export const confirm = new ValidatedMethod({ // verify Token const token = Tokens.findOne({_id: tokenId}); if (!_.isEmpty(token)) { - const email = token.email; - const user = Accounts.findUserByEmail(email); + const + email = token.email, + user = Accounts.findUserByEmail(email); if (!_.isEmpty(user)) { - const userId = user._id; + const {_id} = user; // Activate user Meteor.users.update({ - userId, + _id, emails: { $elemMatch: {address: email} } @@ -168,7 +169,7 @@ export const confirm = new ValidatedMethod({ "emails.$.verified": true } }); - ProfileActions.setStatus.call({userId, status: STATUS_ACTIVE}); + ProfileActions.setStatus.call({userId: _id, status: STATUS_ACTIVE}); } } else { throw new Meteor.Error('invalid-token', 'User token is invalid or has been used.'); diff --git a/imports/startup/client/routes.js b/imports/startup/client/routes.js index 4df78e4..acb8e58 100644 --- a/imports/startup/client/routes.js +++ b/imports/startup/client/routes.js @@ -32,7 +32,6 @@ import Dashboard from '/imports/ui/containers/dashboard/Dashboard'; import Organizations from '/imports/ui/containers/organizations/Organizations'; import CreateOrganization from '/imports/ui/containers/organizations/CreateOrganization'; import UpdateOrganization from '/imports/ui/containers/organizations/UpdateOrganization'; -import Employees from '/imports/ui/containers/employees/Employees'; // methods import * as Notifications from '/imports/api/notifications/methods'; @@ -189,7 +188,8 @@ signInRoutes.route('/:action', { } // sign in to user's account if (params.action == 'account') { - if (Meteor.isLoggingIn || Meteor.userId()) { + if (Meteor.loggingIn() || Meteor.userId()) { + console.log({logingIn: Meteor.loggingIn(), userId: Meteor.userId()}) FlowRouter.go('app.dashboard'); } else { mount(SignInAccount); @@ -389,17 +389,3 @@ appRoutes.route('/organizations/update/:_id', { }) } }); - -/** - * Route for manage employees - */ -appRoutes.route('/employees', { - name: 'app.employees', - action(params) { - mount(MainLayout, { - content() { - return - } - }) - } -}); diff --git a/imports/startup/server/fixtures.js b/imports/startup/server/fixtures.js index d979ccd..046e9d6 100644 --- a/imports/startup/server/fixtures.js +++ b/imports/startup/server/fixtures.js @@ -172,7 +172,7 @@ export function createDefaults() { mettings: `There are few signs that shows teams are drifting. One of the main indicators is meetings. You leave meetings feeling like they’ve been a waste of time, or you decide to stop having team meetings because they’re not productive anymore.`, rules: `Ground rules are an important tool for helping individuals function together as a team. They reflect what is important to the members about how they work together.Ground rules should focus on three elements: Tasks – Expected activities and deliverables for the team; Process – How the activities will be carried out; and Norms – Ways in which team members will interact with each other.`, communications: `It is simply impossible to become a great leader without being a great communicator.`, - leadershiop: `It can be hard to define and it means different things to different people. This is why it is important to be measured in point of view of your team members, are you leading them properly?`, + leadership: `It can be hard to define and it means different things to different people. This is why it is important to be measured in point of view of your team members, are you leading them properly?`, workload: `As the leader of a high-performing team, how you distribute and balance work across the members of that team is a critical success factor. It needs to be done fairly. Note, I didn't say equally.`, energy: `Commitment of a leader inspires and motivates the followers and helps them to be more stronger towards the purpose and vision of the organization and team.`, stress: `It is acceptable to have stress in the business in fact today's business is very stressful. However a good leader must be able to manage the stress in order to ensure the team is performing in their best focus.`, diff --git a/imports/startup/server/index.js b/imports/startup/server/index.js index b1ff2e6..486dbc1 100644 --- a/imports/startup/server/index.js +++ b/imports/startup/server/index.js @@ -9,6 +9,8 @@ import {Jobs} from '/imports/api/jobs/jobs'; import {Workers} from '/imports/api/jobs/workers'; Meteor.startup(function () { + let type = ""; + process.env.MAIL_URL = Meteor.settings.MAILGUN_URL; // process.env.MAIL_URL = 'smtp://postmaster%40mail.theleader.io:04e27c0e13cbd45254e7aff7b4ed946a@smtp.mailgun.org:587'; process.env.ROOT_URL = Meteor.settings.public.ROOT_URL; @@ -18,21 +20,41 @@ Meteor.startup(function () { DailyJobs.startJobServer(); QueueJobs.startJobServer(); - // create & start daily job + /** + * DailyJobs + * @job: enqueue_surveys + * @job: measure metric + */ // sending survey email job - if(!DailyJobs.find({type: "enqueue_surveys"}).count()) { - const type = "enqueue_surveys"; + type = "enqueue_surveys"; + let attributes = {}; + if(Meteor.settings.public.env === "dev") { + console.log(`dev environment`) + attributes = {priority: "normal", repeat: {schedule: later.parse.text("every 5 minutes")}}; + } else { + attributes = {priority: "normal", repeat: {schedule: later.parse.text(Meteor.settings.jobs.runTime.metricEmailSurvey)}}; + } + var data = {type}; + Jobs.create(type, attributes, data); + } + // measure score of metric job + if(!DailyJobs.find({type: "measure_metric"}).count()) { + type = "measure_metric"; let attributes = {}; if(Meteor.settings.public.env === "dev") { console.log(`dev environment`) - attributes = {priority: "high", repeat: {schedule: later.parse.text("every 5 minutes")}}; + attributes = {priority: "normal", repeat: {schedule: later.parse.text("every 5 minutes")}}; } else { - attributes = {priority: "high", repeat: {schedule: later.parse.text(Meteor.settings.jobs.runTime.metricEmailSurvey)}}; + attributes = {priority: "normal", repeat: {schedule: later.parse.text(Meteor.settings.jobs.runTime.measureMetric)}}; } const data = {type}; Jobs.create(type, attributes, data); - Workers.start(type); } + type = "enqueue_surveys"; + Workers.start(type); + type = "measure_metric"; + Workers.start(type); + }); \ No newline at end of file diff --git a/imports/store/index.js b/imports/store/index.js index 890115b..dd74423 100644 --- a/imports/store/index.js +++ b/imports/store/index.js @@ -14,7 +14,7 @@ export const createStore = () => { return new Promise((resolve, reject) => { // Apply middlewares - const middlewares = [thunk, logger]; + const middlewares = [thunk]; Meteor.AppState = new AppState({ middlewares }); // register modules diff --git a/imports/ui/common/Navigation.js b/imports/ui/common/Navigation.js index 5040e50..ad293b6 100644 --- a/imports/ui/common/Navigation.js +++ b/imports/ui/common/Navigation.js @@ -11,9 +11,6 @@ class Navigation extends Component { {route: 'app.dashboard', path: FlowRouter.url('app.dashboard'), label: 'Dashboard', icon: 'fa fa-dashboard'}, {route: 'app.preferences', path: FlowRouter.url('app.preferences'), label: 'Preferences', icon: 'fa fa-gears'}, {route: 'app.organizations', path: FlowRouter.url('app.organizations'), label: 'Organizations', icon: 'fa fa-sitemap'}, - {route: 'app.employees', path: FlowRouter.url('app.employees'), label: 'Employees', icon: 'fa fa-users'}, - {route: '', path: '', label: 'Feedback', icon: 'fa fa-gift'}, - {route: '', path: '', label: 'Measure', icon: 'fa fa-info'}, ] }; } diff --git a/imports/ui/common/TopNav.js b/imports/ui/common/TopNav.js index 4170f93..9a8cd4d 100644 --- a/imports/ui/common/TopNav.js +++ b/imports/ui/common/TopNav.js @@ -14,7 +14,7 @@ class TopNav extends Component { const {userProfile} = this.props; let profilePhoto = DEFAULT_PROFILE_PHOTO; if(!_.isEmpty(userProfile)) { - profilePhoto = userProfile.imageUrl; + profilePhoto = userProfile.getPicture(); } else { profilePhoto = this.props.imageUrl; } @@ -48,8 +48,9 @@ class TopNav extends Component { diff --git a/imports/ui/components/EmptyBox.js b/imports/ui/components/EmptyBox.js new file mode 100644 index 0000000..c0e7be6 --- /dev/null +++ b/imports/ui/components/EmptyBox.js @@ -0,0 +1,33 @@ +import React, {Component} from 'react'; + +export default class EmptyBox extends Component { + render() { + const styles = { + container: { + width: '100%', + height: '300px', + border: '2px dashed #ddd', + borderRadius: '6px', + textAlign: 'center', + margin: '30px 0', + padding: '40px' + }, + msg: { + textAlign: 'center' + }, + icon: { + fontSize: 40, + color: '#ddd' + } + }, + {icon, message} = this.props + ; + return ( +
+ +

{message}

+
+ ); + } +} + diff --git a/imports/ui/components/FormInput.js b/imports/ui/components/FormInput.js index 9736964..e209a58 100644 --- a/imports/ui/components/FormInput.js +++ b/imports/ui/components/FormInput.js @@ -14,29 +14,35 @@ class FormInput extends Component { static defaultProps = { label: '', + type: 'text', defaultValue: '', value: '', placeholder: '', error: '', disabled: false, multiline: false, + required: false, + autoFocus: false, onChangeText: () => null, }; render() { const { label, + type, value, placeholder, error, disabled, multiline, onChangeText, + required, + autoFocus } = this.props; return (
- + {(label !== '') && ()} { multiline ? (