Search code examples
phplaraveltimezonephp-carbon

User timezones issue in Laravel


I currently store all of my dates in UTC. When a user logs in, it uses moment.js (the moment.tz.guess() function) to fill in a hidden timezone input with their timezone name e.g. America/Toronto, and then when they log in it updates their record in the database with said timezone.

I have an accessor trait I created in order to display all dates in the users timezone. See this code:

trait Timezone
{
    public function getCreatedAtAttribute($value)
    {
        return $this->timezone($value);
    }

    public function getUpdatedAtAttribute($value)
    {
        return $this->timezone($value);
    }

    public function getDeletedAtAttribute($value)
    {
        return $this->timezone($value);
    }

    // convert date to user timezone
    public function timezone($value)
    {
        $carbon = Carbon::parse($value);

        if (auth()->check()) {
            $carbon->tz(auth()->user()->timezone);
        }

        return $carbon->toDateTimeString();
    }
}

Now I am running into a small problem. I have a reporting feature in the app, and it has a dropdown where the user can select a date range such as This Week, Last Week, etc.

I will use UTC in order to query the results, and then convert it to the users timezone. The problem is that for some users, depending on the time of day, the week is showing as being from tuesday to monday, or sunday to saturday, instaed of monday to sunday.

Here is my code for that:

public static function dateStartEnd($date_range)
{
    if ($date_range == 'Today') {
        $start_date = Carbon::now()->startOfDay();
        $end_date = Carbon::now()->endOfDay();
    }
    else if ($date_range == 'Yesterday') {
        $start_date = Carbon::now()->subDay()->startOfDay();
        $end_date = Carbon::now()->subDay()->endOfDay();
    }
    else if ($date_range == 'This Week') {
        $start_date = Carbon::now()->startOfWeek();
        $end_date = Carbon::now()->endOfWeek();
    }
    else if ($date_range == 'Last Week') {
        $start_date = Carbon::now()->subWeek()->startOfWeek();
        $end_date = Carbon::now()->subWeek()->endOfWeek();
    }
    else if ($date_range == 'This Month') {
        $start_date = Carbon::now()->startOfMonth();
        $end_date = Carbon::now()->endOfMonth();
    }
    else if ($date_range == 'Last Month') {
        $start_date = Carbon::now()->subMonth()->startOfMonth();
        $end_date = Carbon::now()->subMonth()->endOfMonth();
    }
    else {
        // All Time
        if ($lead = Lead::orderBy('created_at', 'asc')->first()) {
            $start_date = Carbon::parse($lead->created_at);
        }
        else {
            $start_date = Carbon::now()->startOfDay();
        }

        $end_date = Carbon::now()->endOfDay();
    }

    return [
        'start_date' => $start_date,
        'end_date' => $end_date,
    ];
}

I'm wondering how I can do this properly so that it will query results using UTC but show monday to sunday in the users timezone, no matter what the user timezone is compared to UTC.


Solution

  • I ended up creating separate return properties for local and utc start and end dates.

    Heres what my code now looks like:

    public static function dateStartEnd($date_range, $date_from, $date_to)
    {
        if ($date_range == 'Today') {
            $start_local = Carbon::now(auth()->user()->timezone)->startOfDay();
            $end_local = Carbon::now(auth()->user()->timezone)->endOfDay();
        }
        else if ($date_range == 'Yesterday') {
            $start_local = Carbon::now(auth()->user()->timezone)->subDay()->startOfDay();
            $end_local = Carbon::now(auth()->user()->timezone)->subDay()->endOfDay();
        }
        else if ($date_range == 'This Week') {
            $start_local = Carbon::now(auth()->user()->timezone)->startOfWeek();
            $end_local = Carbon::now(auth()->user()->timezone)->endOfWeek();
        }
        else if ($date_range == 'Last Week') {
            $start_local = Carbon::now(auth()->user()->timezone)->subWeek()->startOfWeek();
            $end_local = Carbon::now(auth()->user()->timezone)->subWeek()->endOfWeek();
        }
        else if ($date_range == 'This Month') {
            $start_local = Carbon::now(auth()->user()->timezone)->startOfMonth();
            $end_local = Carbon::now(auth()->user()->timezone)->endOfMonth();
        }
        else if ($date_range == 'Last Month') {
            $start_local = Carbon::now(auth()->user()->timezone)->subMonth()->startOfMonth();
            $end_local = Carbon::now(auth()->user()->timezone)->subMonth()->endOfMonth();
        }
        else if ($date_range == 'Custom') {
            $start_local = Carbon::parse($date_from, auth()->user()->timezone)->startOfDay();
            $end_local = Carbon::parse($date_to, auth()->user()->timezone)->endOfDay();
        }
        else {
            // All Time
            if ($lead = Lead::orderBy('created_at', 'asc')->first()) {
                $start_local = Carbon::parse($lead->created_at, auth()->user()->timezone);
            }
            else {
                $start_local = Carbon::now(auth()->user()->timezone)->startOfDay();
            }
    
            $end_local = Carbon::now(auth()->user()->timezone)->endOfDay();
        }
    
        return [
            'start_local' => $start_local,
            'start_utc' => $start_local->copy()->timezone(config('app.timezone')),
            'end_local' => $end_local,
            'end_utc' => $end_local->copy()->timezone(config('app.timezone')),
        ];
    }