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

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.

Laravel and AngularJS work great together, but it can be a little tricky to get going at first, especially if you are new to the frameworks. In a previous article, Chris showed you how to make a Single Page Comment App with Laravel and Angular. This tutorial will again bring the two frameworks together as we build out a simple time tracking application.

We’ll be going into a lot of detail in this tutorial, so to make things manageable it has been broken into two parts. The first part will focus on getting the front-end setup with AngularJS and the second part on getting the backend setup with Laravel 5.

The Application

We’ll be building a simple time tracking application that will give users the ability to track hours spent on tasks by clocking-in and clocking-out. We’ll use the Timepicker directive offered by UI Bootstrap to let users enter their start and end times, and will give them a field to enter comments. Users will be able to list all of their time entries, create new ones, and also edit or delete existing entries.

time-tracker-1-1

We will be using the AngularJS Style Guide by John Papa which sets out an opinionated set of conventions for Angular apps. If you haven’t seen this style guide before, some of the ways we set things up might look a bit foreign. Don’t worry though, it’s easy to catch on, and you’ll likely find our code easier to read and understand with the it applied.

Setup the Folder Structure and Install Dependencies

As we focus on the front-end for this part of the tutorial (the Angular side, we’ll deal with the Laravel side in part 2), let’s keep our folder structure simple. Create a new directory and give it the following structure:

|--bower_components
|--css
|--data
|--scripts
    |--controllers
    |--services

Next, let’s install our dependencies with Bower. From the command line:

bower install angular angular-bootstrap angular-resource bootstrap moment

That’s a lot of dependencies! Let’s go through them to see what we’ve got:

  • Angular – the AngularJS framework.
  • Angular Bootstrap – UI Boostrap, which gives us native Angular directives for Boostrap. For more on using the two together, read up on How to Use Bootstrap and Angular.
  • Angular Resource – ngResource gives us a way to interact with RESTful server-side data sources.
  • Bootstrap – We’ll need the standard Boostrap styles, so let’s grab the original framework too.
  • Moment.js – A great library that drastically simplifies working with time in JavaScript. We will use it to calculate our tracked time.

Setup the JavaScript Files

Let’s create our JavaScript files and setup their basic structure. There are three files that we will need:

  • scripts/app.js – where our application module is defined, the starting point for the application.
  • scripts/controllers/TimeEntry.js – our main controller for the time entries.
  • scripts/services/time.js – an Angular factory that will be used to abstract away common tasks.

Our app.js file is very simple—we just need to define the application module, which we will call timeTracker, and put in our dependencies:

/* scripts/app.js */
    
(function() {

    'use strict';

    angular
        .module('timeTracker', [
            'ngResource',
            'ui.bootstrap'
        ]);
    
})();

In our TimeEntry.js file we need to declare our controller which we will call TimeEntry. Following the AngularJS style guide mentioned earlier, we will use a capture variable called vm (ViewModel) for our controller:

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

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

        function TimeEntry(time) {

            // vm is our capture variable
            var vm = this;

            vm.timeentries = [];

        }
})();

Here you can see that we’ve declared an empty array called vm.timeentries. This array will eventually hold the time entry data that we grab with ngResource. You’ll also notice that we create a named function called TimeEntry and pass it to the controller. The time argument we pass into this function is actually a dependency that we are injecting, and it references the time service that we will create next.

Finally, our time.js file will be the service for abstracting common code, especially the ngResource pieces.

/* scripts/services/time.js */
    
(function() {
    
    'use strict';

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

        function time($resource) {

            // ngResource call to our static data
            var Time = $resource('data/time.json');
            
            return {};

        }
})();

This service follows the same pattern that we saw in our controller, but this time uses a factory called time. Our named function time takes ngResource as a dependency and currently returns an empty object.

There’s one other piece in there right now—a variable called Time that uses ngResource to make a call to a static JSON file called time.json. So what is happening here and what is this JSON file for? We will eventually wire things up with Laravel acting as a RESTful API for our app, and will have it return results as JSON. This time.json file is a simple way to get started with some mocked out data without the need for a backend just yet. It allows us to use ngResource off the bat, and when we’re ready, we can simply switch out the call to this static file for a call to the RESTful API that we build with Laravel.

Now that we know what it’s for, let’s put some data in the time.json file. Feel free to change things up with your own details:

/* data/time.json */
    
[
    {
      "id":1,
      "user_id":1,
      "user_firstname":"Ryan",
      "user_lastname":"Chenkie",
      "start_time":"2015-02-21T18:56:48Z",
      "end_time":"2015-02-21T20:33:10Z",
      "comment": "Initial project setup."
    },
    {
      "id":2,
      "user_id":1,
      "user_firstname":"Ryan",
      "user_lastname":"Chenkie",
      "start_time":"2015-02-27T10:22:42Z",
      "end_time":"2015-02-27T14:08:10Z",
      "comment": "Review of project requirements and notes for getting started."
    },
    {
      "id":3,
      "user_id":1,
      "user_firstname":"Ryan",
      "user_lastname":"Chenkie",
      "start_time":"2015-03-03T09:55:32Z",
      "end_time":"2015-03-03T12:07:09Z",
      "comment": "Front-end and backend setup."
    }
]   
Note: You must omit the file comment at the top of the JSON file in your actual code

Setting up the View

Let’s now setup our index.html file that will provide the structure to our single-page app. For the time being, we’ll put in the basic elements and link up our CSS and JavaScript files.

<!-- index.html -->

<!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>
        </nav>

        <div class="container">

        </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>

As you can see, we have bootstrapped our application on the body tag by setting ng-app to the timeTracker module. We’re also making use of the controller as syntax in our controller declaration. The controller as syntax is a big part of the AngularJS Style Guide. It can make for cleaner code because we are able to bind methods and properties directly onto the controller by using the this keyword, like you saw earlier when we setup the JavaScript files. This means that we don’t have to rely on using $scope.

We’ve now got the main structure for the application in place, but it isn’t doing much just yet. Next up we will fetch our sample data and display it.

Extra Styling

Here is some CSS that we’ll use for adjusting the elements to display properly:

/* css/style.css */
    
.time-numbers > *, .time-entry > * {
    display: inline-block;
}

.timepicker {
    margin-right: 15px;
}

@media (min-width: 546px) {
    .time-entry-comment {
        position: relative;
        top: 60px;
    }
}

Fetching the Data

We’re going to let the time service be responsible for fetching data. We’ve already seen that a reference exists for ngResource to talk to the static time.json file, but now we need to get a method in place to fetch the data. Let’s update our time.js file with a method to handle this request:

/* scripts/services/time.js */
    
(function() {
    
    'use strict';

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

        function time($resource) {

            // ngResource call to our static data
            var Time = $resource('data/time.json');

            function getTime() {
                // $promise.then allows us to intercept the results
                // which we will use later
                return Time.query().$promise.then(function(results) {
                    return results;
                }, function(error) { // Check for errors
                    console.log(error);
                });
            }

            return {
                getTime: getTime,
            }
        }
            
})();

Here we’ve added a method called getTime. This method is responsible for using ngResource to make a query to the static data and then return the results. The $promise.then syntax is necessary in our case because a little later we will be modifying the returned array on the fly. For now, it is simply returning the results of the query. Finally, we’ve modified the object that the time service returns to include the getTime method.

Next, let’s hook into this new method in our controller:

/* scripts/controllers/TimeEntry.js */
    
(function() {
    
    'use strict';

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

        function TimeEntry(time) {

            // vm is our capture variable
            var vm = this;

            vm.timeentries = [];

            // Fetches the time entries from the static JSON file
            // and puts the results on the vm.timeentries array
            time.getTime().then(function(results) {
                vm.timeentries = results;
                console.log(vm.timeentries);
            }, function(error) { // Check for errors
                console.log(error);
            });

        }
})();

We use then to grab the results and put them on our vm.timeentries array. To make sure we’re getting data back, we can log the array out to the console. If everything worked properly, you should see it show up in developer tools:

time-tracker-1-2

Finally, let’s have the results show up in our application. We can add in some additional HTML and put the templating in place to display our results. At this time, we can also add in the controls for making new time entries:

<!-- index.html --> 
...
<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 timeentry" 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>
                    </div>
                </div>
            </div>
        </div>
    </div>

</body>    
...

There’s a lot going on here, so let’s go through the changes step by step.

  • Firstly, we’ve made some changes to the navbar. We’ve added in the timepicker directive that is offered by UI Bootstrap and set some default parameters for it. The timepicker directive gives us a nice little widget that will allow users to clock-in and clock-out at specific times.
  • We’ve also added a field for users to include comments with their time entries. You’ll see that for all of these we have specified names for them on ng-model and that they are prefaced with vm. This is because we are using the controller as syntax and need an alias for our View Model (you can use anything as an alias, but we’ll stick to vm for now).
  • On our “Log Time” button we have specified that a method called logNewTime is called when the button is clicked. We haven’t defined this method yet, but we will in the next steps.

Below the navbar, we are setting up an ng-repeat to display the time entries. You’ll see that for the date of the time entry we are taking the end_date and formatting it with an Angular date filter.

If everything is wired up correctly, you should see our entries displayed:

time-tracker-1-3

Things are looking good so far, but we aren’t actually displaying the amount of time that is involved for each time entry. In the next section we’re going to get the hour and minute calculations setup with Moment.js, after which we can update our app to display the amount of time logged.

Calculating Time with Moment.js

You might be wondering why we don’t bother just storing a calculated value for the total amount of time in each time entry. This is a good question, and we could certainly set things up this way; however, when it comes to database schemas, it’s a best practice to not store calculated values. If we simply store the start and end times, we have more flexibility when it comes to updating the database schema or when we make edits to our time entries. Say, for instance, that we wanted to update the start time for a given entry. If we store calculated values, we would have to write additional logic that updates the total time once our start time is updated. It is much simpler if we calculate things on the fly.

Time Diff and Total Time

Moment.js gives us some great tools, and for this task we want the diff and duration methods. Let’s setup two new methods in our time service, one for getting the time difference and the other for getting the total time:

/* scripts/services/time.js */
    
(function() {
    
    'use strict';

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

        function time($resource) {

            // ngResource call to our static data
            var Time = $resource('data/time.json');

            // $promise.then allows us to intercept the results of the 
            // query so we can add the loggedTime property
            function getTime() {
                return Time.query().$promise.then(function(results) {
                    angular.forEach(results, function(result) {

                        // Add the loggedTime property which calls 
                        // getTimeDiff to give us a duration object
                        result.loggedTime = getTimeDiff(result.start_time, result.end_time);
                    });
                    return results;
                }, function(error) { // Check for errors
                    console.log(error);
                });
            }

            // Use Moment.js to get the duration of the time entry
            function getTimeDiff(start, end) {
                var diff = moment(end).diff(moment(start));
                var duration = moment.duration(diff);
                return {
                    duration: duration
                }
            }
            
            // Add up the total time for all of our time entries
            function getTotalTime(timeentries) {
                var totalMilliseconds = 0;

                angular.forEach(timeentries, function(key) {
                    totalMilliseconds += key.loggedTime.duration._milliseconds;
                });

                // After 24 hours, the Moment.js duration object
                // reports the next unit up, which is days.
                // Using the asHours method and rounding down with
                // Math.floor instead gives us the total hours
                return {
                    hours: Math.floor(moment.duration(totalMilliseconds).asHours()),
                    minutes: moment.duration(totalMilliseconds).minutes()
                }
            }

            return {
                getTime: getTime,
                getTimeDiff: getTimeDiff,
                getTotalTime: getTotalTime
            }
        }
})();

Our getTimeDiff method has two parameters, a start time and an end time, which will be the times at which the user clocks in and clocks out. To find the difference between the start and end times, we use Moment’s diff method, which returns the total time in milliseconds. We could work with the total number of milliseconds for each time entry and do the math to get our total number of hours, but that could get cumbersome. Instead, let’s use Moment’s duration method. It will return an object containing a nice breakdown of the total time spent on the task.

time-tracker-1-4

Finally, we return an object with a duration key that equals the duration we just derived. When we exceed 24 hours worth of time entries, the Moment.js duration object will report the next unit up, which is days. We could use this unit, but I think it’s a bit nicer to work with hours as the highest unit, so to get around this we use the asHours method that Moment.js offers. We use Math.floor to round it down and then keep using minutes from the duration object to report the number of minutes.

You’ll also notice that we’ve amended the getTime method with an angular.forEach loop. This loop allows us to intercept the results that are returned from our call to the static JSON file and add in a new property called loggedTime, which is equal to the result of a call to our new getTimeDiff method. We pass in the start and end times found in the static data to the getTimeDiff method.

Finally, we want to have a way to calculate the total time that all of our time entries make up. To do this, we create a getTotalTime method that accepts our timeentries array, uses angular.forEach to loop through them, and adds the number of milliseconds from each to a variable called totalMilliseconds. Since we’ll want access to both the number of hours and number of minutes, we return an object with keys for each. We once again use Moment’s duration method here, but in this case we ask for access directly to hours and minutes.

Total Time in the View

Now that we have methods in place to calculate our time, let’s put them to use in the view.

<!-- index.html --> 
... 
<div class="container">
    <div class="col-sm-8">
        <div class="well timeentry" 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>    
...

We’ve made a few changes to the view to reflect our time calculations. Let’s first take a look at the new h2 and h4 tags that have been added into the time-numbers div. These are used to display the number of hours and minutes that each time entry makes up. For the number of hours, we’re putting some checks in place to determine whether we need to display the hour span, and then whether we need to pluralize the word “hour”.

Next, we’ve added in another box to the right which gives us the total number of hours that all of the displayed time entries make up. However, if you run this right now you’ll see that there are no values for the total time box, and that’s because we still need to complete this functionality in the controller.

time-tracker-1-5

Finishing Up the Controller

There are just a couple more pieces left to finish out in the controller before we have a working app. First, we need an object called totalTime that is responsible for tallying up the time from each time entry. Second, we need to complete the logNewTime method that we mentioned earlier. This method is called when we click the “Log Time” button up in the navbar.

Let’s handle the time aggregation by putting in a method called updateTotalTime:

/* scripts/controllers/TimeEntry.js */
    
(function() {
    
    'use strict';

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

        function TimeEntry(time) {

            // vm is our capture variable
            var vm = this;

            vm.timeentries = [];
        
            vm.totalTime = {};

            // Fetches the time entries from the static JSON file
            // and puts the results on the vm.timeentries array
            time.getTime().then(function(results) {
                vm.timeentries = results;
                updateTotalTime(vm.timeentries);            
            }, function(error) { // Check for errors
                console.log(error);
            });

            // Updates the values in the total time box by calling the
            // getTotalTime method on the time service
            function updateTotalTime(timeentries) {
                vm.totalTime = time.getTotalTime(timeentries);
            }
        }
    
})();

You’ll see that we’ve added an updateTotalTime method at the bottom. This method takes the timeentries array as an argument and updates the newly declared vm.totalTime object to equal the result of a call to the getTotalTime method from the time service. Like we saw earlier, the getTotalTime method from the time service takes the array of time entries and loops through to count the total number of milliseconds from each. It then aggregates all that time and uses Moment.js to find and return the total number of hours and minutes.

If we refresh the page we now see that the total number of hours and minutes are being displayed properly:

time-tracker-1-6

Finally, let’s add in the logNewTime method that will handle new time entries. Because we are working with static JSON data for demo purposes here, all we need to do to update the list is push a new object onto the already existing timeentries array. This is a temporary shim that will no longer be necessary once we get database persistence with Laravel working.

Here is the finished TimeEntry controller:

/* scripts/controllers/TimeEntry.js */
    
(function() {
    
    'use strict';

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

        function TimeEntry(time) {

            // vm is our capture variable
            var vm = this;

            vm.timeentries = [];

            vm.totalTime = {};

            // Initialize the clockIn and clockOut times to the current time.
            vm.clockIn = new Date();
            vm.clockOut = new Date();

            // Fetches the time entries from the static JSON file
            // and puts the results on the vm.timeentries array
            time.getTime().then(function(results) {
                vm.timeentries = results;
                updateTotalTime(vm.timeentries);            
            }, function(error) { // Check for errors
                console.log(error);
            });

            // Updates the values in the total time box by calling the
            // getTotalTime method on the time service
            function updateTotalTime(timeentries) {
                vm.totalTime = time.getTotalTime(timeentries);
            }
        
            // Submits 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;
                }

                vm.timeentries.push({
                    "user_id":1,
                    "user_firstname":"Ryan",
                    "user_lastname":"Chenkie",
                    "start_time":vm.clockIn,
                    "end_time":vm.clockOut,
                    "loggedTime": time.getTimeDiff(vm.clockIn, vm.clockOut),
                    "comment":vm.comment
                });

                updateTotalTime(vm.timeentries);

                vm.comment = "";
            }
        }
            
})();

There are few things going on in the logNewTime method, so let’s break it down.

Firstly, we want to make sure that new time entries make sense—the user shouldn’t be able to enter a clock-in time that is after the clock-out time. For that, we put in a simple conditional that checks whether the clock-in time is after the clock-out time, and if it is, alerts the user of the error. We’re also checking to make sure the user has actually logged some time and that they haven’t just left the clock-in and clock-out times the same.

Next, we push a new object onto our time entries array. Notice here that we’re specifying that start_time and end_time are equal to vm.clockIn and vm.clockOut respectively. These values come directly from the timepicker directives we used in our view. Because of the way the timepicker directive works, if the user doesn’t make a change to the clock-in and clock-out times, it will give null values. This can cause problems, so to get around it we are initializing vm.clockIn and vm.clockOut to the current date.

If you look again at the data we started with in the static time.json file, you’ll notice that there is no loggedTime property. This is because we have things setup in the time service to add this property on as the data is read. Now that we’re adding new time entries in after the initial data is loaded, we’ll need to specify this property. We can reuse the getTimeDiff method here, passing in the clock-in and clock-out times.

Finally, we make a call to our updateTotalTime method so that the total time box reflects the right amount of time. We also want to clear the comment field so that it is ready for the next time entry.

Wrapping Up

There we go—we’ve got a good start at a time tracking application! There are, however, some obvious limitations with the app as it is now:

  • We don’t have any database persistence
  • We’re hard-coding in the name of the user when we add new time entries
  • We don’t have a way to edit or delete the time entries

We’re going to fix these problems in the next part of this tutorial when we get a Laravel 5 backend running. We’ll have Laravel expose a RESTful API and let our Angular front-end consume it. We’ll also provide a way to log time as a specific user and edit or delete time entries. Stay tuned for more!

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!