Build A Support Ticket Application With Laravel – Part 2

Learn how to build a support ticket application with Laravel

Update (August 15, 2016): Fixed typos in code snippets and add "use Illuminate\Support\Facades\Auth" to "CommentsController" This is the second and concluding part of Build A Support Ticket Application With Laravel. In part 1, we setup the application and created our first ticket.

In case you missed it, you should check Build A Support Ticket Application With Laravel - Part 1 out because we will be continuing from where we stopped.

Displaying User Tickets

We start off by allowing our users to see a list of all the tickets they have created and from there they can go on to view a particular ticket. Add the code below to TicketsController

// TicketsController.php

// Remember to add the lines below at the top of the controller
// use App\User;
// use App\Ticket;
// use App\Category;
// use Illuminate\Support\Facades\Auth;

public function userTickets()
{
    $tickets = Ticket::where('user_id', Auth::user()->id)->paginate(10);
    $categories = Category::all();

    return view('tickets.user_tickets', compact('tickets', 'categories'));
}

We are getting the tickets created by the authenticated user by passing Auth::user()->id to the where() and getting only the 10 tickets to be displayed in a page by using Laravel paginate() and passing 10 to it.

This will prevent us from showing all tickets created by a user in a single page which can be awkward if the user has lots of tickets. The tickets together with the categories are passed to a view file which will we will create shortly. Before creating the view file, let's create the routes that will handle displaying all tickets created by a user. Add the line below to routes.php

Route::get('my_tickets', 'TicketsController@userTickets');

With the routes created, lets go on and create the view. Create a new view file named user_tickets.blade.php in the tickets folder and paste the code below to it:

@extends('layouts.app')

@section('title', 'My Tickets')

@section('content')
    <div class="row">
        <div class="col-md-10 col-md-offset-1">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <i class="fa fa-ticket"> My Tickets</i>
                </div>

                <div class="panel-body">
                    @if ($tickets->isEmpty())
                        <p>You have not created any tickets.</p>
                    @else
                        <table class="table">
                            <thead>
                                <tr>
                                    <th>Category</th>
                                    <th>Title</th>
                                    <th>Status</th>
                                    <th>Last Updated</th>
                                </tr>
                            </thead>
                            <tbody>
                            @foreach ($tickets as $ticket)
                                <tr>
                                    <td>
                                    @foreach ($categories as $category)
                                        @if ($category->id === $ticket->category_id)
                                            {{ $category->name }}
                                        @endif
                                    @endforeach
                                    </td>
                                    <td>
                                        <a href="{{ url('tickets/'. $ticket->ticket_id) }}">
                                            #{{ $ticket->ticket_id }} - {{ $ticket->title }}
                                        </a>
                                    </td>
                                    <td>
                                    @if ($ticket->status === 'Open')
                                        <span class="label label-success">{{ $ticket->status }}</span>
                                    @else
                                        <span class="label label-danger">{{ $ticket->status }}</span>
                                    @endif
                                    </td>
                                    <td>{{ $ticket->updated_at }}</td>
                                </tr>
                            @endforeach
                            </tbody>
                        </table>

                        {{ $tickets->render() }}
                    @endif
                </div>
            </div>
        </div>
    </div>
@endsection

First, we check if there are tickets and then display them within a table. For the ticket category, we check if the ticket category_id is equal to the category id then display the name of the category. Lastly the render() will help display the pagination links.

Now if you visit the route /my_tickets you should see a page listing the tickets (in my case, just a ticket) that has been created my the authenticated user like the image below:

Showing A Ticket

Now that a user can see a list of all the tickets he/she has created, wouldn't it be nice if the user could view a particular ticket? Of course.

Let's tackle that. We want the user to be able to access a particular ticket by this route tickets/ticket_id.

Okay, let's create the route in routes.php

Route::get('tickets/{ticket_id}', 'TicketsController@show');

As you can see, when the user hits that route, the show(), which we have yet to create, will be triggered. Add this code below to TicketsController:

// app/Http/Controllers/TicketsController.php

public function show($ticket_id)
{
    $ticket = Ticket::where('ticket_id', $ticket_id)->firstOrFail();

    $category = $ticket->category;

    return view('tickets.show', compact('ticket', 'category'));
}

Here show() accepts an argument ticket_id which will be passed from the route, and we get the ticket with that ticket_id. Remember from Part 1 where we define our Ticket to Category relationship. Using $ticket->category will get the category which the ticket belongs to. And finally we return a view file with the ticket and category passed to it. Create a view file in the tickets folder with the name show.blade.php and paste the code below into it:

// /resources/views/tickets/show.blade.php

@extends('layouts.app')

@section('title', $ticket->title)

@section('content')
    <div class="row">
        <div class="col-md-10 col-md-offset-1">
            <div class="panel panel-default">
                <div class="panel-heading">
                    #{{ $ticket->ticket_id }} - {{ $ticket->title }}
                </div>

                <div class="panel-body">
                    @include('includes.flash')

                    <div class="ticket-info">
                        <p>{{ $ticket->message }}</p>
                        <p>Categry: {{ $category->name }}</p>
                        <p>
                        @if ($ticket->status === 'Open')
                            Status: <span class="label label-success">{{ $ticket->status }}</span>
                        @else
                            Status: <span class="label label-danger">{{ $ticket->status }}</span>
                        @endif
                        </p>
                        <p>Created on: {{ $ticket->created_at->diffForHumans() }}</p>
                    </div>

                    <hr>

                    <div class="comment-form">
                        <form action="{{ url('comment') }}" method="POST" class="form">
                            {!! csrf_field() !!}

                            <input type="hidden" name="ticket_id" value="{{ $ticket->id }}">

                            <div class="form-group{{ $errors->has('comment') ? ' has-error' : '' }}">
                                <textarea rows="10" id="comment" class="form-control" name="comment"></textarea>

                                @if ($errors->has('comment'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('comment') }}</strong>
                                    </span>
                                @endif
                            </div>

                            <div class="form-group">
                                <button type="submit" class="btn btn-primary">Submit</button>
                            </div>
                        </form>
                </div>
            </div>
        </div>
    </div>
@endsection

You should get something like the image below when you click on the link of a particular ticket.

If you noticed, there is a text box which will allow the user to reply/comment on ticket. Not to worry we'll get to it shortly. We'll be updating the show view too to display the comments on the ticket.

Commenting On A Ticket

Now let's allow the user to comment on a ticket. But first we need a place to store the comments. Create a Comment model and a migration file with the command below:

php artisan make:model Comment -m

Open the migration file just created and update with the code below:

Schema::create('comments', function (Blueprint $table) {
    $table->increments('id');
    $table->integer('ticket_id')->unsigned();
    $table->integer('user_id')->unsigned();
    $table->text('comment');
    $table->timestamps();
});

Run the migration

php artisan migrate

Open Comment model and add the following to it:

// Comment.php

protected $fillable = [
    'ticket_id', 'user_id', 'comment'
];

The above will define the fields that can be mass assigned.

Ticket To Comment Relationship

A Comment can belong to a Ticket, while a Ticket can have many Comments. This is a one to many relationship and we will use Eloquent to setup the relationship. Open the Comment model and paste the code below into it:

// Comment.php

public function ticket()
{
    return $this->belongsTo(Ticket::class);
}

Now let's edit the Ticket model in the same manner to add the inverse relationship:

// Ticket.php

public function comments()
{
    return $this->hasMany(Comment::class);
}

User To Comment Relationship

A Comment can belong to a User, while a User can have many Comments. This is a one to many relationship and we will use Eloquent to setup the relationship. Open the Comment model and paste the code below into it:

// Comment.php

public function user()
{
    return $this->belongsTo(User::class);
}

Now let's edit the User model in the same manner to add the inverse relationship:

// User.php

public function comments()
{
    return $this->hasMany(Comment::class);
}

User To Ticket Relationship

A Ticket can belong to a User, while a User can have many Ticket. This is a one to many relationship and we will use Eloquent to setup the relationship. Open the User model and paste the code below into it:

// User.php

public function tickets()
{
    return $this->hasMany(Ticket::class);
}

Now let's edit the Ticket model in the same manner to add the inverse relationship:

// Ticket.php

public function user()
{
    return $this->belongsTo(User::class);
}

Comment Controller

We need to create a new controller that will handle all comments specific logics. Run the code below to create a CommentsController.

php artisan make:controller CommentsController

The CommentsController will have only one method called postComment().

// Remember to add the lines below at the top of the controller
// use App\User;
// use App\Ticket;
// use App\Comment;
// use App\Mailers\AppMailer;
// use Illuminate\Support\Facades\Auth;

public function postComment(Request $request, AppMailer $mailer)
{
    $this->validate($request, [
        'comment'   => 'required'
    ]);

        $comment = Comment::create([
            'ticket_id' => $request->input('ticket_id'),
            'user_id'   => Auth::user()->id,
            'comment'   => $request->input('comment'),
        ]);

        // send mail if the user commenting is not the ticket owner
        if ($comment->ticket->user->id !== Auth::user()->id) {
            $mailer->sendTicketComments($comment->ticket->user, Auth::user(), $comment->ticket, $comment);
        }

        return redirect()->back()->with("status", "Your comment has be submitted.");
}

What the postComment() does is simple; make sure the comment box is filled, store a comment in the comments table with the ticket_id, user_id and the actually comment. Then send an email, if the user commenting is not the ticket owner (that is, if the comment was made by an admin, an email will be sent to the ticket owner). And finally display a status message.

You will notice that I used the relationships defined above extensively. Let me further explain the code snippet.

// send mail if the user commenting is not the ticket owner

if ($comment->ticket->user->id !== Auth::user()->id) {
    $mailer->sendTicketComments($comment->ticket->user, Auth::user(), $comment->ticket, $comment);
}

I check to see if the user id of the ticket that was commentted on is not equal to the authenticated user id. If it is (that is, ticket owner made the comment) there is no point sending an email to the user since he/she is the one that made the comment. If it is not (that is, an admin made the comment) an email is sent to the ticket owner.

Comment Form

Let's take a closer look at the comment box included in the show ticket view.

<div class="comment-form">
    <form action="{{ url('comment') }}" method="POST" class="form">
        {!! csrf_field() !!}

        <input type="hidden" name="ticket_id" value="{{ $ticket->id }}">

        <div class="form-group{{ $errors->has('comment') ? ' has-error' : '' }}">
            <textarea rows="10" id="comment" class="form-control" name="comment"></textarea>
            @if ($errors->has('comment'))
                <span class="help-block">
                    <strong>{{ $errors->first('comment') }}</strong>
                </span>
            @endif
        </div>

        <div class="form-group">
            <button type="submit" class="btn btn-primary">Submit</button>
        </div>
    </form>
</div>

The form will be POSTed to comment route which we are yet to create. Go on and add this to routes.php

Route::post('comment', 'CommentsController@postComment');

Now you should be able to comment on a ticket but even after commenting, you won't see the comment yet because we haven't updated the show view to display comments. First we need to update the show() to also fetch comments and pass it to the view.

// TicketsController.php

public function show($ticket_id)
{
    $ticket = Ticket::where('ticket_id', $ticket_id)->firstOrFail();

    $comments = $ticket->comments;

    $category = $ticket->category;

    return view('tickets.show', compact('ticket', 'category', 'comments'));
}

Also update the show view, add the code below just after <hr> to do just that.

// resources/views/tickets/show.blade.php

<div class="comments">
    @foreach ($comments as $comment)
        <div class="panel panel-@if($ticket->user->id === $comment->user_id) {{"default"}}@else{{"success"}}@endif">
            <div class="panel panel-heading">
                {{ $comment->user->name }}
                <span class="pull-right">{{ $comment->created_at->format('Y-m-d') }}</span>
            </div>

            <div class="panel panel-body">
                {{ $comment->comment }}     
            </div>
        </div>
    @endforeach
</div>

Sending Comment Email

Remember from part 1, we created a dedicated class called AppMailler that will handle all emails that we will be sending? From the postComment() in the CommentsController, I called a sendTicketComments() which does not exist yet, passing along some arguments. Now we will create this method. Open app/Mailers/AppMailer.php and we will add the sendTicketComments() just above the deliver().

// app/Mailers/AppMailer.php

public function sendTicketComments($ticketOwner, $user, Ticket $ticket, $comment)
{
    $this->to = $ticketOwner->email;
    $this->subject = "RE: $ticket->title (Ticket ID: $ticket->ticket_id)";
    $this->view = 'emails.ticket_comments';
    $this->data = compact('ticketOwner', 'user', 'ticket', 'comment');

    return $this->deliver();
}

We need one more view file. This view file will be inside theemails folder in the view directory. Go on and create ticket_comments.blade.php and add:

// resources/views/emails/ticket_comments.blade.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Suppor Ticket</title>
</head>
<body>
    <p>
        {{ $comment->comment }}
    </p>

    ---
    <p>Replied by: {{ $user->name }}</p>

    <p>Title: {{ $ticket->title }}</p>
    <p>Title: {{ $ticket->ticket_id }}</p>
    <p>Status: {{ $ticket->status }}</p>

    <p>
        You can view the ticket at any time at {{ url('tickets/'. $ticket->ticket_id) }}
    </p>

</body>
</html>

Marking Ticket As Closed

The last feature of the our Support Ticket application is the ability to mark ticket as closed. But a ticket can only be marked as closed by an admin. So we need to setup an admin. Remember from part 1, that when we created a our users table, we created a column called is_admin with a default value of 0 which indicate not an admin. A quick refreshener

$table->integer('is_admin')->unsigned()->default(0);

AdminMiddleware

We are going to create an AdminMiddleware which will allow only an admin access to an admin only area. Run the command below to create AdminMiddleware

php artisan make:middleware AdminMiddleware

Update the handle() with:

// AdminMiddleware.php
// Remember to add the line below at the top
// use Illuminate\Support\Facades\Auth;

public function handle($request, Closure $next)
{
    if (Auth::user()->is_admin !== 1) {
        return redirect('home');
    } 

    return $next($request);
}

We need to register our newly created middleware. Open app/Http/Kernel.php under the $routeMiddleware array, and add the line below to it

'admin' => \App\Http\Middleware\AdminMiddleware::class,

Now that the admin middleware has been registered, lets go on and create our admin specific routes. Add the following to routes.php

Route::group(['prefix' => 'admin', 'middleware' => 'admin'], function() {
    Route::get('tickets', 'TicketsController@index');
    Route::post('close_ticket/{ticket_id}', 'TicketsController@close');
});

The code above is pretty straightforward, define some routes not only an authenticated user can access but must also be an admin.

// TicketsController.php

public function index()
{
    $tickets = Ticket::paginate(10);
    $categories = Category::all();

    return view('tickets.index', compact('tickets', 'categories'));
}

The first route /admin/tickets will trigger the index() on the TicketsController which will get all the tickets that have been created. For the view file, create a file index.blade.php in the tickets folder and paste the code into it.


// resources/views/tickets/index.blade.php

@extends('layouts.app')

@section('title', 'All Tickets')

@section('content')
    <div class="row">
        <div class="col-md-10 col-md-offset-1">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <i class="fa fa-ticket"> Tickets</i>
                </div>

                <div class="panel-body">
                    @if ($tickets->isEmpty())
                        <p>There are currently no tickets.</p>
                    @else
                        <table class="table">
                            <thead>
                                <tr>
                                    <th>Category</th>
                                    <th>Title</th>
                                    <th>Status</th>
                                    <th>Last Updated</th>
                                    <th style="text-align:center" colspan="2">Actions</th>
                                </tr>
                            </thead>
                            <tbody>
                            @foreach ($tickets as $ticket)
                                <tr>
                                    <td>
                                    @foreach ($categories as $category)
                                        @if ($category->id === $ticket->category_id)
                                            {{ $category->name }}
                                        @endif
                                    @endforeach
                                    </td>
                                    <td>
                                        <a href="{{ url('tickets/'. $ticket->ticket_id) }}">
                                            #{{ $ticket->ticket_id }} - {{ $ticket->title }}
                                        </a>
                                    </td>
                                    <td>
                                    @if ($ticket->status === 'Open')
                                        <span class="label label-success">{{ $ticket->status }}</span>
                                    @else
                                        <span class="label label-danger">{{ $ticket->status }}</span>
                                    @endif
                                    </td>
                                    <td>{{ $ticket->updated_at }}</td>
                                    <td>
                                        <a href="{{ url('tickets/' . $ticket->ticket_id) }}" class="btn btn-primary">Comment</a>
                                    </td>
                                    <td>
                                        <form action="{{ url('admin/close_ticket/' . $ticket->ticket_id) }}" method="POST">
                                            {!! csrf_field() !!}
                                            <button type="submit" class="btn btn-danger">Close</button>
                                        </form>
                                    </td>
                                </tr>
                            @endforeach
                            </tbody>
                        </table>

                        {{ $tickets->render() }}
                    @endif
                </div>
            </div>
        </div>
    </div>
@endsection

The second route close_ticket/ticket_id will trigger the close() on the ticketsController. The method will handle closing a particular ticket.

// TicketsController.php

public function close($ticket_id, AppMailer $mailer)
{
    $ticket = Ticket::where('ticket_id', $ticket_id)->firstOrFail();

    $ticket->status = 'Closed';

    $ticket->save();

    $ticketOwner = $ticket->user;

    $mailer->sendTicketStatusNotification($ticketOwner, $ticket);

    return redirect()->back()->with("status", "The ticket has been closed.");
}

We get the ticket with the ticket_id passed from the route, and set it status to Closed and save it. Then send an email by calling sendTicketStatusNotification() (which we will create shortly) on the AppMailer class. And finally display a status message.

Sending Ticket Status Notification

Add the code below to app/Mailers/AppMailer.php just above the deliver()

// app/Mailers/AppMailer.php

public function sendTicketStatusNotification($ticketOwner, Ticket $ticket)
{
    $this->to = $ticketOwner->email;
    $this->subject = "RE: $ticket->title (Ticket ID: $ticket->ticket_id)";
    $this->view = 'emails.ticket_status';
    $this->data = compact('ticketOwner', 'ticket');

    return $this->deliver();
}

We need one more view file. This view file will be inside theemails folder in the view directory. Go on and create ticket_status.blade.php and add:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Suppor Ticket Status</title>
</head>
<body>
    <p>
        Hello {{ ucfirst($ticketOwner->name) }},
    </p>
    <p>
        Your support ticket with ID #{{ $ticket->ticket_id }} has been marked has resolved and closed.
    </p>
</body>
</html>

Allowing Only Authenticated Users Access

Only authenticated users can use our Support Ticket Application. Laravel has us covered by providing Authenticate middleware which will allow only authenticated users access to the routes in which the auth middleware is added to. We will be add the auth middleware to TicketsController contructor:

// TicketsController.php

public function __construct()
{
    $this->middleware('auth');
}

This way, all the methods in TicketsController will only be accessible by authenticated users.

Finishing Touches

When we ran the make:auth command in part 1, Laravel created some scaffolding for us which we are going to make use of. A HomeController and a home. view. We'll be making some few changes to the home view. Update the home view with:

@extends('layouts.app')

@section('title', 'Dashboard')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-10 col-md-offset-1">
            <div class="panel panel-default">
                <div class="panel-heading">Dashboard</div>

                <div class="panel-body">
                    <p>You are logged in!</p>

                    @if (Auth::user()->is_admin)
                        <p>
                            See all <a href="{{ url('admin/tickets') }}">tickets</a>
                        </p>
                    @else
                        <p>
                            See all your <a href="{{ url('my_tickets') }}">tickets</a> or <a href="{{ url('new_ticket') }}">open new ticket</a>
                        </p>
                    @endif
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Conclusion

That's it. Our Support Ticket Application is ready. You can check out the demo and also the project repository to view the complete source code. If you have any questions about the tutorial, let me know in the comments below

Chimezie Enyinnaya

web developer [PHP Laravel VueJS|AngularJS] | movie lover | I run http://openlaravel.com | blogs at http://itoocode.com