diff --git a/app/Http/Controllers/Web/DashboardController.php b/app/Http/Controllers/Web/DashboardController.php index 9ba17cf9..56897496 100644 --- a/app/Http/Controllers/Web/DashboardController.php +++ b/app/Http/Controllers/Web/DashboardController.php @@ -7,7 +7,6 @@ use App\Models\Organization; use App\Models\User; use App\Service\DashboardService; -use Illuminate\Support\Str; use Inertia\Inertia; use Inertia\Response; @@ -25,181 +24,15 @@ public function dashboard(DashboardService $dashboardService): Response $totalWeeklyBillableTime = $dashboardService->totalWeeklyBillableTime($user, $organization); $totalWeeklyBillableAmount = $dashboardService->totalWeeklyBillableAmount($user, $organization); $weeklyProjectOverview = $dashboardService->weeklyProjectOverview($user, $organization); + $latestTeamActivity = $dashboardService->latestTeamActivity($organization); + $latestTasks = $dashboardService->latestTasks($user, $organization); + $lastSevenDays = $dashboardService->lastSevenDays($user, $organization); return Inertia::render('Dashboard', [ 'weeklyProjectOverview' => $weeklyProjectOverview, - 'latestTasks' => [ - // the 4 tasks with the most recent time entries - [ - 'id' => Str::uuid(), - 'name' => 'Task 1', - 'project_name' => 'Research', - 'project_id' => Str::uuid(), - ], - [ - 'id' => Str::uuid(), - 'name' => 'Task 2', - 'project_name' => 'Research', - 'project_id' => Str::uuid(), - ], - [ - 'id' => Str::uuid(), - 'name' => 'Task 3', - 'project_name' => 'Research', - 'project_id' => Str::uuid(), - ], - [ - 'id' => Str::uuid(), - 'name' => 'Task 4', - 'project_name' => 'Research', - 'project_id' => Str::uuid(), - ], - ], - 'lastSevenDays' => [ - // the last 7 days with statistics for the time entries - [ - 'date' => '2024-02-26', - 'duration' => 3600, // in seconds - // if that is too difficult we can just skip that for now - 'history' => [ - // duration in s of the 3h windows for the day starting at 00:00 - 300, - 0, - 500, - 0, - 100, - 200, - 100, - 300, - ], - ], - [ - 'date' => '2024-02-25', - 'duration' => 7200, // in seconds - 'history' => [ - // duration in s of the 3h windows for the day starting at 00:00 - 300, - 0, - 500, - 0, - 100, - 200, - 100, - 300, - ], - ], - [ - 'date' => '2024-02-24', - 'duration' => 10800, // in seconds - 'history' => [ - // duration in s of the 3h windows for the day starting at 00:00 - 300, - 0, - 500, - 0, - 100, - 200, - 100, - 300, - ], - ], - [ - 'date' => '2024-02-23', - 'duration' => 14400, // in seconds - 'history' => [ - // duration in s of the 3h windows for the day starting at 00:00 - 300, - 0, - 500, - 0, - 100, - 200, - 100, - 300, - ], - ], - [ - 'date' => '2024-02-22', - 'duration' => 18000, // in seconds - 'history' => [ - // duration in s of the 3h windows for the day starting at 00:00 - 300, - 0, - 500, - 0, - 100, - 200, - 100, - 300, - ], - ], - [ - 'date' => '2024-02-21', - 'duration' => 21600, // in seconds - 'history' => [ - // duration in s of the 3h windows for the day starting at 00:00 - 300, - 0, - 500, - 0, - 100, - 200, - 100, - 300, - ], - ], - [ - 'date' => '2024-02-20', - 'duration' => 25200, // in seconds - 'history' => [ - // duration in s of the 3h windows for the day starting at 00:00 - 300, - 0, - 500, - 0, - 100, - 200, - 100, - 300, - ], - ], - - ], - 'latestTeamActivity' => [ - // the 4 most recently active members of your team with user_id, name, description of the latest time entry, time_entry_id, task_id and a boolean status if the team member is currently working - [ - 'user_id' => Str::uuid(), - 'name' => 'John Doe', - 'description' => 'Working on the new feature', - 'time_entry_id' => Str::uuid(), - 'task_id' => Str::uuid(), - 'status' => true, - ], - [ - 'user_id' => Str::uuid(), - 'name' => 'Jane Doe', - 'description' => 'Working on the new feature', - 'time_entry_id' => Str::uuid(), - 'task_id' => Str::uuid(), - 'status' => false, - ], - [ - 'user_id' => Str::uuid(), - 'name' => 'John Smith', - 'description' => 'Working on the new feature', - 'time_entry_id' => Str::uuid(), - 'task_id' => Str::uuid(), - 'status' => true, - ], - [ - 'user_id' => Str::uuid(), - 'name' => 'Jane Smith', - 'description' => 'Working on the new feature', - 'time_entry_id' => Str::uuid(), - 'task_id' => Str::uuid(), - 'status' => false, - ], - ], + 'latestTasks' => $latestTasks, + 'lastSevenDays' => $lastSevenDays, + 'latestTeamActivity' => $latestTeamActivity, 'dailyTrackedHours' => $dailyTrackedHours, 'totalWeeklyTime' => $totalWeeklyTime, 'totalWeeklyBillableTime' => $totalWeeklyBillableTime, diff --git a/app/Models/Task.php b/app/Models/Task.php index 6f2122df..7e331e05 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -5,10 +5,12 @@ namespace App\Models; use Database\Factories\TaskFactory; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Carbon; /** @@ -20,6 +22,7 @@ * @property Carbon|null $updated_at * @property-read Project $project * @property-read Organization $organization + * @property-read Collection $timeEntries * * @method static TaskFactory factory() */ @@ -52,4 +55,12 @@ public function organization(): BelongsTo { return $this->belongsTo(Organization::class, 'organization_id'); } + + /** + * @return HasMany + */ + public function timeEntries(): HasMany + { + return $this->hasMany(TimeEntry::class, 'task_id'); + } } diff --git a/app/Service/DashboardService.php b/app/Service/DashboardService.php index 164edbc2..f51bc2d1 100644 --- a/app/Service/DashboardService.php +++ b/app/Service/DashboardService.php @@ -6,6 +6,8 @@ use App\Enums\Weekday; use App\Models\Organization; +use App\Models\Project; +use App\Models\Task; use App\Models\TimeEntry; use App\Models\User; use Carbon\Carbon; @@ -41,11 +43,11 @@ private function lastDays(int $days, CarbonTimeZone $timeZone): Collection /** * @return Collection */ - private function daysOfThisWeek(CarbonTimeZone $timeZone, Weekday $weekday): Collection + private function daysOfThisWeek(CarbonTimeZone $timeZone, Weekday $startOfWeek): Collection { $result = new Collection(); $date = Carbon::now($timeZone); - $start = $date->startOfWeek($weekday->carbonWeekDay()); + $start = $date->startOfWeek($startOfWeek->carbonWeekDay()); for ($i = 0; $i < 7; $i++) { $result->push($start->format('Y-m-d')); $start->addDay(); @@ -80,6 +82,18 @@ private function constrainDateByPossibleDates(Builder $builder, Collection $poss ]); } + /** + * @param Builder $builder + * @return Builder + */ + private function constrainDateByCurrentWeek(Builder $builder, CarbonTimeZone $timeZone, Weekday $startOfWeek): Builder + { + return $builder->whereBetween('start', [ + Carbon::now($timeZone)->startOfWeek($startOfWeek->carbonWeekDay())->utc(), + Carbon::now($timeZone)->endOfWeek($startOfWeek->carbonWeekDay())->utc(), + ]); + } + /** * Get the daily tracked hours for the user * First value: date @@ -234,23 +248,251 @@ public function totalWeeklyBillableAmount(User $user, Organization $organization * @return array */ public function weeklyProjectOverview(User $user, Organization $organization): array + { + $timezone = $this->timezoneService->getTimezoneFromUser($user); + + $query = TimeEntry::query() + ->select(DB::raw('project_id, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) + ->where('user_id', '=', $user->getKey()) + ->where('organization_id', '=', $organization->getKey()) + ->groupBy('project_id'); + + $query = $this->constrainDateByCurrentWeek($query, $timezone, $user->week_start); + /** @var Collection $entries */ + $entries = $query->get(); + + $projectIds = $entries->pluck('project_id')->whereNotNull()->all(); + $projectsMap = Project::query() + ->select(['id', 'name', 'color']) + ->whereBelongsTo($organization, 'organization') + ->whereIn('id', $projectIds) + ->get() + ->keyBy('id'); + + $response = []; + + $aggregateOther = 0; + + foreach ($entries as $entry) { + $project = $projectsMap->get($entry->project_id); + if ($project === null) { + $aggregateOther += (int) $entry->aggregate; + + continue; + } + + $response[] = [ + 'value' => (int) $entry->aggregate, + 'id' => $entry->project_id, + 'name' => $project->name, + 'color' => $project->color, + ]; + } + + if ($aggregateOther > 0 || count($response) === 0) { + $response[] = [ + 'value' => $aggregateOther, + 'id' => null, + 'name' => 'No project', + 'color' => '#cccccc', + ]; + + } + + return $response; + } + + /** + * Rhe 4 most recently active members of your team with user_id, name, description of the latest time entry, time_entry_id, task_id and a boolean status if the team member is currently working + * + * @return array + */ + public function latestTeamActivity(Organization $organization): array + { + $timeEntries = TimeEntry::query() + ->select(DB::raw('distinct on (user_id) user_id, description, id, task_id, start, "end"')) + ->whereBelongsTo($organization, 'organization') + ->orderBy('user_id') + ->orderBy('start', 'desc') + // Note: limit here does not work because of the distinct on + ->with([ + 'user', + ]) + ->get() + ->sortByDesc('start') + ->slice(0, 4); + + $response = []; + + foreach ($timeEntries as $timeEntry) { + $response[] = [ + 'user_id' => $timeEntry->user_id, + 'name' => $timeEntry->user->name, + 'description' => $timeEntry->description, + 'time_entry_id' => $timeEntry->id, + 'task_id' => $timeEntry->task_id, + 'status' => $timeEntry->end === null, + ]; + } + + return $response; + } + + /** + * The 4 tasks with the most recent time entries + * + * @return array + */ + public function latestTasks(User $user, Organization $organization): array + { + $tasks = Task::query() + ->where('organization_id', '=', $organization->getKey()) + ->with([ + 'project', + ]) + ->whereHas('timeEntries', function (Builder $builder) use ($user, $organization): void { + /** @var Builder $builder */ + $builder->where('user_id', '=', $user->getKey()) + ->where('organization_id', '=', $organization->getKey()); + }) + ->orderByDesc( + TimeEntry::select('start') + ->whereColumn('task_id', 'tasks.id') + ->orderBy('start', 'desc') + ->limit(1) + ) + ->limit(4) + ->get(); + + $response = []; + + foreach ($tasks as $task) { + $response[] = [ + 'id' => $task->id, + 'name' => $task->name, + 'project_name' => $task->project->name, + 'project_id' => $task->project->id, + ]; + } + + return $response; + } + + /** + * The last 7 days with statistics for the time entries + * + * @return array }> + */ + public function lastSevenDays(User $user, Organization $organization): array { return [ [ - 'value' => 120, - 'name' => 'Project 11', - 'color' => '#26a69a', + 'date' => '2024-02-26', + 'duration' => 3600, // in seconds + // if that is too difficult we can just skip that for now + 'history' => [ + // duration in s of the 3h windows for the day starting at 00:00 + 300, + 0, + 500, + 0, + 100, + 200, + 100, + 300, + ], + ], + [ + 'date' => '2024-02-25', + 'duration' => 7200, // in seconds + 'history' => [ + // duration in s of the 3h windows for the day starting at 00:00 + 300, + 0, + 500, + 0, + 100, + 200, + 100, + 300, + ], ], [ - 'value' => 200, - 'name' => 'Project 2', - 'color' => '#d4e157', + 'date' => '2024-02-24', + 'duration' => 10800, // in seconds + 'history' => [ + // duration in s of the 3h windows for the day starting at 00:00 + 300, + 0, + 500, + 0, + 100, + 200, + 100, + 300, + ], ], [ - 'value' => 150, - 'name' => 'Project 3', - 'color' => '#ff7043', + 'date' => '2024-02-23', + 'duration' => 14400, // in seconds + 'history' => [ + // duration in s of the 3h windows for the day starting at 00:00 + 300, + 0, + 500, + 0, + 100, + 200, + 100, + 300, + ], ], + [ + 'date' => '2024-02-22', + 'duration' => 18000, // in seconds + 'history' => [ + // duration in s of the 3h windows for the day starting at 00:00 + 300, + 0, + 500, + 0, + 100, + 200, + 100, + 300, + ], + ], + [ + 'date' => '2024-02-21', + 'duration' => 21600, // in seconds + 'history' => [ + // duration in s of the 3h windows for the day starting at 00:00 + 300, + 0, + 500, + 0, + 100, + 200, + 100, + 300, + ], + ], + [ + 'date' => '2024-02-20', + 'duration' => 25200, // in seconds + 'history' => [ + // duration in s of the 3h windows for the day starting at 00:00 + 300, + 0, + 500, + 0, + 100, + 200, + 100, + 300, + ], + ], + ]; } } diff --git a/tests/Unit/Model/TaskModelTest.php b/tests/Unit/Model/TaskModelTest.php index 2cf0cb67..b65fd667 100644 --- a/tests/Unit/Model/TaskModelTest.php +++ b/tests/Unit/Model/TaskModelTest.php @@ -7,6 +7,7 @@ use App\Models\Organization; use App\Models\Project; use App\Models\Task; +use App\Models\TimeEntry; class TaskModelTest extends ModelTestAbstract { @@ -39,4 +40,20 @@ public function test_it_belongs_to_a_project(): void $this->assertNotNull($projectRel); $this->assertTrue($projectRel->is($project)); } + + public function test_it_has_many_time_entries(): void + { + // Arrange + $otherTask = Task::factory()->create(); + $task = Task::factory()->create(); + $timeEntries = TimeEntry::factory()->forTask($task)->count(3)->create(); + $otherTimeEntries = TimeEntry::factory()->forTask($otherTask)->count(2)->create(); + + // Act + $task->refresh(); + $timeEntries = $task->timeEntries; + + // Assert + $this->assertCount(3, $timeEntries); + } } diff --git a/tests/Unit/Service/DashboardServiceTest.php b/tests/Unit/Service/DashboardServiceTest.php index e239dd6f..f8d524b1 100644 --- a/tests/Unit/Service/DashboardServiceTest.php +++ b/tests/Unit/Service/DashboardServiceTest.php @@ -6,10 +6,13 @@ use App\Enums\Weekday; use App\Models\Organization; +use App\Models\Project; +use App\Models\Task; use App\Models\TimeEntry; use App\Models\User; use App\Service\DashboardService; use Carbon\Carbon; +use Carbon\CarbonImmutable; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -251,4 +254,282 @@ public function test_total_weekly_billable_amount_returns_correct_value(): void 'currency' => $currency, ], $result); } + + public function test_weekly_project_overview_returns_correct_value_if_time_entries_for_projects_exist_in_current_week(): void + { + // Arrange + // Note: Is a Monday + $now = CarbonImmutable::create(2024, 1, 1, 12, 0, 0, 'Europe/Vienna'); + $this->travelTo($now); + $user = User::factory()->create([ + 'timezone' => 'Europe/Vienna', + 'week_start' => Weekday::Sunday, + ]); + $organization = Organization::factory()->withOwner($user)->create(); + $organization->users()->attach($user, [ + 'role' => 'owner', + ]); + $project1 = Project::factory()->forOrganization($organization)->create(); + $project2 = Project::factory()->forOrganization($organization)->create(); + $timeEntry1Project1 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->forProject($project1)->create([ + // Note: At the start of the week + 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), + 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), + ]); + $timeEntry2Project1 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->forProject($project1)->create([ + // Note: At the end of the week + 'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), + 'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), + ]); + $timeEntry1Project2 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->forProject($project2)->create([ + // Note: At the start of the week + 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), + 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), + ]); + $timeEntry2Project2 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->forProject($project2)->create([ + // Note: At the end of the week + 'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), + 'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), + ]); + $timeEntry1WithoutProject = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ + // Note: At the start of the week + 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), + 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), + ]); + $timeEntry2WithoutProject = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ + // Note: At the end of the week + 'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), + 'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), + ]); + $timeEntry1WithoutProjectOutsideOfWeek = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ + // Note: Outside of week + 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->subSecond()->utc(), + 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(39)->utc(), + ]); + + // Act + $result = $this->dashboardService->weeklyProjectOverview($user, $organization); + + // Assert + $this->assertSame([ + [ + 'value' => 80, + 'id' => $project1->getKey(), + 'name' => $project1->name, + 'color' => $project1->color, + ], + [ + 'value' => 80, + 'id' => $project2->getKey(), + 'name' => $project2->name, + 'color' => $project2->color, + ], + [ + 'value' => 80, + 'id' => null, + 'name' => 'No project', + 'color' => '#cccccc', + ], + ], $result); + } + + public function test_weekly_project_overview_returns_correct_value_if_only_entries_without_project_exist_in_the_week(): void + { + // Arrange + // Note: Is a Monday + $now = CarbonImmutable::create(2024, 1, 1, 12, 0, 0, 'Europe/Vienna'); + $this->travelTo($now); + $organization = Organization::factory()->create(); + $user = User::factory()->create([ + 'timezone' => 'Europe/Vienna', + 'week_start' => Weekday::Sunday, + ]); + $user->organizations()->attach($organization); + $timeEntry1WithoutProject = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ + // Note: At the start of the week + 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), + 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), + ]); + $timeEntry2WithoutProject = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ + // Note: At the end of the week + 'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), + 'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), + ]); + $timeEntry1WithoutProjectOutsideOfWeek = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ + // Note: Outside of week + 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->subSecond()->utc(), + 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(39)->utc(), + ]); + + // Act + $result = $this->dashboardService->weeklyProjectOverview($user, $organization); + + // Assert + $this->assertSame([ + [ + 'value' => 80, + 'id' => null, + 'name' => 'No project', + 'color' => '#cccccc', + ], + ], $result); + } + + public function test_weekly_project_overview_returns_correct_value_if_no_entries_are_in_the_week(): void + { + // Arrange + // Note: Is a Monday + $this->travelTo(Carbon::create(2024, 1, 1, 12, 0, 0, 'Europe/Vienna')); + $organization = Organization::factory()->create(); + $user = User::factory()->create([ + 'timezone' => 'Europe/Vienna', + 'week_start' => Weekday::Sunday, + ]); + $user->organizations()->attach($organization); + + // Act + $result = $this->dashboardService->weeklyProjectOverview($user, $organization); + + // Assert + $this->assertSame([ + [ + 'value' => 0, + 'id' => null, + 'name' => 'No project', + 'color' => '#cccccc', + ], + ], $result); + } + + public function test_latest_team_activity_returns_the_most_current_working_users_and_what_they_are_working_on(): void + { + // Arrange + $organization = Organization::factory()->create(); + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + $user3 = User::factory()->create(); + $user4 = User::factory()->create(); + $user5 = User::factory()->create(); + $task1 = Task::factory()->forOrganization($organization)->create(); + $timeEntry1 = TimeEntry::factory()->forUser($user1)->forOrganization($organization)->active()->create([ + 'start' => now()->subMinutes(10), + ]); + $timeEntry2 = TimeEntry::factory()->forUser($user2)->forOrganization($organization)->create([ + 'start' => now()->subMinutes(20), + ]); + $timeEntry3 = TimeEntry::factory()->forUser($user3)->forOrganization($organization)->forTask($task1)->create([ + 'description' => '', + 'start' => now()->subMinutes(30), + ]); + $timeEntry4 = TimeEntry::factory()->forUser($user4)->forOrganization($organization)->forTask($task1)->create([ + 'description' => 'TEST 123', + 'start' => now()->subMinutes(40), + ]); + $timeEntry5 = TimeEntry::factory()->forUser($user4)->forOrganization($organization)->forTask($task1)->create([ + 'description' => 'TEST 321', + 'start' => now()->subMinutes(50), + ]); + $timeEntry6 = TimeEntry::factory()->forUser($user5)->forOrganization($organization)->forTask($task1)->create([ + 'description' => 'TEST 321', + 'start' => now()->subMinutes(60), + ]); + + // Act + $result = $this->dashboardService->latestTeamActivity($organization); + + // Assert + $this->assertSame([ + [ + 'user_id' => $user1->getKey(), + 'name' => $user1->name, + 'description' => $timeEntry1->description, + 'time_entry_id' => $timeEntry1->getKey(), + 'task_id' => null, + 'status' => true, + ], + [ + 'user_id' => $user2->getKey(), + 'name' => $user2->name, + 'description' => $timeEntry2->description, + 'time_entry_id' => $timeEntry2->getKey(), + 'task_id' => null, + 'status' => false, + ], + [ + 'user_id' => $user3->getKey(), + 'name' => $user3->name, + 'description' => $timeEntry3->description, + 'time_entry_id' => $timeEntry3->getKey(), + 'task_id' => $task1->getKey(), + 'status' => false, + ], + [ + 'user_id' => $user4->getKey(), + 'name' => $user4->name, + 'description' => $timeEntry4->description, + 'time_entry_id' => $timeEntry4->getKey(), + 'task_id' => $task1->getKey(), + 'status' => false, + ], + ], $result); + } + + public function test_latest_tasks_returns_the_4_tasks_with_the_latest_time_entries(): void + { + // Arrange + $organization = Organization::factory()->create(); + $user = User::factory()->create(); + $task1 = Task::factory()->forOrganization($organization)->create(); + $task2 = Task::factory()->forOrganization($organization)->create(); + $task3 = Task::factory()->forOrganization($organization)->create(); + $task4 = Task::factory()->forOrganization($organization)->create(); + $task5 = Task::factory()->forOrganization($organization)->create(); + + $timeEntry1Task1 = TimeEntry::factory()->forTask($task1)->forUser($user)->forOrganization($organization)->create([ + 'start' => now()->subMinutes(20), + ]); + $timeEntry1Task2 = TimeEntry::factory()->forTask($task2)->forUser($user)->forOrganization($organization)->create([ + 'start' => now()->subMinutes(30), + ]); + $timeEntry1Task3 = TimeEntry::factory()->forTask($task3)->forUser($user)->forOrganization($organization)->create([ + 'start' => now()->subMinutes(40), + ]); + $timeEntry1Task4 = TimeEntry::factory()->forTask($task4)->forUser($user)->forOrganization($organization)->create([ + 'start' => now()->subMinutes(50), + ]); + $timeEntry1Task5 = TimeEntry::factory()->forTask($task5)->forUser($user)->forOrganization($organization)->create([ + 'start' => now()->subMinutes(60), + ]); + + // Act + $result = $this->dashboardService->latestTasks($user, $organization); + + // Assert + $this->assertSame([ + [ + 'id' => $timeEntry1Task1->task->getKey(), + 'name' => $timeEntry1Task1->task->name, + 'project_name' => $timeEntry1Task1->task->project->name, + 'project_id' => $timeEntry1Task1->task->project->getKey(), + ], + [ + 'id' => $timeEntry1Task2->task->getKey(), + 'name' => $timeEntry1Task2->task->name, + 'project_name' => $timeEntry1Task2->task->project->name, + 'project_id' => $timeEntry1Task2->task->project->getKey(), + ], + [ + 'id' => $timeEntry1Task3->task->getKey(), + 'name' => $timeEntry1Task3->task->name, + 'project_name' => $timeEntry1Task3->task->project->name, + 'project_id' => $timeEntry1Task3->task->project->getKey(), + ], + [ + 'id' => $timeEntry1Task4->task->getKey(), + 'name' => $timeEntry1Task4->task->name, + 'project_name' => $timeEntry1Task4->task->project->name, + 'project_id' => $timeEntry1Task4->task->project->getKey(), + ], + ], $result); + } }