Build a Time Tracker with Laravel 5 and AngularJS – Part 2

Free Course

Getting Started with Angular 2

Angular 2 is the shiny new framework that comes with a lot of new concepts. Learn all the great new features.

This is the second of a two-part series on using Laravel 5 and AngularJS together to build a simple time tracking application. If you’ve gone through part 1, you’ll have seen that we put together the front-end first and used a simple JSON file with some mocked-up data to test with. We left off with the ability to add new time entries and have the total time from all of them display on the side. We didn’t include any way to edit or delete the time entries, and of course there was no persistence to a database.

In this part we will complete the application so that the time entries get stored in a database and our Angular front-end and Laravel backend work together to create, read, update and delete from it.

Installing Laravel

To get us started, let’s grab a fresh install of Laravel. We won’t go into great detail about the Laravel installation process here, and if you’re using Laravel for the first time there might be some snags you run into when setting it up. The Laravel docs are the best place to go to get full instructions on installation.

In the terminal, navigate to where your localhost files are served from. Assuming you have Composer and the Laravel installer properly installed globally, from the command line:

laravel new time-tracker-2

This command will create a new directory called time-tracker-2 and will populate it with Laravel 5 files. It also does a lot of the initial setup work for us, like setting the application key. With Laravel’s Artisan CLI we can spin up a web server. Let’s do that now to make sure the application is working. From the command line,

cd time-tracker-2
php artisan serve

If everything is working properly, you should see the Laravel 5 welcome page:

time-tracker-2-1

Setting Up the Database

For this tutorial we’re going to use a SQLite database for the sake of simplicity. SQLite is a local database that sits on the filesystem and gives us a quick and flexible way of storing data without the need for connecting to an external database. You might not have used SQLite before, so if you feel more comfortable with MySQL or even something else, feel free to change the database up as you like. All of the Laravel code we write to run migrations, seed the database, and setup relationships will work across all databases that Laravel supports.

Create SQLite Database

Navigate to the time-tracker-2/storage folder and add a new sqlite database. I like to do this from my Sublime editor by right-clicking the folder, then selecting new file. Save the file and name it database.sqlite.

Modify the Config File

Next, let’s modify the database config file to let Laravel know that we’re using SQLite. In congfig/database.php, switch this line:

'default' => 'mysql',

for this:

'default' => 'sqlite',

Since we are using a database stored on our filesystem, we don’t need to update the .env file with any database user credentials. If, however, you are using MySQL, you’ll need to modify it so that your local web server’s database user is recognized. To do so, open time-tracker-2/.env and modify the credentials. These are the ones that come with the Laravel installation:

DB_HOST=localhost
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

Download a Database GUI for SQLite

If you’re used to using phpMyAdmin for MySQL, you’ll likely want to have a similar tool for viewing your SQLite database. There is a Firefox add-on called SQLite Manager which is great for this job.

Migrations and Seeding

Updating and Creating Migrations

Now that the database is setup, the first thing we’ll want to do is run some migrations to setup our tables, and also seed the database with some initial data.

If you look in the database/migrations folder, you’ll see that Laravel comes with two migrations—one that creates a users table and another that creates a password reset table. We’ll use the default migrations, but let’s make a small change to the columns used for storing the user’s name in the users table migration so that both first name and last name are stored separately:


// database/migrations/2014_10_12_000000_create_users_table.php

...
    
// the up method creates the below fields in our users table
public function up()
{
    Schema::create('users', function(Blueprint $table)
    {
        $table->increments('id');
        $table->string('first_name');
        $table->string('last_name');
        $table->string('email')->unique();
        $table->string('password', 60);
        $table->rememberToken();
        $table->timestamps();
    });
}
    
...

Next, let’s create a migration for the time_entries table. A cool feature in Laravel 5 is that when you use Artisan to create a model, it will automatically create a migration for that model for you. Let’s hit two birds with one stone here. From the command line:

php artisan make:model TimeEntries

Let’s specify in our TimeEntry model that we want it to use the time_entries table.


// app/TimeEntry.php

...

use IlluminateDatabaseEloquentModel;

class TimeEntry extends Model {

    // Use the time_entries table
    protected $table = 'time_entries';

}

...

We’ll need to specify the fields we want to add to the table in the migration script.


// database/migrations/0000_00_00_000000_create_time_entries_table.php

...

use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;

class CreateTimeEntriesTable extends Migration {
        
    // the up method creates the below fields in our time entries table
    public function up()
    {
        Schema::create('time_entries', function(Blueprint $table)
        {
            $table->increments('id');
            $table->integer('user_id')->unsigned();
            $table->dateTime('start_time');
            $table->dateTime('end_time');
            $table->string('comment')->nullable();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::drop('time_entries');
    }

}

Next, let’s run the migrations. From the command line:

php artisan migrate

If everything worked, you should see that the migrations were created successfully. We can also check SQLite Manager to be sure:

time-tracker-2-2

Creating Seed Data

We’ll need to provide some intial data to work with, much like we did in the first part of this series, only this time it will be in a database instead of a static JSON file. Laravel offers a great method for seeding databases, and we’ll make use of it here. Let’s modify the already existing database/seeds/DatabaseSeeder.php file.


// database/seeds/DatabaseSeeder.php

...

use IlluminateDatabaseSeeder;
use IlluminateDatabaseEloquentModel;
use AppUser;
use AppTimeEntry;

class DatabaseSeeder extends Seeder {

    public function run()
    {
        Model::unguard();
            
        // Call the seed classes to run the seeds
        $this->call('UsersTableSeeder');
        $this->call('TimeEntriesTableSeeder');
    }

}

class UsersTableSeeder extends Seeder {

    public function run()
    {
            
        // We want to delete the users table if it exists before running the seed
        DB::table('users')->delete();

        $users = array(
                ['first_name' => 'Ryan', 'last_name' => 'Chenkie', 'email' => 'ryanchenkie@gmail.com', 'password' => Hash::make('secret')],
                ['first_name' => 'Chris', 'last_name' => 'Sevilleja', 'email' => 'chris@scotch.io', 'password' => Hash::make('secret')],
                ['first_name' => 'Holly', 'last_name' => 'Lloyd', 'email' => 'holly@scotch.io', 'password' => Hash::make('secret')],
                ['first_name' => 'Adnan', 'last_name' => 'Kukic', 'email' => 'adnan@scotch.io', 'password' => Hash::make('secret')],
        );
            
        // Loop through each user above and create the record for them in the database
        foreach ($users as $user)
        {
            User::create($user);
        }
    }
}

class TimeEntriesTableSeeder extends Seeder {

    public function run()
    {
        DB::table('time_entries')->delete();

        $time_entries = array(
                ['user_id' => 1, 'start_time' => '2015-02-21T18:56:48Z', 'end_time' => '2015-02-21T20:33:10Z', 'comment' => 'Initial project setup.'],
                ['user_id' => 2, 'start_time' => '2015-02-27T10:22:42Z','end_time' => '2015-02-27T14:08:10Z','comment' => 'Review of project requirements and notes for getting started.'],
                ['user_id' => 3, 'start_time' => '2015-03-03T09:55:32Z','end_time' => '2015-03-03T12:07:09Z','comment' => 'Front-end and backend setup.'],
        );

        foreach($time_entries as $time_entry)
        {
            TimeEntry::create($time_entry); 
        }   

    }
}

With database seeding, we can either break the seeds for each table into separate files, or just pile them all into one big file. If you are doing a lot of seeding, I’d certainly recommend breaking them out, but we’ll just put everything in one file in our case. You’ll see that we’re creating an array with a list of names, along with email addresses and a hashed password. We then loop through the array and populate the database with the data. The same thing goes for the time_entries table, only that the data structure is different. Up at the top in the run function, we are calling on the seeder classes.

Let’s run the seed. From the command line:

php artisan db:seed

Browsing back to SQLite Manager, we can see that the database has been seeded successfully:

time-tracker-2-3

Migrating the Front-end Files

How that we’ve got our seed data in place, it’s time to migrate all of the HTML, CSS, and Javascript we wrote in part 1. If you’ve worked with Laravel before, you’ll likely know that there is a public folder at the root of the project. Let’s copy the bower_components, data, and scripts folders and paste them right into the public directory of our Laravel project. Then, let’s copy css/style.css and paste it into the css folder.

What About index.html?

There are a couple different ways to deal with the index file when making Angular and Laravel work together, but the way I like to do it is to create an index.php file in the Laravel views folder, located at resources/views, and then create a new route within routes.php to handle the rendering.

Let’s create a new file in resources/views called index.php and copy all of the index.html content from part 1 into it. Note: You’ll see that the other views are named with .blade.php to make use of Laravel’s Blade syntax. We won’t be using Blade, so we’ll just use standard naming.


<!-- resources/views/index.php -->

<!doctype html>
<html>
    <head>
        <title>Time Tracker</title>
        <link rel="stylesheet" href="css/style.css">
        <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
    </head>
    <body ng-app="timeTracker" ng-controller="TimeEntry as vm">

        <nav class="navbar navbar-default">
            <div class="container-fluid">
                <div class="navbar-header">
                    <a class="navbar-brand" href="#">Time Tracker</a>
                </div>
            </div>
            <div class="container-fluid time-entry">
                <div class="timepicker">
                    <span class="timepicker-title label label-primary">Clock In</span><timepicker ng-model="vm.clockIn" hour-step="1" minute-step="1" show-meridian="true"></timepicker> 
                </div>
                <div class="timepicker">
                    <span class="timepicker-title label label-primary">Clock Out</span><timepicker ng-model="vm.clockOut" hour-step="1" minute-step="1" show-meridian="true"></timepicker>
                </div>
                <div class="time-entry-comment">                
                    <form class="navbar-form">
                        <input class="form-control" ng-model="vm.comment" placeholder="Enter a comment"></input>
                        <button class="btn btn-primary" ng-click="vm.logNewTime()">Log Time</button>
                    </form>
                </div>    
            </div>
        </nav>

        <div class="container">
            <div class="col-sm-8">

                <div class="well vm" ng-repeat="time in vm.timeentries">
                    <div class="row">
                        <div class="col-sm-8">
                            <h4><i class="glyphicon glyphicon-user"></i> {{time.user_firstname}} {{time.user_lastname}}</h4>
                            <p><i class="glyphicon glyphicon-pencil"></i> {{time.comment}}</p>                  
                        </div>
                        <div class="col-sm-4 time-numbers">
                            <h4><i class="glyphicon glyphicon-calendar"></i> {{time.end_time | date:'mediumDate'}}</h4>
                            <h2><span class="label label-primary" ng-show="time.loggedTime.duration._data.hours > 0">{{time.loggedTime.duration._data.hours}} hour<span ng-show="time.loggedTime.duration._data.hours > 1">s</span></span></h2>
                            <h4><span class="label label-default">{{time.loggedTime.duration._data.minutes}} minutes</span></h4>
                        </div>
                    </div>
                </div>

            </div>

            <div class="col-sm-4">
                <div class="well time-numbers">
                    <h1><i class="glyphicon glyphicon-time"></i> Total Time</h1>
                    <h1><span class="label label-primary">{{vm.totalTime.hours}} hours</span></h1>
                    <h3><span class="label label-default">{{vm.totalTime.minutes}} minutes</span></h3>
                </div>
            </div>
        </div>  
    </body>

    <!-- Application Dependencies -->
    <script type="text/javascript" src="bower_components/angular/angular.js"></script>
    <script type="text/javascript" src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
    <script type="text/javascript" src="bower_components/angular-resource/angular-resource.js"></script>
    <script type="text/javascript" src="bower_components/moment/moment.js"></script>

    <!-- Application Scripts -->
    <script type="text/javascript" src="scripts/app.js"></script>
    <script type="text/javascript" src="scripts/controllers/TimeEntry.js"></script>
    <script type="text/javascript" src="scripts/services/time.js"></script>
</html>

Next, let’s add a route to routes.php to handle this view, and remove the already existing route that renders the welcome view.


// app/Http/routes.php

...

Route::get('/', function() 
{
    return view('index');
});
    
// Remove or comment this out
// Route::get('/', 'WelcomeController@index');

...

What we’ve done here is registered a route such that when a GET request is made to the default route of the site (signified by ‘/’), Laravel will render the index view, which we have in our index.php file. Let’s make sure the page is being displayed as it should. Make sure you still have the local web server fired up, and head to the page. For me, it’s at localhost:8000. If everything is working, you should see the application we had from part 1:

time-tracker-2-4

Awesome, we’re done migrating the front-end from part 1. However, the data that is being displayed is still from the static JSON file. Let’s fix that in the next section.

Finishing the Model Setup

There is a bit more we need to do with our TimeEntry model before we can proceed. By default, Laravel protects against mass assignment, so we’ll need to tell the model which fields are fillable. We’ll also need to setup an Eloquent relationship so that our TimeEntry model talks to the User model.


// app/TimeEntry.php

...

use IlluminateDatabaseEloquentModel;

class TimeEntry extends Model {

    protected $table = "time_entries";
        
    // An array of the fields we can fill in the time_entries table
    protected $fillable = ['user_id', 'start_time', 'end_time', 'comment'];

    protected $hidden = ['user_id'];
        
    // Eloquent relationship that says one user belongs to each time entry
    public function user()
    {
        return $this->belongsTo('AppUser');
    }

}

An Eloquent relationship is setup by creating a new method called user that calls belongsTo on the User model (namespaced here with AppUser). Setting up relationships with Eloquent is a whole other article, but using belongsTo here means that we’re going to have our TimeEntry model use it’s user_id and look up that id in the users table to match up time entries with users.

For the sake of reducing unnecessary data in our API, we’re also hiding the user_id field from being returned since we won’t need it for the app to function as it should.

The User model is already setup for us out of the box with Laravel, but I like to hide the created_at and updated_at fields since we won’t require them on the front-end:


// app/User.php

...
    
protected $hidden = ['password', 'remember_token', 'created_at', 'updated_at'];
    
...

Setting Up the API

To pull data from our database, we’ll first need to setup our API. Laravel offers a very easy way to create a REST API along with the resource controllers necessary for it. We won’t get into details about what REST is in this article, but if you are new to the concept, you should check out Teach a Dog to REST.

First, let’s define a route group and resources within it in routes.php.


// app/Http/routes.php

...
    
// A route group allows us to have a prefix, in this case api
Route::group(array('prefix' => 'api'), function()
{
    Route::resource('time', 'TimeEntriesController');
    Route::resource('users', 'UsersController');
});

Our RESTful controllers are going to respond to HTTP requests that we make to various routes. It’s a good practice to prefix the group of routes that we will send requests to with something that signifies that it is an API. In our case, we’ll just use the prefix api, but you could also prefix it with a version number.

Within this api route group, we are defining two resources. The first is a time resource, which will be for our time entries. The other is a users resource, which will make use of the users table to give us options for who the time entries are assigned to.

Now that we have our route groups and resources, let’s use Artisan to create our resource controllers. From the command line:

php artisan make:controller TimeEntriesController
php artisan make:controller UsersController

If you now navigate to app/Http/Controllers, you should see that TimeEntriesController.php and UsersController.php have been created.

View all the Available Routes

When we register route resources, Laravel will enumerate the endpoints and we can view them using Artisan. From the command line:

php artisan route:list

This will return a list of all the defined routes along with which HTTP verbs they respond to. Since we’ve registered our API resource routes, you should see them listed:

time-tracker-2-5

Return all Time Entries

The first thing we’ll want to do with the TimeEntriesController is make it return all of our time entries. Let’s use the index method to accomplish that:


// app/Http/Controllers/TimeEntriesController.php

...

use AppHttpRequests;
use AppHttpControllersController;    
use AppTimeEntry;
    
use IlluminateSupportFacadesRequest;

class TimeEntriesController extends Controller {
        
    // Gets time entries and eager loads their associated users
    public function index()
    {
        $time = TimeEntry::with('user')->get();

        return $time;
    }
    
...

The index method is usually responsibile for displaying all of the data for a given resource, and that’s just what we’re doing with it here. We are using eager loading to get all the results from our time_entries table along with the associated users and returning it. Note that we’re also pulling in our TimeEntry model at the top with a use statement.

Let’s check to make sure we are getting all the results for this endpoint. I like the Chrome app Postman which is a REST client to help test and debug REST APIs.

If we make a GET request to the api/time endpoint, we should see a list of all our time entries from the database:

time-tracker-2-6

Return All Users

Let’s also setup our UsersController to display all users when we do a GET request to the /users endpoint:


// app/Http/Controllers/UsersController.php

...

use AppHttpRequests;
use AppHttpControllersController;
use AppUser;

use IlluminateHttpRequest;

class UsersController extends Controller {
        
    // Gets all users in the users table and returns them
    public function index()
    {
        $users = User::all();

        return $users;
    }
    
...

Here you can see that we’re doing the same thing as the TimeEntriesController on the index method: using the model to get all of the users and then returning them. If I use Postman to hit the /users endpoint, I see the data returned properly.

Updating the Front-End

Now that we’ve got Laravel serving our API, let’s update the front-end Anuglar code to make calls to it. The first thing we’ll need to change is our time service that currently makes a call to the static JSON file.

// public/scripts/services/time.js
    
(function() {

  'use strict';

  angular
    .module('timeTracker')
    .factory('time', time);

    function time($resource) {

      // ngResource call to the API with id as a paramaterized URL
      // and setup for the update method
      var Time = $resource('api/time/:id', {}, {
        update: {
            method: 'PUT'
        }
      });
     
  ...

You’ll see here that we are again using ngResource to make a call for the data, but this time it is to our api/time endpoint. We’ve also put in a bit of extra stuff here to set us up for later. For updating and deleting time entries, we are going to require the id of the entry as a URL parameter so that we can properly communicate with our API, so we use :id here as a URL template. The second argument that $resource expects is an object for parameter defaults, but we don’t need that in our case so we pass in an empty object. Finally you’ll see that we are defining a custom action for updating our time entries. We call the custom action update and specify that it uses the PUT method.

Let’s check to see if that worked. If you refresh the page, you should see that we are still getting time entries but they are now coming from the API:

time-tracker-2-7

The only thing that is different here is that we aren’t getting the names of the users for each time entry. This is because our data structure has changed a little bit from our test data. When we use eager loading with Laravel, it nests related data one level deeper than the rest of the data. Let’s change up our view to reflect this new structure.


<!-- resources/views/index.php -->

...

<h4><i class="glyphicon glyphicon-user"></i> {{time.user.first_name}} {{time.user.last_name}}</h4>

...

You should now be seeing the names displayed:

time-tracker-2-8

Settting Up the Users List

Earlier we setup the UsersController with Laravel and successfully retrieved a list of all users with the API using Postman. Now, let’s setup an Anuglar service that will make a call for that data. The user service will be similar to our time service, but we’ll make it be responsible only for retrieving the user data.

//public/scripts/services/user.js
    
(function() {

  'use strict';

  angular
    .module('timeTracker')
    .factory('user', user);

    function user($resource) {

      // ngResource call to the API for the users
      var User = $resource('api/users');
        
      // Query the users and return the results
      function getUsers() {
        return User.query().$promise.then(function(results) {
          return results;
        }, function(error) {
          console.log(error);
        });
      }

      return {
        getUsers: getUsers
      }
    }
  })();

This looks quite similar to the first part of the time service. We setup the factory, define User which is a call to our API at the /users endpoint, and then define a method that uses ngResource to retrieve all the users. Finally, we return an object that has on it a key called getUsers which references out getUsers method.

We’ll need to add the newly created user service to the application scripts we call in the main view.


<!-- resources/views/index.php -->

...

<!-- Application Scripts -->
<script type="text/javascript" src="scripts/app.js"></script>
<script type="text/javascript" src="scripts/controllers/TimeEntry.js"></script>
<script type="text/javascript" src="scripts/services/time.js"></script>
<script type="text/javascript" src="scripts/services/user.js"></script>

Let’s now make use of this in the controller:

// public/scripts/controllers/TimeEntry.js
    
(function() {

  'use strict';
  
  angular
    .module('timeTracker')
    .controller('TimeEntry', TimeEntry);

    function TimeEntry(time, user, $scope) {

      var vm = this;

      vm.timeentries = [];
      vm.totalTime = {};
      vm.users = [];
          
      // Initialize the clockIn and clockOut times to the current time.
      vm.clockIn = moment();
      vm.clockOut = moment();
        
      // Grab all the time entries saved in the database
      getTimeEntries();
          
      // Get the users from the database so we can select
      // who the time entry belongs to
      getUsers();

      function getUsers() {
        user.getUsers().then(function(result) {
          vm.users = result;
        }, function(error) {
          console.log(error);
        });
      }
          
      // Fetches the time entries and puts the results
      // on the vm.timeentries array
      function getTimeEntries() {
        time.getTime().then(function(results) {
          vm.timeentries = results;
            updateTotalTime(vm.timeentries);
            console.log(vm.timeentries);
          }, function(error) {
            console.log(error);
          });
        }         
...

Notice here that we are injecting the user service into our TimeEntry method so that we can use it as a dependency. We then define a getUsers method on the controller that allows us to make use of the user service to grab a list of all users. Finally, we put the list of users we retrieve onto the vm.users array.

You’ll also notice that we’ve wrapped our call to time.getTime from part 1 into a function that we call getTimeEntries. This will come in handy later when we need to update time entries after creating, editing and deleting them.

Finally for this part, let’s create a dropdown list of the users so we can choose who time entries should be attributed to.


<!-- resources/views/index.php -->

...

<div class="time-entry-comment">                
    <form class="navbar-form">
        <select name="user" class="form-control" ng-model="vm.timeEntryUser" ng-options="user.first_name + ' ' + user.last_name for user in vm.users">
            <option value="">-- Select a user --</option>
        </select>
        <input class="form-control" ng-model="vm.comment" placeholder="Enter a comment"></input>
        <button class="btn btn-primary" ng-click="vm.logNewTime()">Log Time</button>
    </form>
</div>

...

We’ve added in a select element just beside our comment input box and we’re using the ng-options directive to loop through and display the users on the vm.users array. You should now see the list of users available in our app:

time-tracker-2-8

Now that we are able to read the time entries and users from the database, in the next sections we will look at creating, updating, and deleting time entries.

Creating Time Entries

In part 1 we had the ability to create time entries, but we were just pushing the new entries onto the already existing array. This time, let’s create the Laravel logic to capture and store time entries, and then the Angular parts send the HTTP request to the API.

First, let’s add to the already existing store method in the TimeEntriesController.


// app/Http/Controllers/TimeEntriesController.php

...

use AppHttpRequests;
use AppHttpControllersController;
use AppTimeEntry;

use IlluminateSupportFacadesRequest;

...
    
// Grab all the data passed into the request and save a new record
public function store()
{
    $data = Request::all();

    $timeentry = new TimeEntry();

    $timeentry->fill($data);

    $timeentry->save();

}
    
...

The store method accepts a POST to the /time endpoint of our API. In the method we are grabbing all of the data passed along with the request, creating a new instance of our TimeEntry class, then using Laravel’s fill method to store all of the data. The fill method is a nice helper that goes through all of the data and puts it in the appropriate database fields. Finally we call save to finish things out. Notice here that we are also using Larave’s Request facade which we bring in with the use statement. To avoid conflicts, we’ll have to get rid of the already existing HttpRequest class that was previously brought in.

Now let’s setup Angular to send data to the API to store new time entries. There are a few spots we’ll need to change from part 1. First, let’s update the time service:

// public/scripts/services/time.js
    
... 
    
// Grab data passed from the view and send
// a POST request to the API to save the data
function saveTime(data) {

  return Time.save(data).$promise.then(function(success) {
    console.log(success);
  }, function(error) {
    console.log(error);
  });
}
   
...
   
return {
  getTime: getTime,
  getTimeDiff: getTimeDiff,
  getTotalTime: getTotalTime,
  saveTime: saveTime
}
      
...

Here we’ve created a new method called saveTime which gets all the user input and uses ngResource‘s save method to send a POST request to the API with the data. We have to be sure to add this new method to our returned object at the end of the service.

Next, let’s change the vm.logNewTime method in our TimeEntry controller from part 1 to use this new method on our service.

// public/scripts/controllers/TimeEntry.js
    
...
    
// Submit the time entry that will be called 
// when we click the "Log Time" button
vm.logNewTime = function() {
  // Make sure that the clock-in time isn't after
  // the clock-out time!
  if(vm.clockOut < vm.clockIn) {
    alert("You can't clock out before you clock in!");
    return;
  }

  // Make sure the time entry is greater than zero
  if(vm.clockOut - vm.clockIn === 0) {
    alert("Your time entry has to be greater than zero!");
    return;
  }
      
  // Call to the saveTime method on the time service
  // to save the new time entry to the database
  time.saveTime({
    "user_id":vm.timeEntryUser.id,
    "start_time":vm.clockIn,
    "end_time":vm.clockOut,
    "comment":vm.comment
  }).then(function(success) {
    getTimeEntries();
    console.log(success);
  }, function(error) {
    console.log(error);
  });

  getTimeEntries();

  // Reset clockIn and clockOut times to the current time
  vm.clockIn = moment();
  vm.clockOut = moment();

  // Clear the comment field
  vm.comment = "";

  // Deselect the user
  vm.timeEntryUser = "";

}
    
...

In the newly renovated logNewTime method, we are still checking to be sure that the clock-in time isn’t after the clock-out time and then we call the saveTime method on the time service. We pass an object into the saveTime method where we specify the fields in our table that will need to be filled. If the save was successfull, we call our getTimeEntries method to refresh the listing of time entries.

There are a couple different ways we could refresh the listing of time entries on save. I’ve elected here to get the full list again each time a new time entry is saved, but some people prefer to push the new data onto the local array. The problem I see with this is that the local data and what is in the database become out of sync. For a simple application like this, I prefer to take the expense of doing a database query again to make sure what is being shown to the user is what is in the database.

Updating Time Entries

We’ll need to provide the user a way to update their time entries in case they need to change the clock-in or clock-out times, the user the time entry belongs to, or the comment provided. To get us started, let’s first adjust the view to give us a UI for editing the time entries.


<!-- resources/views/index.php -->

...

<div class="container">
    <div class="col-sm-8">

        <div class="well vm" ng-repeat="time in vm.timeentries">
            <div class="row">
                <div class="col-sm-8">
                    <h4><i class="glyphicon glyphicon-user"></i> {{time.user.first_name}} {{time.user.last_name}}</h4>
                   <p><i class="glyphicon glyphicon-pencil"></i> {{time.comment}}</p>                  
               </div>
               <div class="col-sm-4 time-numbers">
                    <h4><i class="glyphicon glyphicon-calendar"></i> {{time.end_time | date:'mediumDate'}}</h4>
                   <h2><span class="label label-primary" ng-show="time.loggedTime.duration._data.hours > 0">{{time.loggedTime.duration._data.hours}} hour<span ng-show="time.loggedTime.duration._data.hours > 1">s</span></span></h2>
                   <h4><span class="label label-default">{{time.loggedTime.duration._data.minutes}} minutes</span></h4>
               </div>

           </div>
           <div class="row">
                <div class="col-sm-3">
                    <button class="btn btn-primary btn-xs" ng-click="showEditDialog = true">Edit</button>
                    <button class="btn btn-danger btn-xs" ng-click="vm.deleteTimeEntry(time)">Delete</button>
                </div>
            </div>

            <div class="row edit-time-entry" ng-show="showEditDialog === true">
                <h4>Edit Time Entry</h4>
                <div class="time-entry">
                    <div class="timepicker">
                        <span class="timepicker-title label label-primary">Clock In</span><timepicker ng-model="time.start_time" hour-step="1" minute-step="1" show-meridian="true"></timepicker> 
                    </div>
                    <div class="timepicker">
                        <span class="timepicker-title label label-primary">Clock Out</span><timepicker ng-model="time.end_time" hour-step="1" minute-step="1" show-meridian="true"></timepicker>
                    </div>
               </div>
               <div class="col-sm-6">
                    <h5>User</h5>
                    <select name="user" class="form-control" ng-model="time.user" ng-options="user.first_name + ' ' + user.last_name for user in vm.users track by user.id">
                        <option value="user.id"></option>
                    </select>
                </div>
                <div class="col-sm-6">
                    <h5>Comment</h5>
                    <textarea ng-model="time.comment" class="form-control">{{time.comment}}</textarea>
                </div>
                <div class="edit-controls">
                    <button class="btn btn-primary btn-sm" ng-click="vm.updateTimeEntry(time)">Save</button>
                    <button class="btn btn-danger btn-sm" ng-click="showEditDialog = false">Close</button>
                </div>                            
            </div>

        </div>

    </div>

    ...

We’ve added in quite a bit since the original markup we had in part 1. We’ve included some controls for opening the edit dialog and deleting the time entry (which we will handle next) and we’ve put in the structure for the editing screen. We’re hiding and showing the edit area conditionally using ng-show to which we attach a property called showEditDialog, the state of which determines whether the edit screen is shown.

The select element that uses ng-options looks a bit different for the editing screen. We use the track by statement for ng-options here to let the select box know which user should be pre-selected when the screen is opened.

Finally, we are passing the whole time record into the vm.updateTimeEntry method which is called when we click the save button.

We’ll need some additional CSS to make things look a bit better:

/* public/css/style.css */
    
.edit-time-entry {
    border-top: 1px solid #ccc;
    background-color: #e0e0e0;
    padding: 15px;
    margin: 10px !important;
}

time-tracker-2-10

Adding Logic to the Laravel Controller

Next, let’s add some logic to the Laravel controller to update the database.


// app/Http/Controllers/TimeEntriesController.php

...
    
// Grab all the data passed into the request and fill the database record with the new data
public function update($id)
{
    $timeentry = TimeEntry::find($id);
    
    $data = Request::all();

    $timeentry->fill($data);

    $timeentry->save();
        
}
    
...

The update method needs to know which time entry we want to update, so we pass in an id to work with. We use Laravel’s find helper to get the appropriate time entry and also capture all the data passed to the endpoint. We again use fill to fill in all of the database fields with the data we passed to the controller and then save the record.

Updating the Service and Controller

Now that the markup and Laravel logic is in place, let’s update the service and the controller.

// public/scripts/services/time.js
    
...
    
// Use a PUT request to save the updated data passed in
function updateTime(data) {
  return Time.update({id:data.id}, data).$promise.then(function(success) {
    console.log(success);
  }, function(error) {
    console.log(error);
  });
}
    
...
    
return {
  getTime: getTime,
  getTimeDiff: getTimeDiff,
  getTotalTime: getTotalTime,
  saveTime: saveTime,
  updateTime: updateTime
}
    
...

Here we’ve added in a new method called updateTime which takes the data passed from the controller and calls update on the Time resource. This sends a PUT request to the Laravel update controller which takes the data in and saves it.

// public/scripts/controllers/TimeEntry.js
    
...
    
vm.updateTimeEntry = function(timeentry) {
        
  // Collect the data that will be passed to the updateTime method
  var updatedTimeEntry = {
    "id":timeentry.id,
    "user_id":timeentry.user.id,
    "start_time":timeentry.start_time,
    "end_time":timeentry.end_time,
    "comment":timeentry.comment
  }     
      
  // Update the time entry and then refresh the list
  time.updateTime(updatedTimeEntry).then(function(success) {
    getTimeEntries();
    $scope.showEditDialog = false;
    console.log(success);
  }, function(error) {
    console.log(error);
  });

}  
...

In the controller we have added a method called vm.updateTimeEntry which is called from the view using ng-click when the save button is clicked. This method accepts the time object which has all the properties of a single time record—more specifically, the record that is being edited. In this method we call the updateTime method on the time service and after the request has succeeded we call getTimeEntries to refresh the list, as well as close the edit dialog by setting our showEditDialog to false.

All the pieces are now in place to edit our time entries. The only thing left now is to give the user the ability to delete them.

Deleting Time Entries

We’ve already put a button in place for deleting time entries so all we need to do now is put the Laravel logic in place and update the Angular service and controller to handle the delete request.


// app/Http/Controllers/TimeEntriesController.php

...
    
// Find the time entry to be deleted and then call delete
public function destroy($id)
{
    $timeentry = TimeEntry::find($id);

    $timeentry->delete();   
}
    
...

The Laravel logic for deleting a time entry is pretty straight forward—we just need to find the record based on an id we pass into the controller and then call delete on it.

Now let’s update the Angular service:

// public/scripts/services/time.js

...
    
// Send a DELETE request for a specific time entry
function deleteTime(id) {
  return Time.delete({id:id}).$promise.then(function(success) {
    console.log(success);
  }, function(error) {
    console.log(error);
  });
}
      
...
    
return {
  getTime: getTime,
  getTimeDiff: getTimeDiff,
  getTotalTime: getTotalTime,
  saveTime: saveTime,
  updateTime: updateTime,
  deleteTime: deleteTime
}

And finally the controller:

// public/scripts/controllers/TimeEntry.js
    
// Specify the time entry to be deleted and pass it to the deleteTime method on the time service
vm.deleteTimeEntry = function(timeentry) {
        
  var id = timeentry.id;

  time.deleteTime(id).then(function(success) {
    getTimeEntries();
    console.log(success);
  }, function(error) {
    console.log(error);
  });      

}

The vm.timeentry method is called when we click the delete button in the view and uses the id property of the time object to tell Laravel which record to delete

Wrapping Up

There we have it! A fully functioning app that lets users create, read, update and delete time entries. Although we have the base functionality in place for the app to work, there are still a few items that are missing which could improve it:

  • It would be great to have a flash message show when entries are created, updated and deleted
  • We should be more careful with the Laravel logic—it would be great if we could add in some conditional checks to our Laravel controllers to make sure data made it from the front-end to the API
  • We don’t need every Laravel controller method that we currently have—we could get rid of the ones we’re not using
  • As it stands, users can’t select dates from a calendar picker like they should be able to.

A Challenge for You

Now that we’re done building the app together, here’s a challenge for you: put the necessary code in place to accomplish the suggested improvements I mentioned above. I’d love to see your ideas for making those improvements or anything else you think the app could benefit from. Feel free to send pull requests our way!

Drop Me a Line

If you’d like to get more AngularJS and Laravel tutorials, feel free to head over to my website and signup for my mailing list. You should follow me on Twitter—I’d love to hear about what you’re working on!

Ryan Chenkie

Tech Writer at Auth0 where I create tutorials on the latest web technologies. I also write about Angular, Laravel and more on my site. Say hi to me Twitter!