Skip to content

Commit

Permalink
Added week start
Browse files Browse the repository at this point in the history
  • Loading branch information
korridor committed Mar 21, 2024
1 parent 0f71799 commit 5cd08ef
Show file tree
Hide file tree
Showing 15 changed files with 362 additions and 65 deletions.
3 changes: 3 additions & 0 deletions app/Actions/Fortify/UpdateUserProfileInformation.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Actions\Fortify;

use App\Enums\Weekday;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
Expand All @@ -24,6 +25,7 @@ public function update(User $user, array $input): void
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
'timezone' => ['required', 'timezone:all'],
'week_start' => ['required', Rule::enum(Weekday::class)],
])->validateWithBag('updateProfileInformation');

if (isset($input['photo'])) {
Expand All @@ -38,6 +40,7 @@ public function update(User $user, array $input): void
'name' => $input['name'],
'email' => $input['email'],
'timezone' => $input['timezone'],
'week_start' => $input['week_start'],
])->save();
}
}
Expand Down
47 changes: 47 additions & 0 deletions app/Enums/Weekday.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace App\Enums;

use Illuminate\Support\Carbon;

enum Weekday: string
{
case Monday = 'monday';
case Tuesday = 'tuesday';
case Wednesday = 'wednesday';
case Thursday = 'thursday';
case Friday = 'friday';
case Saturday = 'saturday';
case Sunday = 'sunday';

public function carbonWeekDay(): int
{
return match ($this) {
Weekday::Monday => Carbon::MONDAY,
Weekday::Tuesday => Carbon::TUESDAY,
Weekday::Wednesday => Carbon::WEDNESDAY,
Weekday::Thursday => Carbon::THURSDAY,
Weekday::Friday => Carbon::FRIDAY,
Weekday::Saturday => Carbon::SATURDAY,
Weekday::Sunday => Carbon::SUNDAY,
};
}

/**
* @return array<string, string>
*/
public static function toSelectArray(): array
{
return [
Weekday::Monday->value => __('enum.weekday.'.Weekday::Monday->value),
Weekday::Tuesday->value => __('enum.weekday.'.Weekday::Tuesday->value),
Weekday::Wednesday->value => __('enum.weekday.'.Weekday::Wednesday->value),
Weekday::Thursday->value => __('enum.weekday.'.Weekday::Thursday->value),
Weekday::Friday->value => __('enum.weekday.'.Weekday::Friday->value),
Weekday::Saturday->value => __('enum.weekday.'.Weekday::Saturday->value),
Weekday::Sunday->value => __('enum.weekday.'.Weekday::Sunday->value),
];
}
}
12 changes: 12 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Models;

use App\Enums\Weekday;
use Database\Factories\UserFactory;
use Filament\Panel;
use Illuminate\Database\Eloquent\Builder;
Expand All @@ -27,6 +28,7 @@
* @property string|null $password
* @property string $timezone
* @property bool $is_placeholder
* @property Weekday $week_start
* @property Collection<Organization> $organizations
* @property Collection<TimeEntry> $timeEntries
*
Expand Down Expand Up @@ -79,6 +81,16 @@ class User extends Authenticatable
'email_verified_at' => 'datetime',
'is_admin' => 'boolean',
'is_placeholder' => 'boolean',
'week_start' => Weekday::class,
];

/**
* The model's default values for attributes.
*
* @var array<string, mixed>
*/
protected $attributes = [
'week_start' => Weekday::Monday,
];

/**
Expand Down
1 change: 1 addition & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Filament\Forms\Components\Section;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
Expand Down
2 changes: 2 additions & 0 deletions app/Providers/JetstreamServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use App\Actions\Jetstream\InviteOrganizationMember;
use App\Actions\Jetstream\RemoveOrganizationMember;
use App\Actions\Jetstream\UpdateOrganization;
use App\Enums\Weekday;
use App\Models\Organization;
use App\Models\OrganizationInvitation;
use App\Service\TimezoneService;
Expand Down Expand Up @@ -120,6 +121,7 @@ protected function configurePermissions(): void
function (Request $request, array $data) {
return array_merge($data, [
'timezones' => $this->app->get(TimezoneService::class)->getSelectOptions(),
'weekdays' => Weekday::toSelectArray(),
]);
}
);
Expand Down
155 changes: 107 additions & 48 deletions app/Service/DashboardService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,92 @@

namespace App\Service;

use App\Enums\Weekday;
use App\Models\TimeEntry;
use App\Models\User;
use Carbon\Carbon;
use Carbon\CarbonTimeZone;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class DashboardService
{
private TimezoneService $timezoneService;

public function __construct(TimezoneService $timezoneService)
{
$this->timezoneService = $timezoneService;
}

/**
* @return array<int, string>
* @return Collection<int, string>
*/
private function lastDays(int $days, CarbonTimeZone $timeZone): array
private function lastDays(int $days, CarbonTimeZone $timeZone): Collection
{
$result = [];
$date = Carbon::now($timeZone);
$result = new Collection();
$date = Carbon::now($timeZone)->subDays($days);
for ($i = 0; $i < $days; $i++) {
$result[] = $date->format('Y-m-d');
$date = $date->subDay();
$date->addDay();
$result->push($date->format('Y-m-d'));
}

return $result;
}

/**
* @return Collection<int, string>
*/
private function daysOfThisWeek(CarbonTimeZone $timeZone, Weekday $weekday): Collection
{
$result = new Collection();
$date = Carbon::now($timeZone);
$start = $date->startOfWeek($weekday->carbonWeekDay());
for ($i = 0; $i < 7; $i++) {
$result->push($start->format('Y-m-d'));
$start->addDay();
}

return $result;
}

/**
* @param Collection<int, string> $possibleDates
* @param Builder<TimeEntry> $builder
* @return Builder<TimeEntry>
*/
private function constrainDateByPossibleDates(Builder $builder, Collection $possibleDates, CarbonTimeZone $timeZone): Builder
{
$value1 = Carbon::createFromFormat('Y-m-d', $possibleDates->first(), $timeZone);
$value2 = Carbon::createFromFormat('Y-m-d', $possibleDates->last(), $timeZone);
if ($value2 === false || $value1 === false) {
throw new \RuntimeException('Provided date is not valid');
}
if ($value1->gt($value2)) {
$last = $value1;
$first = $value2;
} else {
$last = $value2;
$first = $value1;
}

return $builder->whereBetween('start', [
$first->startOfDay()->utc(),
$last->endOfDay()->utc(),
]);
}

/**
* Get the daily tracked hours for the user
* First value: date
* Second value: seconds
*
* @return array<int, array{0: string, 1: int}>
* @return array<int, array{date: string, duration: int}>
*/
public function getDailyTrackedHours(User $user, int $days): array
{
$timezone = new CarbonTimeZone($user->timezone);
$timezoneShift = $timezone->getOffset(new \DateTime('now', new \DateTimeZone('UTC')));
$timezone = $this->timezoneService->getTimezoneFromUser($user);
$timezoneShift = $this->timezoneService->getShiftFromUtc($timezone);

if ($timezoneShift > 0) {
$dateWithTimeZone = 'start + INTERVAL \''.$timezoneShift.' second\'';
Expand All @@ -47,60 +99,67 @@ public function getDailyTrackedHours(User $user, int $days): array
$dateWithTimeZone = 'start';
}

$resultDb = TimeEntry::query()
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as value'))
$possibleDays = $this->lastDays($days, $timezone);

$query = TimeEntry::query()
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate'))
->where('user_id', '=', $user->id)
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')'))
->orderBy('date')
->get()
->pluck('value', 'date');
->orderBy('date');

$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
$resultDb = $query->get()
->pluck('aggregate', 'date');

$result = [];
$lastDays = $this->lastDays($days, $timezone);

foreach ($lastDays as $day) {
$result[] = [$day, (int) ($resultDb->get($day) ?? 0)];
foreach ($possibleDays as $possibleDay) {
$result[] = [
'date' => $possibleDay,
'duration' => (int) ($resultDb->get($possibleDay) ?? 0),
];
}

return $result;
}

/**
* Statistics for the current week starting at Monday / Sunday
* Statistics for the current week starting at weekday of users preference
*
* @return array<int, array{date: string, duration: int}>
*/
public function getWeeklyHistory(User $user): array
{
return [
[
'date' => '2024-02-26',
'duration' => 3600,
],
[
'date' => '2024-02-27',
'duration' => 2000,
],
[
'date' => '2024-02-28',
'duration' => 4000,
],
[
'date' => '2024-02-29',
'duration' => 3000,
],
[
'date' => '2024-03-01',
'duration' => 5000,
],
[
'date' => '2024-03-02',
'duration' => 3000,
],
[
'date' => '2024-03-03',
'duration' => 2000,
],
];
$timezone = $this->timezoneService->getTimezoneFromUser($user);
$timezoneShift = $this->timezoneService->getShiftFromUtc($timezone);
if ($timezoneShift > 0) {
$dateWithTimeZone = 'start + INTERVAL \''.$timezoneShift.' second\'';
} elseif ($timezoneShift < 0) {
$dateWithTimeZone = 'start - INTERVAL \''.abs($timezoneShift).' second\'';
} else {
$dateWithTimeZone = 'start';
}
$possibleDays = $this->daysOfThisWeek($timezone, $user->week_start);

$query = TimeEntry::query()
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate'))
->where('user_id', '=', $user->id)
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')'))
->orderBy('date');

$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
$resultDb = $query->get()
->pluck('aggregate', 'date');

$result = [];

foreach ($possibleDays as $possibleDay) {
$result[] = [
'date' => $possibleDay,
'duration' => (int) ($resultDb->get($possibleDay) ?? 0),
];
}

return $result;
}
}
24 changes: 24 additions & 0 deletions app/Service/TimezoneService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

namespace App\Service;

use App\Models\User;
use Carbon\CarbonTimeZone;
use DateTime;
use DateTimeZone;
use Illuminate\Support\Facades\Log;

class TimezoneService
{
Expand All @@ -19,6 +22,20 @@ public function getTimezones(): array
return $tzlist;
}

public function getTimezoneFromUser(User $user): CarbonTimeZone
{
try {
return new CarbonTimeZone($user->timezone);
} catch (\Exception $e) {
Log::error('User has a invalid timezone', [
'user_id' => $user->getKey(),
'timezone' => $user->timezone,
]);

return new CarbonTimeZone('UTC');
}
}

/**
* @return array<string, string>
*/
Expand All @@ -37,4 +54,11 @@ public function isValid(string $timezone): bool
{
return in_array($timezone, $this->getTimezones(), true);
}

public function getShiftFromUtc(CarbonTimeZone $timeZone): int
{
$timezoneShift = $timeZone->getOffset(new DateTime('now', new DateTimeZone('UTC')));

return $timezoneShift;
}
}
2 changes: 2 additions & 0 deletions database/factories/UserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Database\Factories;

use App\Enums\Weekday;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
Expand Down Expand Up @@ -33,6 +34,7 @@ public function definition(): array
'current_team_id' => null,
'is_placeholder' => false,
'timezone' => 'Europe/Vienna',
'week_start' => Weekday::Monday,
];
}

Expand Down
Loading

0 comments on commit 5cd08ef

Please sign in to comment.