Create a time tracking application using Laravel and Vue

create-time-tracking-application-using-laravel-vue-header.png

In this article, we will be building a time tracking application using Vue and Laravel.

Introduction

In this article we will be building a time tracking application using Laravel, also one of the most popular PHP frameworks and Vue, one of the most popular JavaScript frameworks.

Toggl is a time tracking application that allows you know how much time is spent on a particular task. With Toggl you can add multiple projects and track how much time you have spent on each of the features in that project. This is useful because it will give a detailed report of how much time a project cost.

The application will look like this when we are done:

Requirements

To follow along in this article, you need the following requirements:

When you have all the requirements, we can proceed.

Setting Up Our Laravel Application

The first thing we would do is set up our Laravel application. In a terminal, run the command below to create a new Laravel application:

    $ laravel new timetracker

When the application is created, cd to the directory and open it in your favorite editor. The first thing we will do is create our migrations, controllers and models. In the terminal, run the command below to create them:

1$ php artisan make:model Project -mc
2    $ php artisan make:model Timer -mc

The command above will not only create the Models, it will also create the migrations and controllers due to the -mc flag. This is a great way to quickly create several components that would have otherwise been done one after the other.

Here is a screenshot of what we get after running the commands above:

Creating Our Project Database Migration, Model and Controller

Let us start editing the files we just generated. First, we will start with the Project migration, model and the controller. In the databases/migrations directory, open the *_create_projects_table.php file and paste the following code:

1<?php
2
3    use Illuminate\Support\Facades\Schema;
4    use Illuminate\Database\Schema\Blueprint;
5    use Illuminate\Database\Migrations\Migration;
6
7    class CreateProjectsTable extends Migration
8    {
9        public function up()
10        {
11            Schema::create('projects', function (Blueprint $table) {
12                $table->increments('id');
13                $table->string('name');
14                $table->unsignedInteger('user_id');
15                $table->foreign('user_id')->references('id')->on('users');
16                $table->timestamps();
17            });
18        }
19
20        public function down()
21        {
22            Schema::dropIfExists('projects');
23        }
24    }

The migration above is a representation of the database schema.

Next, open the Project model file, ./app/Project.php, and paste the code below:

1<?php
2    namespace App;
3
4    use Illuminate\Database\Eloquent\Model;
5
6    class Project extends Model
7    {
8        /**
9         * {@inheritDoc}
10         */
11        protected $fillable = ['name', 'user_id'];
12
13        /**
14        * {@inheritDoc}
15        */
16        protected $with = ['user'];
17
18        /**
19         * Get associated user.
20         *
21         * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
22         */
23        public function user()
24        {
25            return $this->belongsTo(User::class);
26        }
27
28        /**
29         * Get associated timers.
30         *
31         * @return \Illuminate\Database\Eloquent\Relations\HasMany
32         */
33        public function timers()
34        {
35            return $this->hasMany(Timer::class);
36        }
37
38        /**
39         * Get my projects
40         *
41         * @param  \Illuminate\Database\Eloquent\Builder $query
42         * @return \Illuminate\Database\Eloquent\Builder
43         */
44        public function scopeMine($query)
45        {
46            return $query->whereUserId(auth()->user()->id);
47        }
48    }

In the Eloquent model above, we define the fillables, and the with array used to specify relationships to eagerly load.

We define the user relationship, which says a project belongs to one User. We also define a timers relationship which says every project has many Timers.

Finally, we define an Eloquent Query Scope in the scopeMine method. Query scopes make it easier to mask complex queries in an Eloquent model. The scopeMine is supposed to add the where a query that restricts the projects to only those belonging to the current user.

The next file will be the ProjectController. Open the controller file, ./app/Http/ProjectController.php, and paste the following code:

1<?php
2    namespace App\Http\Controllers;
3
4    use App\Project;
5    use Illuminate\Http\Request;
6
7    class ProjectController extends Controller
8    {
9        public function __construct()
10        {
11            $this->middleware('auth');
12        }
13
14        public function index()
15        {
16            return Project::mine()->with('timers')->get()->toArray();
17        }
18
19        public function store(Request $request)
20        {
21            // returns validated data as array
22            $data = $request->validate(['name' => 'required|between:2,20']);
23
24            // merge with the current user ID
25            $data = array_merge($data, ['user_id' => auth()->user()->id]);
26
27            $project = Project::create($data);
28
29            return $project ? array_merge($project->toArray(), ['timers' => []]) : false;
30        }
31    }

In the controller above, we define the middleware auth so that only authenticated users can access methods on the controller.

In the index method, we return all the projects belonging to the logged in user. By adding with('timers') we eager load the timers relationship. In the store method, we just create a new Project for the user.

Creating Our Timer Database Migration, Model and Controller
The next set of components to edit will be the Timer. Open the timer migration file *_create_timers_table.php and paste the following in the file:

1<?php
2
3    use Illuminate\Support\Facades\Schema;
4    use Illuminate\Database\Schema\Blueprint;
5    use Illuminate\Database\Migrations\Migration;
6
7    class CreateTimersTable extends Migration
8    {
9        public function up()
10        {
11            Schema::create('timers', function (Blueprint $table) {
12                $table->increments('id');
13                $table->string('name');
14                $table->unsignedInteger('project_id');
15                $table->unsignedInteger('user_id');
16                $table->timestamp('started_at');
17                $table->timestamp('stopped_at')->default(null)->nullable();
18                $table->timestamps();
19
20                $table->foreign('user_id')->references('id')->on('users');
21                $table->foreign('project_id')->references('id')->on('projects');
22            });
23        }
24
25        public function down()
26        {
27            Schema::dropIfExists('timers');
28        }
29    }

Next, we will update the Timer model. Open the class and, in the file, paste the following code:

1<?php
2    namespace App;
3
4    use Illuminate\Database\Eloquent\Model;
5
6    class Timer extends Model
7    {
8        /**
9         * {@inheritDoc}
10         */
11        protected $fillable = [
12          'name', 'user_id', 'project_id', 'stopped_at', 'started_at'
13        ];
14
15        /**
16        * {@inheritDoc}
17        */
18        protected $dates = ['started_at', 'stopped_at'];
19
20        /**
21         * {@inheritDoc}
22         */
23        protected $with = ['user'];
24
25        /**
26         * Get the related user.
27         *
28         * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
29         */
30        public function user()
31        {
32            return $this->belongsTo(User::class);
33        }
34
35        /**
36         * Get the related project
37         *
38         * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
39         */
40        public function project()
41        {
42            return $this->belongsTo(Project::class);
43        }
44
45        /**
46         * Get timer for current user.
47         *
48         * @param  \Illuminate\Database\Eloquent\Builder $query
49         * @return \Illuminate\Database\Eloquent\Builder
50         */
51        public function scopeMine($query)
52        {
53            return $query->whereUserId(auth()->user()->id);
54        }
55
56        /**
57         * Get the running timers
58         *
59         * @param  \Illuminate\Database\Eloquent\Builder $query
60         * @return \Illuminate\Database\Eloquent\Builder
61         */
62        public function scopeRunning($query)
63        {
64            return $query->whereNull('stopped_at');
65        }
66    }

In the model above, we defined two relationships. The first is user which states that the timer belongs to one User. The second relationship is the project relationship which states that a Timer belongs to a Project.

We also define two query scopes, the scopeMine which adds where a query for all the timers belonging to the user, and the scopeRunning which adds a where a query for all timers that are running.

Next, let’s update the TimerController too. Paste the code below in the controller:

1<?php
2    namespace App\Http\Controllers;
3
4    use App\Timer;
5    use App\Project;
6    use Carbon\Carbon;
7    use Illuminate\Http\Request;
8    use Illuminate\Support\Facades\Auth;
9
10    class TimerController extends Controller
11    {
12        public function store(Request $request, int $id)
13        {
14            $data = $request->validate(['name' => 'required|between:3,100']);
15
16            $timer = Project::mine()->findOrFail($id)
17                                    ->timers()
18                                    ->save(new Timer([
19                                        'name' => $data['name'],
20                                        'user_id' => Auth::user()->id,
21                                        'started_at' => new Carbon,
22                                    ]));
23
24            return $timer->with('project')->find($timer->id);
25        }
26
27        public function running()
28        {
29            return Timer::with('project')->mine()->running()->first() ?? [];
30        }
31
32        public function stopRunning()
33        {
34            if ($timer = Timer::mine()->running()->first()) {
35                $timer->update(['stopped_at' => new Carbon]);
36            }
37
38            return $timer;
39        }
40    }

In the controller above, we have defined the store method. This method just creates a new timer and associates it with the loaded project.

The next method called running just returns active timers belonging to the current user. The final method is the stopRunning. It stops the actively running timer belonging to the current user.

Connecting to a Database
Next, we will connect our application to a Database. To do this, update the values of the DB_* keys in the .env file. To keep this application simple, we will be using the SQLite database driver.

Open the .env file and replace the values:

1DB_CONNECTION=mysql
2    DB_HOST=127.0.0.1
3    DB_PORT=3306
4    DB_DATABASE=homestead
5    DB_USERNAME=homestead
6    DB_PASSWORD=secret

with:

    DB_CONNECTION=sqlite 

Next, create an empty file in the databases directory named database.sqlite. That’s all. Your application is ready to use SQLite as it’s database.

To execute the available migrations we created earlier, run the following command on the terminal:

    $ php artisan migrate

Here is a sample response we will get after running the command:

Adding Authentication To Our Laravel Application
Next, let us add some authentication. To add authentication to our current project, run the command below in your terminal:

    $ php artisan make:auth

After running this article, a couple of files will be added to our project automatically. However, we don’t need to bother about them. That’s all for adding authentication.

Adding the Appropriate Routes
Since we have created our controllers, let’s create routes that point to the methods in them. Open the routes/web.php file and replace the content with the following:

1<?php
2
3    Auth::routes();
4    Route::redirect('/', '/home');
5    Route::get('/home', 'HomeController@index')->name('home');
6    Route::get('/projects', 'ProjectController@index');
7    Route::post('/projects', 'ProjectController@store');
8    Route::post('/projects/{id}/timers/stop', 'TimerController@stopRunning');
9    Route::post('/projects/{id}/timers', 'TimerController@store');
10    Route::get('/project/timers/active', 'TimerController@running');

On line 3 we have the Auth::routes(). Also, we have defined additional routes that will be useful for our application.

Setting Up Our Vue Application

When working with Vue in Laravel, you can either start from an entirely new application or use the in-built Vue integration. We will do the latter. In the terminal, run the command below to install the NPM dependencies:

    $ npm install

After the dependencies have been installed, you can run the command below to build your assets manually every time you make a change:

    $ npm run dev

? You can run **npm run watch** to start Laravel mix with automatic compiling of assets.

Creating Our Application’s Entry Point

The first thing we want to create is an entry point to our Vue application. The entry point will be the page that shows up immediately after a user logs in.

Before we continue, start a PHP server using Artisan by running the command below in a new terminal tab:

    $ php artisan serve

This should make your application available on this URL: 127.0.0.1:8000. Visit the URL and create a new account. When you have created an account, you will be automatically logged in and you should see a “Dashboard”. Let us use this as our Vue entry point.

Open the view that is responsible for this page, resources/views/home.blade.php, and paste in the following code:

1@extends('layouts.app')
2
3    @section('content')
4    <div class="container">
5        <div class="row">
6            <dashboard></dashboard>
7        </div>
8    </div>
9    @endsection

In the above, we have introduced a Vue component called dashboard. Let us create that component in Vue.

Open the resources/assets/js/app.js file and replace the contents with the following code:

1// Load other dependencies
2    require('./bootstrap');
3
4    // Load Vue
5    window.Vue = require('vue');
6
7    // Vue components!
8    Vue.component('dashboard', require('./components/DashboardComponent.vue'));
9
10    // Create a Vue instance
11    const app = new Vue({el: '#app'});

In the code above, we register a new Vue component called dashboard and include it from the ./components/DashboardComponent.vue file. Create that file and let’s build out our Vue component.

In the file, we will divide the process into template and script. The template will be the Vue HTML part and the script will be the Vue JavaScript.

Let’s create the Vue HTML part. In the file paste in the following code:

1<template>
2        <div class="col-md-8 col-md-offset-2">
3            <div class="no-projects" v-if="projects">
4
5                <div class="row">
6                    <div class="col-sm-12">
7                        <h2 class="pull-left project-title">Projects</h2>
8                        <button class="btn btn-primary btn-sm pull-right" data-toggle="modal" data-target="#projectCreate">New Project</button>
9                    </div>
10                </div>
11
12                <hr>
13
14                <div v-if="projects.length > 0">
15                    <div class="panel panel-default" v-for="project in projects" :key="project.id">
16                        <div class="panel-heading clearfix">
17                            <h4 class="pull-left">{{ project.name }}</h4>
18
19                            <button class="btn btn-success btn-sm pull-right" :disabled="counter.timer" data-toggle="modal" data-target="#timerCreate" @click="selectedProject = project">
20                                <i class="glyphicon glyphicon-plus"></i>
21                            </button>
22                        </div>
23
24                        <div class="panel-body">
25                            <ul class="list-group" v-if="project.timers.length > 0">
26                                <li v-for="timer in project.timers" :key="timer.id" class="list-group-item clearfix">
27                                    <strong class="timer-name">{{ timer.name }}</strong>
28                                    <div class="pull-right">
29                                        <span v-if="showTimerForProject(project, timer)" style="margin-right: 10px">
30                                            <strong>{{ activeTimerString }}</strong>
31                                        </span>
32                                        <span v-else style="margin-right: 10px">
33                                            <strong>{{ calculateTimeSpent(timer) }}</strong>
34                                        </span>
35                                        <button v-if="showTimerForProject(project, timer)" class="btn btn-sm btn-danger" @click="stopTimer()">
36                                            <i class="glyphicon glyphicon-stop"></i>
37                                        </button>
38                                    </div>
39                                </li>
40                            </ul>
41                            <p v-else>Nothing has been recorded for "{{ project.name }}". Click the play icon to record.</p>
42                        </div>
43                    </div>
44                    <!-- Create Timer Modal -->
45                    <div class="modal fade" id="timerCreate" role="dialog">
46                        <div class="modal-dialog modal-sm">
47                            <div class="modal-content">
48                                <div class="modal-header">
49                                    <button type="button" class="close" data-dismiss="modal">×</button>
50                                    <h4 class="modal-title">Record Time</h4>
51                                </div>
52                                <div class="modal-body">
53                                    <div class="form-group">
54                                        <input v-model="newTimerName" type="text" class="form-control" id="usrname" placeholder="What are you working on?">
55                                    </div>
56                                </div>
57                                <div class="modal-footer">
58                                    <button data-dismiss="modal" v-bind:disabled="newTimerName === ''" @click="createTimer(selectedProject)" type="submit" class="btn btn-default btn-primary"><i class="glyphicon glyphicon-play"></i> Start</button>
59                                </div>
60                            </div>
61                        </div>
62                    </div>
63                </div>
64                <div v-else>
65                    <h3 align="center">You need to create a new project</h3>
66                </div>
67                <!-- Create Project Modal -->
68                <div class="modal fade" id="projectCreate" role="dialog">
69                    <div class="modal-dialog modal-sm">
70                        <div class="modal-content">
71                            <div class="modal-header">
72                                <button type="button" class="close" data-dismiss="modal">×</button>
73                                <h4 class="modal-title">New Project</h4>
74                            </div>
75                            <div class="modal-body">
76                                <div class="form-group">
77                                    <input v-model="newProjectName" type="text" class="form-control" id="usrname" placeholder="Project Name">
78                                </div>
79                            </div>
80                            <div class="modal-footer">
81                                <button data-dismiss="modal" v-bind:disabled="newProjectName == ''" @click="createProject" type="submit" class="btn btn-default btn-primary">Create</button>
82                            </div>
83                        </div>
84                    </div>
85                </div>
86            </div>
87            <div class="timers" v-else>
88                Loading...
89            </div>
90        </div>
91    </template>

In the template we create a New Project button which loads up a #projectCreate Bootstrap modal. The modal contains a form that adds a new project when the submit button is clicked.

In there we loop through the projects array in Vue and, in that loop, we loop through each of the project’s timers and display them. We have also defined a few buttons such as the stop timer button and the add timer button which stops the timer or adds a new timer to the stack.

In the same file, paste this at the bottom of the file right after the closing template tag:

1<script>
2    import moment from 'moment'
3    export default {
4        data() {
5            return {
6                projects: null,
7                newTimerName: '',
8                newProjectName: '',
9                activeTimerString: 'Calculating...',
10                counter: { seconds: 0, timer: null },
11            }
12        },
13        created() {
14            window.axios.get('/projects').then(response => {
15                this.projects = response.data
16                window.axios.get('/project/timers/active').then(response => {
17                    if (response.data.id !== undefined) {
18                        this.startTimer(response.data.project, response.data)
19                    }
20                })
21            })
22        },
23        methods: {
24            /**
25             * Conditionally pads a number with "0"
26             */
27            _padNumber: number =>  (number > 9 || number === 0) ? number : "0" + number,
28
29            /**
30             * Splits seconds into hours, minutes, and seconds.
31             */
32            _readableTimeFromSeconds: function(seconds) {
33                const hours = 3600 > seconds ? 0 : parseInt(seconds / 3600, 10)
34                return {
35                    hours: this._padNumber(hours),
36                    seconds: this._padNumber(seconds % 60),
37                    minutes: this._padNumber(parseInt(seconds / 60, 10) % 60),
38                }
39            },
40
41            /**
42             * Calculate the amount of time spent on the project using the timer object.
43             */
44            calculateTimeSpent: function (timer) {
45                if (timer.stopped_at) {
46                    const started = moment(timer.started_at)
47                    const stopped = moment(timer.stopped_at)
48                    const time = this._readableTimeFromSeconds(
49                        parseInt(moment.duration(stopped.diff(started)).asSeconds())
50                    )
51                    return `${time.hours} Hours | ${time.minutes} mins | ${time.seconds} seconds`
52                }
53                return ''
54            },
55
56            /**
57             * Determines if there is an active timer and whether it belongs to the project
58             * passed into the function.
59             */
60            showTimerForProject: function (project, timer) {
61                return this.counter.timer &&
62                       this.counter.timer.id === timer.id &&
63                       this.counter.timer.project.id === project.id
64            },
65
66            /**
67             * Start counting the timer. Tick tock.
68             */
69            startTimer: function (project, timer) {
70                const started = moment(timer.started_at)
71
72                this.counter.timer = timer
73                this.counter.timer.project = project
74                this.counter.seconds = parseInt(moment.duration(moment().diff(started)).asSeconds())
75                this.counter.ticker = setInterval(() => {
76                    const time = this._readableTimeFromSeconds(++vm.counter.seconds)
77
78                    this.activeTimerString = `${time.hours} Hours | ${time.minutes}:${time.seconds}`
79                }, 1000)
80            },
81
82            /**
83             * Stop the timer from the API and then from the local counter.
84             */
85            stopTimer: function () {
86                window.axios.post(`/projects/${this.counter.timer.id}/timers/stop`)
87                            .then(response => {
88                                // Loop through the projects and get the right project...
89                                this.projects.forEach(project => {
90                                    if (project.id === parseInt(this.counter.timer.project.id)) {
91                                        // Loop through the timers of the project and set the `stopped_at` time
92                                        return project.timers.forEach(timer => {
93                                            if (timer.id === parseInt(this.counter.timer.id)) {
94                                                return timer.stopped_at = response.data.stopped_at
95                                            }
96                                        })
97                                    }
98                                });
99
100                                // Stop the ticker
101                                clearInterval(this.counter.ticker)
102
103                                // Reset the counter and timer string
104                                this.counter = { seconds: 0, timer: null }
105                                this.activeTimerString = 'Calculating...'
106                            })
107            },
108
109            /**
110             * Create a new timer.
111             */
112            createTimer: function (project) {
113                window.axios.post(`/projects/${project.id}/timers`, {name: this.newTimerName})
114                            .then(response => {
115                                project.timers.push(response.data)
116                                this.startTimer(response.data.project, response.data)
117                            })
118
119                this.newTimerName = ''
120            },
121
122            /**
123             * Create a new project.
124             */
125            createProject: function () {
126                window.axios.post('/projects', {name: this.newProjectName})
127                            .then(response => this.projects.push(response.data))
128
129                this.newProjectName = ''
130            }
131        },
132    }
133    </script>

In the Vue component script above we have started by declaring the data variables. We will be referring to them in the script and template sections of the component.

We have implemented a created method which is called automatically when the Vue component is created. Inside it we make a GET request to /projects, load up all the projects and assign them to the projects variable. We also check if there is an active timer and, if there is, we start it using the startTimer method.

Next, in the methods object, we define a couple of methods that will be available all around our Vue component. The components are well commented and each of them distinctively tells what they do.

In the startTimer method, we start a timer based on the started_at property of the time. In the stopTimer method, we send a request to the API and then stop the timer locally by calling clearInterval on the saved timer instance.

We also have a createProject and a createTimer method. These do just what they say by sending an API request to the Laravel backend and then adding the project/timer to the list of existing ones in Vue.

If you noticed, at the beginning of the script we tried to import Moment but we have not added it to our NPM dependencies. To do this run the command below:

    $ npm install --save moment

Now rebuild your assets using the code below:

    $ npm run dev

With the changes you can now load the page. You can create a PHP server using Artisan if you haven’t already done so using the code below:

    $ php artisan serve

You should now see your web application.

Conclusion

With the power of Laravel and Vue, you can create really modern web applications really quickly and this has demonstrated how quickly you can harness both technologies to create amazing applications.

Hopefully, you have learned a thing or two from the tutorial. If you have any questions or feedback please leave them below in the comment section.

The source code to the application in this article is available on GitHub.