Franchise Management System

Dhruva Reddy,

Problem Statement

Imagine a franchise owner of a popular cafe chain wants to be able to manage information about each of their stores. This information would include things like the store location, sales revenue, manager and employee information and payroll details. You are tasked with assisting this client in building out a web application that enables them to achieve all of that.

Building the Application

This application was built as part of a semester long project and for pedagogical purposes we were tasked with building this out in Ruby on Rails. While it's not the most scalable or memory efficient it provides a lot of the boilerplate code for the MVC framework. A key goal of this project was to familiarize ourselves with the value and utility of such a framework while attempting to abstract as much of the technical system set up as possible.

Gathering user requirements

The first step in building an application is to identify who the stakeholders/users of our application are going to be and what each of their use cases might be with the application. Knowing this we'll better be able to classify what features we'd need to build into our app and at which authorization level these features should be made available

use cases

We can also establish certain use cases for actors within our system as a general guide for how one might use the web application. I've split these into A level and B level use cases. Some samples of A level use cases are as follows:

a level use cases

A-level use cases are the highest priority use cases the system has to meet

Domain Modeling

Now we move on to creating an entity-relationship diagram (ERD), modeling our users, their attributes and their relations. Knowing what information our database might need to store and how these entities are linked to one another helps us establish the operational logic of what we expect our system to be able to do (e.g. creating multiple shifts for an employee etc.)

erd

Look up domain modeling or crows foot notation if this isn't clear

Test-Driven Development

A primary focus of this project was thinking about how to structure the application around the functions we want it to be able to accomplish. This meant coming up with a host of test cases and mock scenarios that we outlined earlier above, and then building our application around those. The code snippets below have been truncated for general readability and the full source code can be made available on request (email me!). Attempting to copy paste these for your application won't work as I've removed a lot of necessary helpers and tests!

Tests

We start by analyzing how we build test cases and how we build a context for our application to test these cases. Rails offers 2 helpful command line functions that achieve this: rails db:contexts and rails test. rails db:contexts allows us to populate our database with contextual information we generate in a contexts.rb file and rails test allows us to holistically run all the files in our test folder.

We use the factory bot gem to help us define a default context for the entities we want to build in our model. An example would look like this:

#building a default configuration for employees such that an object of type employee will always be instantiated with the following params by default
FactoryBot.define do
  factory :employee do
    first_name { 'John' }
    last_name { 'Appleseed' }
    ssn { rand(9 ** 9).to_s.rjust(9,'0') }
    phone { rand(10 ** 10).to_s.rjust(10,'0') }
    role { 1 }
    active { true }
    sequence :username do |n|
      "user#{n}"
    end
    password { 'secret' }
    password_confirmation { 'secret' }
  end
end

Using factories like these to generate test objects within each class, we then define some tests cases. The functions we define in our model should at the very least be able to pass the test cases. An example testing the scopes in the employee class would look like this:

class EmployeeScopeTest < ActiveSupport::TestCase
 
# this is run before any of the tests below. similar to junit in java
  context "Given context" do
    setup do
      create_employees # file we define elsewhere that populates our db
    end
 
    # test the scope 'active'
    should "have all active employees accounted for" do
      assert_equal 6, Employee.active.size 
      deny Employee.active.include?(@chuck)
      assert_equal [@alex,@ben,@cindy,@ed,@kathryn,@ralph], Employee.active.sort_by{ |emp| emp.first_name }
    end
 
    # test scope alphabetical
     should "list employees alphabetically" do
      assert_equal ["Crawford", "Gruberman", "Heimann", "Janeway", "Sisko", "Waldo", "Wilson"], Employee.alphabetical.map{ |e| e.last_name }
    end   
 
# and more ....

Model

Here is where you define the logic of what you're going to do, The below is a non-comprehensive example of a model we define for the Employee class. This model contains scopes and methods that help us to arrange, sort and view specific information about employees. It also defines constraints for an object of this class and specifies how objects of this class are linked to other classes.

class Employee < ApplicationRecord
  # Relationships to other classes
  has_many :assignments 
  has_many :stores, through: :assignments
  has_many :pay_grades, through: :assignments
 
  # Enums
  enum :role, { employee: 1, manager: 2, admin: 3 }, scopes: false, default: :employee, suffix: true
 
  # Scopes
  scope :managers,        -> { where('role = ?', roles['manager']) }
  scope :admins,          -> { where('role = ?', roles['admin']) }
  scope :alphabetical,    -> { order('last_name, first_name') }
 
  # Validations
  validates_presence_of :first_name, :last_name, :ssn
  validates_date :date_of_birth, :on_or_before => lambda { 14.years.ago }, on_or_before_message: 'must be at least 14 years old'
  validates_format_of :ssn, with: /\A\d{3}[- ]?\d{2}[- ]?\d{4}\z/, message: 'should be 9 digits and delimited with dashes only'
 
  # Other methods
  def current_assignment
    curr_assignment = self.assignments.current    
    return nil if curr_assignment.empty?
    curr_assignment.first   # return as a single object, not an array
  end

View

Views in Ruby on Rails are stored in their own folder and can be rendered using partials to enable code reuse. This is exactly what I did in my application. For example, we might generate a view to show a list of all employees when one clicks on the employees tab of the application but we want to reuse the same format when displaying a list of all stores when someone clicks on the stores tab. I've omitted the actual file because it's a little long but the 2 snippets below provide a high level understanding of how this rendering might work

# we render the partial when someone activates the 'index' route for employees and pass in parameters into the local placeholder variables of that partial
<% if logged_in? && current_user.admin_role?%>
 
    <%= render partial: "partials/emp_index_structure", locals: {model_name: "employee", 
                                                            primary: "tabbed_index", 
                                                            secondary: nil,
                                                            sidebar: nil} %>
<% elsif logged_in? && current_user.manager_role? %>
    <%= render partial: "partials/emp_index_structure", locals: {model_name: "employee", 
                                                            primary: "tabbed_index", 
                                                            secondary: nil,
                                                            sidebar: nil} %>
<% end %>
# this is what the emp_index_structure partial looks like with the primary, secondary and sidebar local variables
 
<div class="row">
  <div class="small-8 columns">
    <!-- primary partial gets displayed here in main div -->
    <%= render partial: "#{primary}" %>
    <!-- sometimes we want another partial below -->
    <% unless secondary.nil? %>
      <p>&nbsp;</p>
      <%= render partial: "#{secondary}" %>
    <% end %>
    
  </div>
  <div class="small-4 columns">
    <!-- render the sidebar, if it exists -->
    <%= render partial: "#{sidebar}" unless sidebar.blank? %>
  </div>
</div>
 
 

Controller

I think this snippet from wikipedia explained this concept better than I could:

In Rails, requests arriving from the client are sent to a "router", which maps the request to a specific method of a specific controller. Within that method, the controller interacts with the request data and any relevant model objects and prepares a response using a view. Conventionally, each model type has an associated controller; for example, if the application had a Client model, it would typically have an associated Clients controller as well.

Below is an example of the controller for the employee model. It instantiates objects that take in specific data from the models and passes them onto the view. Methods in a controller usually route through one of the HTTP CRUD operations but additional custom functions can be defined for other routes.

class EmployeesController < ApplicationController
	before_action :check_login 
	authorize_resource
 
# this is the controller for the index action (aka GET in HTTP verbs) that passes information in the form of the @active_employee or @inactive_employee parameter. These objects are referenced in the view
 
	def index
		if current_user.manager_role?
			@active_employees = current_user.current_assignment.store.employees.active.paginate(page: params[:page]).per_page(15)
			@inactive_employees = current_user.current_assignment.store.employees.inactive.paginate(page: params[:page]).per_page(15)
		else
			@active_employees = Employee.active.all.paginate(page: params[:page]).per_page(15)
			@inactive_employees = Employee.inactive.all.paginate(page: params[:page]).per_page(15)
		end
	end
end

Actual Testing

Running rails test produces an output like below. It shows you your test coverage as well to make sure that you're hitting branch coverage on the functions you defined in your model.

rails test output

A useful tool that assisted me in this was Cucumber Testing. Cucumber reads executable specifications written in plain text and validates that the software does what those specifications say.

This part of testing is honestly something I think most people would do manually but it helps to encode this into your testing suite so that you can build a CI/CD pipeline that automates the checking of this user flow. If someone were to make a major modification and push it to your repo/tool of choice, this would raise a flag that you've failed a specific user scenario you've defined before.

An example of such a specification/scenario from the project is as follows:

Feature: Manage stores
  As an administrator
  I want to be able to manage store information
  So I can connect employee activity to particular stores

  Background:
    Given a logged in admin
  
  # READ METHODS
  Scenario: View all stores
    When I go to the stores page
    Then I should see "Active Stores owned by Cafe23"
    And I should see "Name"
    And I should see "Current Assignments"
    And I should see "Oakland"
    And I should see "412-268-8211"
    And I should see "Inactive Stores"
	
    And I should not see "ID"
    And I should not see "_id"
    And I should not see "Created"
    And I should not see "created"
  

Demo

Wireframing

Before building out the application you're about to see, I spent some time attempting to come up with a low-fidelity wireframe for what I wanted the application to look like at the end. The final product is quite a bit different from the wireframes but it was still a useful exercise for designing with an end goal in mind.

wireframes

Final Product

Most of the page's design was developed with Material UI's ruby gem linked here (opens in a new tab). I've included a few screenshots showing some capabilities of the application but for the sake of brevity have limited it to 5.

home

Landing page for all users pre-login

login

Login page for an admin with their own custom photo

upcoming shifts

A view of upcoming shifts for a specific employee

payroll generator

Generating payrolls for employees

new shift

Adding shifts for an employee with dynamic dropdowns

Feel free to leave any comments or reach out if you'd like to know more about this project, it's development or if you'd like to demo the end product. Special thanks to Prof H and the TAs for 67-272 for helping out with this!