timecop.js

var libxmljs = require("libxmljs"),
	neuron = require('neuron'),
	utile = require('utile'),
	nconf = require('nconf'),
	https = require('https'),
	nodemailer = require("nodemailer"),
	cronJob = require("cron").CronJob,
	plates = require("plates"),
	b64 = require("b64"),
	daemon = require('daemon'),
	xmlDoc, 
	timecop = {},
	pid;
	

Version 0.0.1

timecop.version = [0, 0, 1];

load our config file

nconf.argv().file({ file: './config.json' });

Run timecop as a deamon

if (nconf.get('mode') === "daemon") {
	pid = daemon.start('./timecop_logs/stdout.log', './timecop_logs/stderr.log');
	daemon.lock('/tmp/timecop.pid');
}

This is the default value for the max # of hours the user can fall behind before being notified.

timecop.maxGap = 0;

var manager = new neuron.JobManager();

Retrieve a list of all users from Traffic

manager.addJob('checkHours', {

	work: function ( nconf, maxGap ) {
		
		var now = new Date();
		var self = this;
		timecop.maxGap = maxGap;
		
		var req = https.request({
			host: "production-sohnar.apigee.com", 
			headers: {
				"Accept": "application/xml", 
				"Authorization": "Basic " + b64.encode(nconf.get("email")+":"+nconf.get("apiToken"))},

If you have more than 500 employees change this. We have less than 50 so we haven't tested on bigger installs.

				"path": "/TrafficLiteServer/openapi/staff/employee?windowSize=500",
				"port": 443,
				"method": 'GET'}, 
			
			function(res) {
				var body = '';
				
				res.on('data', function (chunk) {
					body+=chunk;
				});
				
				res.on('end', function () {
					

load response as XML document

					xmlDoc = libxmljs.parseXmlString(body);
					

Get a node list of the employees

					var Employees = xmlDoc.find("/pagedResult/trafficEmployee");
					

Loop through the employee nodes

					for (i = 0, j = Employees.length; i<j; i++) {
						
						var available = 0,
							now = new Date(),
							hpd = Number(Employees[i].find("employeeDetails/hoursWorkedPerDayMinutes")[0].text());
						

Loop through all the days of this month. If it is a weekday, increase the hours the employee was available by hoursWorkedPerDayMinutes value from traffic

						for (k = 1, l = now.getDate(); k < l; k++) {
							var thisDate = new Date(now.getFullYear(), now.getMonth(), k);
							if (thisDate.getDay() > 0 && thisDate.getDay() < 6) {
								available += hpd;
							}
						}
						

Create some nodes we will use later

						var newBillable = new libxmljs.Element(xmlDoc, "billable", 0),
							newMinutes = new libxmljs.Element(xmlDoc, "minutes", 0),
							newAvailable = new libxmljs.Element(xmlDoc, "available", String(available));
											

Add the nodes to our XML document

						Employees[i].addChild(newBillable);
						Employees[i].addChild(newMinutes);
						Employees[i].addChild(newAvailable);
						
					}
									
					manager.enqueue('getTimeEntries' );
					self.finished = true;
				});
				
			}
		);
		
		req.end();
		
		req.on('error', function(e) {
			console.error(e);
		});		
		
	}
});

Get all all the days for the current month

manager.addJob('getTimeEntries', {
	work: function ( ) {
		
		var self = this,
			now = new Date(),
			counter = now.getDate();
							

Loop thorugh all the days so far this month

		for (var i = 1, j = now.getDate(); i <= j; i++) {
				manager.enqueue('getDay', i);				
		}
		
		self.finished = true;
		
	}
});

Get all the time entries for this day

manager.addJob('getDay', {
	work: function ( day ) {
		var now = new Date(),
			self = this;

Request all the time entries for this day (max=999). If your company has more than 999 time entries per day open an issue on github and we'll think about it.

		var req = https.request({host: "production-sohnar.apigee.com", headers: {"Accept": "application/xml", "Authorization": "Basic " + b64.encode(nconf.get("email")+":"+nconf.get("apiToken")) },path: "/TrafficLiteServer/openapi/timeentries?startDate=" + now.getFullYear() + "-" + timecop.twoDigit(now.getMonth()+1) + "-" + timecop.twoDigit(day) + "&endDate=" + now.getFullYear() + "-" + timecop.twoDigit(now.getMonth()+1) + "-" + timecop.twoDigit(day + 1) +"&windowSize=999",port:443,method:'GET'}, 
			function(res) {
				
				var body = '',
					thisDate = new Date(now.getFullYear(), now.getMonth(), day);
				
				res.on('data', function (chunk) {
					body+=chunk;
				});
				
				res.on('end', function () {
					

Load the XML

					var timeEntries = libxmljs.parseXmlString(body);
					

Get a node list of all time entries

					var timeEntry = timeEntries.find("jobTaskTimeEntry");
					

Loop through all the time entries

					for (k = 0, l = timeEntry.length; k < l; k++) {
						

Get details of this time entry

						var billable = timeEntry[k].find('billable')[0].text();
						var minutes = Number(timeEntry[k].find('minutes')[0].text());
						var user = timeEntry[k].find('trafficEmployeeId/id')[0].text();
						

Find this employee who made the entry and get some details

						var Employee = xmlDoc.find("/pagedResult/trafficEmployee[@id = '" + user +"']");
						var billableSoFar = Employee[0].find("billable")[0];
						var minutesSoFar = Employee[0].find("minutes")[0];
						

If this task is billable, add the time to the billable total

						if (billable === "true") {
							var t = Number(billableSoFar.text());
							billableSoFar.text(String(t + minutes));
						}
						

Add the time to the total time

						var t = Number(minutesSoFar.text());
						minutesSoFar.text(String(t + minutes));
						
					}
					
					self.finished = true;
					
				});

			}
		);
		
		req.end();
		
		req.on('error', function(e) {
			console.error(e);
		});	
	}
});

Send alerts to users

manager.addJob('outputResults', {
	work: function ( ) {
		
		var self = this
			now = new Date();
		
		

Get a node list of all the users

		var users = xmlDoc.find("/pagedResult/trafficEmployee");
		

create reusable transport method (opens pool of SMTP connections)

		var smtpTransport = nodemailer.createTransport("SMTP",{
		    host: nconf.get("smtp:host"),
		    port: nconf.get("smtp:port"),
		    auth: {
		        user: nconf.get("smtp:user"),
		        pass: nconf.get("smtp:pass")
		    }
		});

Loop through all the users

		for (i = 0, j= users.length; i<j; i++) {
			
			var user = users[i];
			var name = user.find("employeeDetails/personalDetails/firstName")[0].text() + " " + user.find("employeeDetails/personalDetails/lastName")[0].text();
			
			var billable = user.find("billable"),
				minutes = user.find("minutes"),
				available = user.find("available"),
				useremail = user.find("employeeDetails/personalDetails/emailAddress")[0].text();
			

If we are in test mode, only send email to the API user

			if (nconf.get("mode") !== "test" || useremail === nconf.get("email")) {

If this user has billed any time, and that time exceeds maxGap hours

				if (Number(minutes[0].text()) > 0 && (Number(available[0].text())/60.0 - Number(minutes[0].text())/60.0 > timecop.maxGap)) {
					

Populate template

					var html = nconf.get("templates:hours");
					var data = { 
						"name": name,
						"minutes": String((Number(minutes[0].text())/60.0).toFixed(1)),
						"billable": String((Number(billable[0].text())/60.0).toFixed(1)),
						"available":String((Number(available[0].text())/60.0).toFixed(1))
					};
					var output = plates.bind(html, data); 
					

Create email

					var mailOptions = {
					    from: nconf.get("smtp:from"),
					    to: useremail,
					    subject: nconf.get("smtp:subject") + " " + now.toString(),
					    text: output,
					    html: output,
					    headers: {"X-SMTPAPI": {"category": "Timecop"}}
					}
					

send mail with defined transport object

					smtpTransport.sendMail(mailOptions, function(error, response){
					    if(error){
					        console.log(error);
					   }
					
					});
				}
			}
		}
		self.finished = true;
	}
});

Fires every time a job finshes

 manager.on('finish', function (job, worker) {
		
	var idle = true;
	
	for (i = 0, j = Object.keys(this.jobs).length; i<j; i++ ) {
		var name = Object.keys(this.jobs)[i];
		if ( Object.keys(this.jobs[name].running).length > 0 || this.jobs[name].queue.length > 0) idle = false;
	}

If we are idle and the last job to run was getDay, it must be time to output results

	if (job.name == 'getDay' && idle) {
		manager.enqueue('outputResults');
	}
});
	

Convert a single digit number to a two digit string

timecop.twoDigit = function( value ) {
	value= String(value);
	if (value.length === 1) value = "0" + value;
	return value;
}

Entry point

var jobs = nconf.get("jobs");

Loop through all the jobs in the config.json file and put them on chron

utile.each(jobs, function(job, key, obj) {
	new cronJob(job.schedule, 
		function(){
	    	manager.enqueue(job.type, nconf, job.maxGap);  
	    }, null, true
	);
});

If the user passed --mode test then run a test immediately

if (nconf.get('mode') === "test") {
	manager.enqueue('checkHours', nconf, 8);
}