Building a Simple Project Management Tool With ExpressionEngine

Ian Ebden · February 28, 2013

Looks like this article was written a while ago. Some of the ideas and/or techniques described may now be obselete.

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…


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:


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]-->
	<meta charset="utf-8">

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



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



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="{segment_1'}">
	<select name="status">
		<option value="not foo">All</option>
			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>
	<input type="submit" value="Go" class="button alt"/>

			<th>Client Ref.</th>
			<th>Job No.</th>
		status="not foo"
		{if no_results}
			<td colspan="4" class="uncolor">No records available</td>
		<tr class="{switch='odd|even'}">
			<td><a href="{title_permalink='job'}">{title}</a></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 class="pagination">
			<td colspan="4">Page: {pagination_links}</td>

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.


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.


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="{segment_2'}">Back to {segment_2}</a></p>

{if last_segment != "success"}

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"}


	<p><small class="uncolor">Fields marked * are mandatory</small></p>
			<label for="title">Name <em>*</em></label>
			<input type="text" name="title" id="js_title" value="{title}" maxlength="100" class="required" required/>
			<select name="status">
			... SafeCracker's custom field loop ...
			<label>Categories <span class="helper">Cmd or Ctrl + click to select multiple categories.</span></label>
			<select name="category[]" id="js_categories" multiple="multiple">
	<input type="submit" name="submit" value="Save" class="button"/> <a href="{segment_2'}" class="button alt">Cancel</a>

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:


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="//"></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.