Laravel Nova Metrics
Tags: laravel, nova, queries, widgets
Published: 2021-09-06 13:30:46

Since our web application is powered by laravel it makes sense to install Laravel Nova, so that we don't have to build out a custom dashboard for our Admins.  Nova is a dashboard for admins where when properly set up and configured properly, allow full CRUD for any Models.  It's pretty powerful and I won't go too in depth with it but one thing that managment wants is for "widgets" on the dashboard when you log in.  So a widget to them is something that shows any type of data that they request.  They come from a WordPress background so to them it's a widget.  To programmers who have to do all this, it would be a nova metric. 

"Nova metrics allow you to quickly gain insight on key business indicators for your application. For example, you may define a metric to display the total number of users added to your application per day, or the amount of weekly sales." This is taken directly from Nova docs and there are 3 types of metrics available.
1. Value - displays a single value
2. Trend - displays value over time in a line chart 
3. Partition - displays a pie chart for values
For todays post and for the vast majority of requests from management, I'll be using Value metrics.  

All of my value metrics will more or less have the same drop down values to calculate the value.  We came up with monthly (jan/feb/mar/etc...) and past 7 days.  The only real caveat about this is that come next year we'll have to add more values to the drop down.  It is what it is.
```
<?php

namespace App\Nova\Metrics;

use App\Models\User;
use Illuminate\Http\Request;
use Laravel\Nova\Metrics\Value;

class NewUsers extends Value
{
    /**
     * Calculate the value of the metric.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return mixed
     */
    public function calculate(Request $request)
    {
        return $this->count($request, User::class);
    }

    /**
     * Get the ranges available for the metric.
     *
     * @return array
     */
    public function ranges()
    {
        return [
            30 => '30 Days',
            60 => '60 Days',
            365 => '365 Days',
            'TODAY' => 'Today',
            'MTD' => 'Month To Date',
            'QTD' => 'Quarter To Date',
            'YTD' => 'Year To Date',
        ];
    }

    /**
     * Get the URI key for the metric.
     *
     * @return string
     */
    public function uriKey()
    {
        return 'new-users';
    }
}
```
When you create a value metrics this is the default class that is created.  You can add name() method if you'd like to change what is displayed as the title of the metric.  The calculate method should call one of the possible methods and return a value.  
count()
result()
average()
sum()
max()
min()
and they all act the same way you take a time range and query the DB and get a value.  I will be modifying them to query some more specific items and I'll only be using result since I'll be getting a single value with my queries / any additional math that is needed.  I won't be going over each metric that I am creating but I will go over one in depth to show you how I chose to use them.  

One metric that I was asked to keep track of is `Average Trip Cost`.  The idea is that any booking requested, whether declined / accepted, to get the average cost.  Even if that booking is cancelled as well if during that time range period, the bookings just not a draft, to add it and get the average.
Here are my ranges:
```
    public function ranges()
    {
        return [
            '7'             => '7 Days',
            '01/01/2021'    => 'January 2021',
            '02/01/2021'    => 'February 2021',
            '03/01/2021'    => 'March 2021',
            '04/01/2021'    => 'April 2021',
            '05/01/2021'    => 'May 2021',
            '06/01/2021'    => 'June 2021',
            '07/01/2021'    => 'July 2021',
            '08/01/2021'    => 'August 2021',
            '09/01/2021'    => 'September 2021',
            '10/01/2021'    => 'October 2021',
            '11/01/2021'    => 'November 2021',
            '12/01/2021'    => 'December 2021',
        ];
    }
```
```
    public function calculate(NovaRequest $request)
    {
        $range = $this->getRange($request->range);
        $bookings = Booking::where('status', '!=', 'draft')
            ->whereDate('created_at', '>=', $range['start'])
            ->whereDate('created_at', '<=', $range['end'])
            ->get();
        $totalCost = 0;
        foreach ($bookings as $booking) {
            if ($booking->cost) {
                $totalCost += $booking->cost;
            } else {
                $totalCost = $booking->trip->cost + $totalCost;
            }
        }
        if ($bookings->count() < 1) {
            $average = 0;
        } else {
            $average = round(($totalCost / $bookings->count()) / 100, 2);
        }
        return $this->result($average)->format('0,0')->prefix('$');
    }
```
So as you can see in my calculate method I have this `$this->getRange($request->range)` method.  
Well $request->range is the value of the dropdown selected (well there is timezone attached too but we do not need that for our purposes).  

So what is the getRange method?  Well in that particular method I take in a string value and return a start / end date using Carbon so I'll have a specific range to query from when I need to get values.  So here it is:
```
    public function getRange(string $range)
    {
        if ($range == '7') {
            return [
                'start' => now()->subDays(7),
                'end'   => now()
            ];
        }
        return [
            'start' => Carbon::parse($range)->startOfMonth(),
            'end'   => Carbon::parse($range)->endOfMonth()
        ];
    }
```
So if it's the past 7 day selection it returns the first part otherwise it takes the value `m/d/y` format and it creates a new carbon value!!!!!!  

Now I have a start/end for the newly created `$range` variable in the default calculate method.  From the above calculate you can see that after I get the $range I get `$bookings` that are not in draft that fall between the range start/end.  I then need to loop through and add all the totals together.  The reason I have the conditional statement is that when we first built this we made a dumb mistake and attached trip to booking (which we still do) but we calculated booking cost off of the trip, which is great until the captain goes in and raises the trip cost.  So we learned after a nice little phone call and added the trip cost to the booking model.

So now we have a total we divide it by the count of the bookings and we have an average!!!!!!  This is if we have bookings, obviously if we don't we just set the average to 0 and move on.  Heck yea folks.  We did it!!!!!  I also used a built in prefix method to add the dollar sign to make it look better.  Check out the photos below.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mrsp47bynhuisx0sg4q2.png)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/30lntpzr3eg73hhmthyn.png)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/64myhs3g77fmgx60pxey.png)

So if you don't use a custom query like I did you can actually have some pretty cool metrics as far as past comparison to last values but for my purposes management loves the single value.  Even though it doesn't exactly look pretty it is super funcational for them and I can build out basically any metric (widget) they would like as long as they have the calculation that is needed.  In the past I've personally liked the partition metrics the best since people like "visual" items and the pie chart is a nice touch.  Definitely take a look at what Nova provides if you are using Laravel and need an admin dashboard.  You hoensly can't beat it, pay the $100 bucks or whatever it is and then update free for life.