Building a Simple Project Management Tool With ExpressionEngine.

Looks like this article is pretty old. Some of the information and/or techniques may now be obselete. Just sayin’
Building a Simple Project Management Tool With ExpressionEngine on DesignKarma
The “Dashboard” users get after logging in.

I recently built a simple project management tool for a client to replace their previous setup – a creaky old Microsoft Access database. With thousands of entries the system was grinding to a halt, not to mention the fact it was running off a single desktop with no backup strategy!

Building a Simple Project Management Tool With ExpressionEngine on DesignKarma

The client wanted a web-based app they could all access from any location, 24/7. They were property maintenance specialists, and needed a system to manage projects (or “jobs” as they call them), clients, suppliers and tradesmen. They also wanted to track time and materials on a project, and use this information to do some basic reporting on remaining budgets. A major bonus was that they didn’t want to migrate any old data – preferring instead to just archive the Access database, and start over. Phew.

ExpressionEngine to the Rescue

I managed this all pretty quickly with ExpressionEngine, using only a handful of native and 3rd-party add-ons including:

  • SafeCracker. I didn’t want the client to have to flip between the ExpressionEngine control panel (for data entry) and a “front end” (to view content), so I used SafeCracker to handle publishing and editing, keeping everything “front end”.
  • Query module for a few quick grabs, calculations, etc…
  • Matrix to handle timesheets and record supplier invoices.
  • Auto Increment Field. Job numbers needed to be 5 digits, auto-increment, and be read-only. They also needed to start where the old system left off (at 25985).
  • moreMatrixRelations for 1-to-1 relationships in Matrix. Normally I’d use Playa but it seemed a bit overkill on this project.
  • IfElse and Switchee, which I use on most projects to speed up conditionals and cut down on templates/code used.

Setting up

Custom fields, categories and statuses

I created four custom field groups: Job, Client, Supplier and Tradesmen. The last three just contained text inputs for address details; whereas Job was a little more involved, with extra fields for things like Job Number (Auto Increment Field), Client (Relationship),Timesheet and Materials (both Matrix).

I added some custom categories for my Client channel (e.g. Healthcare) and some Generalcategories (Plumbing, Electrical etc…) to share across my Supplier, Job and Tradesmenchannels.

Lastly, I needed some custom statuses for my Jobs channel – the client needed to indicate whether a job was Pending, Cancelled, Invoiced, etc…

Templates

With my channels and custom stuff all setup, I was pretty much done with the control panel and ready to start the templates. My template groups looked like this:

Building a Simple Project Management Tool With ExpressionEngine on DesignKarma

Firstly I created a document outline that would form the basis of every template:

{if logged_out}{redirect='site_index'}{/if}
<!DOCTYPE html>
<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
	<meta charset="utf-8">
	<title>DesignKarma</title>
	{gv_styles}
</head>
<body>

	{sn_header}
	
	<div class="main" role="main">
		
		<div class="wrapper clearfix">
			
			<div class="content">
				... content goes here ...
			</div>
			
			<aside class="aside">
				<h1 class="visuallyhidden">Taskbar</h1>
				{sn_search}
				... other stuff can go in here ...
			</aside>
			
		</div>
		
	</div>

{gv_jquery}
{gv_scripts}

</body>
</html>

Since this is a member-only site the first thing to do on each page is redirect anyone who isn’t logged in. The only exception is obviously the sign-in/index template, where we do the opposite (redirect to dashboard/index if logged in).

As usual I added my any scripts as a global variable just inside the </body> tag, although this time I split out my jQuery into a separate global variable ({gv_jquery}). That’s because SafeCracker prefers jQuery in the <head> – something that has caught me out a few times in the past. With jQuery in its’ own global variable I could pop it in the <head>just on my SafeCracker pages.

Key pages

Dashboard

Building a Simple Project Management Tool With ExpressionEngine on DesignKarma

After login users are redirected to the Dashboard, listing all jobs. Users can filter the job list by the custom statuses I setup earlier. I used the Query module to construct my status menu, and set dynamic_parameters=“status” in my channel entries tag to get the filter working.

<form method="post" action="http://designkarma.co.uk/{segment_1'}">
	<select name="status">
		<option value="not foo">All</option>
		{exp:query 
			cache="yes" 
			refresh="60" 
			sql="SELECT status_id, group_id, status, status_order FROM exp_statuses WHERE group_id = '2' ORDER BY status_order ASC"
		}
		<option value="{status}">{status}</option>
		{/exp:query}
    </select>
	<input type="submit" value="Go" class="button alt"/>
</form>

<table>
	<thead>
		<tr>
			<th>Client Ref.</th>
			<th>Job No.</th>
			<th>Client</th>
			<th>Status</th>
		</tr>
	</thead>
	<tbody>
	{exp:channel:entries
		channel="{pr_channel_short_name}"
		disable="categories|member_data|pagination"
		dynamic_parameters="status"
		limit="25"
		orderby="entry_id"
		paginate="bottom"
		sort="desc"
		status="not foo"
	}
		{if no_results}
		<tr>
			<td colspan="4" class="uncolor">No records available</td>
		</tr>
		{/if}
		<tr class="{switch='odd|even'}">
			<td><a href="{title_permalink='job'}">{title}</a></td>
			<td>{cf_job_num}</td>
			<td>{related_entries id="cf_job_client"}<a href="{title_permalink='clients'}">{title}</a>{/related_entries}</td>
			<td class="status {status}">{status}</td>
		</tr>
		{paginate}
		<tr class="pagination">
			<td colspan="4">Page: {pagination_links}</td>
		</tr>
		{/paginate}
	{/exp:channel:entries}
	</tbody>
</table>

Notice in the above I’m using “not foo” in the filter menu’s first option value=, and the channel entries’ status= parameter. That’s a neat way of basically saying show all statuses. Nod to @low for suggesting that to me recently.

Job Breakdown

From the dashboard users can click on a job to view a full breakdown, including a green “budget remaining” indicator bar that basically takes Time and Materials totals and works out costs as a percentage of the overall budget (via some nifty SQL). That percentage is then set as the width value of the green bar.

Building a Simple Project Management Tool With ExpressionEngine on DesignKarma

Remaining Pages

Clients, Suppliers and Tradesmen pages are just paginated lists, with a bit of category filtering thrown in. Clicking a title takes you to a single entry page, which is actually a pre-populated SafeCracker edit form, so the client can view and edit details at the same time.

Building a Simple Project Management Tool With ExpressionEngine on DesignKarma

Publishing and editing with SafeCracker

That brings us nicely to the publishing and editing forms. For newbies, SafeCracker is a standalone entry form (SAEF) module that let’s users create and edit entries outside the ExpressionEngine control panel. It can be a tricky customer, and I once used it on a site to manage complex hedge fund information. It nearly killed me. But this is a pretty simple setup.

The Job form had a bit more complexity than the others, so I started with the Clients,Suppliers and Tradesmen forms, which it turns out could all be done with one template (create/index) and the same form. The key lines of code in the create template are:

{if last_segment == "success"}
<h2 class="success"> Your new entry has been created</h2>
<p><a href="http://designkarma.co.uk/{segment_2'}">Back to {segment_2}</a></p>
{/if}

{if last_segment != "success"}
{sn_safecracker}
{/if}

I’m checking for a “success” segment (shown after an entry has been submitted), and using whatever is in {segment_2} (e.g. /create/clients/success) as a link back. Otherwise load our SafeCracker form, stored as a snippet.

In the {sn_safecracker} snippet I used the IfElse plugin to output the necessary SafeCracker parameters, based on whether this is a publish (segment contains “create”) or edit form:

{exp:ifelse parse="inward"}
{if segment_1 == "create"}

{exp:safecracker
	channel="{segment_2}" 
	class="clearfix" 
	datepicker="no" 
	id="js_safecracker" 
	include_jquery="no" 
	return="create/{segment_2}/success"
	safecracker_head="no" 
	use_live_url="no"
}
{if:else}

{exp:safecracker
	channel="{segment_2}" 
	class="clearfix" 
	datepicker="no" 
	id="js_safecracker" 
	include_jquery="no" 
	return="{segment_2}" 
	require_entry="yes" 
	safecracker_head="no" 
	url_title="{last_segment}"
	use_live_url="no"
}
{/if}
{/exp:ifelse}
	<p><small class="uncolor">Fields marked * are mandatory</small></p>
	<ol>
		<li>
			<label for="title">Name <em>*</em></label>
			<input type="text" name="title" id="js_title" value="{title}" maxlength="100" class="required" required/>
		</li>
		<li>
			{status_menu}
			<label>Status</label>
			<select name="status">
				{select_options}
			</select>
			{/status_menu}
		</li>
		{custom_fields}
		<li>
			... SafeCracker's custom field loop ...
		</li>
		{/custom_fields}
		<li>
			{category_menu}
			<label>Categories <span class="helper">Cmd or Ctrl + click to select multiple categories.</span></label>
			<select name="category[]" id="js_categories" multiple="multiple">
				{select_options}
			</select>
			{/category_menu}
		</li>
	</ol>
	<input type="submit" name="submit" value="Save" class="button"/> <a href="http://designkarma.co.uk/{segment_2'}" class="button alt">Cancel</a>
{/exp:safecracker}

I’m using URL segments (e.g. “clients” or “tradesmen”) so the form will be generic enough to work for any of my channels. SafeCracker’s {custom_fields} loop will output the necessary fields.

The Job form was a little more complex given the extra fields – dates, checkboxes and Matrix – plus some specific validation rules. But the basics were the same. I simply created an extra condition in {sn_snippet} to check if this was a job entry, and included a hand-coded form instead of using the {custom_fields} loop. The form looks like this:

Building a Simple Project Management Tool With ExpressionEngine on DesignKarma

Form validation

SafeCracker and ExpressionEngine have their own form validation, but I like to add some client-side stuff on top. For a while now I’ve been using Jörn Zaefferer’s jQuery Validation plugin, and using it with SafeCracker is a cinch. If you’re using SafeCracker’s{custom_fields} loop just add {if required} class=“required” required{/if}to your fields. Then, at the bottom of your document add the jQuery Validation script:

<script src="//ajax.aspnetcdn.com/ajax/jquery.validate/1.11.0/jquery.validate.min.js"></script>
<script>
$(document).ready(function(){
	$("#js_safecracker").validate();
});
</script>

Hey presto, client-side form validation on all your required fields.

Building on the basics

The project had a limited scope and budget, so this was a nice simple solution. But there’s no reason why it couldn’t be extended. For example using the Comment module to add/share project notes; Using Matrix for to-do checklists like Basecamp; Create additional member groups to allow clients, tradesmen or suppliers to update their details; Generate invoices, and so on…

Hope you found this useful. Love to hear your thoughts on Twitter @designkarma.

Let us help tell your story.

Ready to inject some karma into your project?

Get started