In this article, we will be building a time tracking application using Vue and Laravel.
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:
To follow along in this article, you need the following requirements:
When you have all the requirements, we can proceed.
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:
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 fillable
s, 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 Timer
s.
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.
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.
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.
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.