Building a Small Job Tracker in Ruby

Written by

A job looks simple on a calendar. There is a date, a customer and something that needs doing. Open the email thread behind the job, though, and things become more complicated. For instance, someone wants to move the appointment. A supplier has not responded. The address in the last message differs from the one on the booking.

That is a good problem for a Ruby-based application. Create a job tracker that lets a small team see what is booked, what is missing, and who needs to take the next step. The initial release can be basic: a few records, some predefined rules and a reliable list of tasks due on a particular day.

Give the project a concrete use case

For this example, assume a coordinator responsible for managing deliveries and meetings with external vendors. One booking might involve tiny house movers, while another concerns equipment delivery or a site visit.

The Ruby application manages these records. Your application keeps track of the vendor’s reference number, records confirmation, and identifies any missing information. Ultimately, deciding how to physically accomplish a move remains with the individuals performing the task.

Be careful when establishing boundaries regarding the type of functionality you want to include in your application. Creating a dashboard displaying unconfirmed appointments is a reasonable first goal. Determining whether all jobs are prepared to proceed automatically would require more information than a date and a status field can provide.

Decide what a job contains

Create a small set of fields for each job: internal job id, job schedule date, job status, job coordinator assignment, provider reference, and additional notes if needed for any details that don’t warrant individual fields. Using arrays of hashes provides sufficient flexibility to allow you to access each record in your Ruby application until you clearly determine the behavior of each feature. At that point create a dedicated Job class. There is little value in developing an overly complex object hierarchy before you understand the questions your tracker must answer.

When creating a required field, consider what happens if that field is not included. Without a default argument or block, calling job.fetch(:status) raises KeyError if the key doesn’t exist. This allows you to identify potential issues early with incomplete data rather than allowing that issue to potentially propagate throughout multiple methods. It does not verify the content of the field. Therefore, even though you provided valid keys (e.g., :status), the value associated with that key could potentially contain nil or an unacceptable string.

Also assign each job its own unique identifier. The customer name and/or address can change. Thus neither should be used as the sole identifying factor for the associated booking and its supporting documentation.

Write down the allowed status changes

Using a status field incorrectly leads to confusion since everyone uses it differently. By definition, does “confirmed” indicate agreement by the customer, agreement by the vendor, or both?

Define a limited vocabulary (e.g., requested, confirmed, completed, cancelled) for each possible status of a job and document which transitions are permitted from each status. A requested job can either transition to confirmed or transition to cancelled. A completed job must go through a defined error resolution process in order for that incorrect value to be corrected.

Implement checks related to these transitions in methods that can be invoked from any interface. If your web form enforces one rule, but your data importer silently applies another, that creates an inconsistent state.

To avoid encapsulating many decisions within a single method named update_job, name your methods according to the actions you intend to perform (e.g., confirm!, cancel!). The exclamation mark is part of the Ruby method name; it does not automatically enforce validation, mutation or persistence.

Let Enumerable handle the daily list

Once you’ve ensured your records have been created uniformly, you’ll find Ruby’s collections very helpful.

Use select to retrieve jobs that match certain criteria, use sort_by to order them based on some value, and use group_by to aggregate jobs under common keys. These three operations cover most of the daily questions you’ll likely face: which jobs remain unconfirmed? What comes next? How much work is assigned to each coordinator?

Consider using these three operations as building blocks for your question-answering code. Be sure to keep each operation easily readable. A colleague should be able to alter the query parameters without having to disentangle a long series of nested blocks.

This approach works well when dealing with small amounts of data that fit entirely in memory. However, when working with databases, filter/sort at the database level whenever possible. Creating large amounts of temporary storage simply to view today’s workload creates excessive processing overhead for your application.

Make dates explicit

While “Next Friday” makes sense during casual conversations, it is also poor stored data.

Determine a specific date format for input and convert accepted values into date objects as they enter the system. Ruby’s Date.iso8601, made available via require "date", will parse date strings formatted in accordance with the ISO 8601 standard. If you require users to enter dates in an exact format (YYYY-MM-DD), ensure you validate that shape as well; ISO 8601 defines multiple acceptable ways to express dates.

Provide meaningful feedback to users when they submit bad dates. Replacing a user-submitted bad date with today’s date could inadvertently place a job in an inappropriate category.

You should also decide whether scheduling a job requires only a calendar date or whether an accurate appointment time is also required. Calendar dates do not adequately specify “at 9 am at the destination.” Consider specifying the time zone applicable to an appointment when determining whether an appointment time matters so that users can rely on reminder notifications correctly.

Expect two people to edit the same booking

A shared tracker needs a way to handle overlapping edits. Two coordinators may attempt to modify the same booking while viewing different versions of it. Any subsequent update performed on the stale version of that booking should not silently overwrite previous changes.

If you implement your interface in Rails, Active Record supports optimistic locking through a lock_version column. Attempting to update from an out-of-date version of an object raises an ActiveRecord::StaleObjectError, providing your application an opportunity to resolve the conflict.

The visual presentation still requires consideration of what users experience when attempting to complete their modifications. Display that the booking was modified; maintain any pending input; prompt users to examine the current state of the booking.

A raw exception page does little to help users complete their work.

Test the questions people will actually ask

Test unusual scenarios: a job without a provider reference; an invalid date; a cancelled appointment that must disappear from the active list. Also test that an empty day returns an intelligible result.

Pass in an arbitrary reporting date as an argument into any methods dependent upon the current date. This will enable tests to use a fixed date rather than cause method behavior to change as calendars progress.

As a final check, manually run a few fictional bookings through your tracking application. Confirm one booking, reschedule another booking and cancel one more booking. Have another user identify what should come next from viewing the screen. If they feel compelled to open the original email thread to understand the status of that booking, then that is the next piece of your Ruby application that you need to enhance.