Time Tracking Using Plaint Text and Drafts - Part 5 (the getClient function)

As mentioned in my last post, I made a getClient function to make my plain text time tracking a whole lot easier. The function allows me to standardize client names and retrieve other data related to those clients. Standardizing improves the plain text time tracking experience immensely:

First, if you don’t standardize the client name, you get a lot of entries, that should really be one. For example, ABC Corp. and ABC and abc corp and abc corp. would be treated as separate clients (since the names don’t match exactly). This annoyance can be fixed in a single time slip relatively easily. But as soon as you want to calculate a total number of hours over a week or month or year, you need to make sure all the entries for a client use exactly the same client name. And remembering that name can be really hard if the last time you worked on a matter was weeks or months ago.

Second, there’s a tension between (1) choosing a short client name that is easy to type as you record your time and (2) choosing a client name that is both distinct from other client names and descriptive enough to make sense to your assistant or future self who is trying to understand which actual client-matter an entry refers to.

The solution is to have a database, (here just a simple JSON file) to keep track of everything. Each client can have an official name and a whole bunch of “aliases,” which don’t have to be unique, along with a number, a billable flag, and any other useful information.

That’s what the getClient function does. And it’s got some bells and whistles too: It take a string, checks to see if it matches any alias of any client. If it matches more than one, it lets you choose which one you really meant. If there was a typo in a client alias in your time slip, it allows you to correct it, and if you have a new client, it allows you to add the new client to the JSON database.

Once getClient knows which client you’re actually referring to, it returns the entire client object, allowing you to access the client name, billing number, or whatever other data you might want.

I use getClient in several different actions, not just the action to process a time slip. For example, I have a keyboard button that looks up the client number for selected text (or prompts to type the text if nothing is selected). This is very helpful all those times when you need the billing number, but can’t quite remember it.

Finally, this process allows me to define a non-trackable “client” with a billing number of zero. The aliases for that client include all the things that I don’t want to capture in my time tracking—things like “lunch”, “break”, “doctor”, “dentist”, etc. The time tracking script mentioned a couple of posts ago filters out all entries where the billing number is 0.

Anyway, here’s the script:

// function that takes string returns client object with matching alias
// or prompts to correct string or add new client object if no match
// client object includes name, nickname, number, billable, alias, and description

function getClient(candidate) {
	const fileName =  "clientList.json";
	// make candidate lowercase for easy comparison
	candidate = candidate.toLowerCase().trim();
	// load contents of file
	myCloud = FileManager.createCloud();
	let contents = myCloud.read("/" + fileName);
	let clientList = JSON.parse(contents);
	// filter potential matches
	let matches = clientList.clients.filter(c => c.alias.includes(candidate));
	switch (matches.length) {
		// if one match, return match
		case 1:
		// if no match, prompt for correction or new client
		case 0:
			let p = Prompt.create();
			p.title = "no match";
			p.message = "there was no match for " + candidate + "\n\n if there was a typo, enter corrected name. If " + candidate + " is a new client, enter name, nickname, aliases (separated by a comma) and a client-number";
			p.addTextField("clientName", "Corrected/New Client Name", candidate, {wantsFocus:true});
			p.addTextField("clientNickname", "Nickname (for iCloud folder)", "");
			p.addTextField("clientAliases", "Client Aliases", "");
			p.addTextField("clientNumber", "Client Number", "", {placeholder:"00000-0"});
			p.addSwitch("billable", "Billable?", true);
			p.addTextField("description", "Description", "");

			// display the prompt
			let didNotCancel = p.show();

			// if cancelled, just quit;
			if (!didNotCancel) { 
				context.cancel("user cancelled");
			} else {
			// if chose to add new client, add to list and return new info
			if (p.buttonPressed == "Add") {
				let newClientName = p.fieldValues["clientName"];
				newClientName = newClientName.toLowerCase().trim();
				let newClientNickname = p.fieldValues["clientNickname"];
				newClientNickname = newClientNickname.toLowerCase().trim();
				let alias = p.fieldValues["clientAliases"].split(",");
				alias = alias.map(x => x.toLowerCase().trim());
				alias = [...new Set(alias)]; // eliminate any duplicates
				let number = p.fieldValues["clientNumber"];
				let bill = p.fieldValues["billable"];
				let description = p.fieldValues["description"];
				// build a new client object
				let newClient = {
					name: newClientName,
					nickname: newClientNickname,
					number: number,
					billable: bill,
					alias: alias,
					description: description
				// add new client to list

				// write the updated client list to the file
				contents = JSON.stringify(clientList);
				let success = myCloud.write("/" + fileName, contents);
				if (success) {alert(candidate + " added!");}
				else {alert("error: file wasn't written");}
			// if corrected, re-run function with corrected candidate
			if (p.buttonPressed == "Correct") {
				console.log("correct selected. Now running getClient on: " + p.fieldValues["clientName"]);

		// if multiple matches, chose among them
			let q = Prompt.create();
			q.title = "multiple matches";
			q.message = "choose which client you meant:";
			for (c in matches) {
			let choice = matches.filter(c => c.name == (q.buttonPressed));

Note that it stores the database as clientList.json in the top level of your Drafts folder in iCloud, but you can move this elsewhere and adjust the script accordingly.

Next, I’ll be putting together a tl;dr version of all this, with links to download the actions to the action directory.

Ciaran Connelly @ciaran