mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 12:42:15 +01:00
Added missing chart data to dashboard endpoint
This commit is contained in:
@@ -7,7 +7,6 @@ namespace App\Http\Controllers\Web;
|
|||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\DashboardService;
|
use App\Service\DashboardService;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
|
|
||||||
@@ -25,181 +24,15 @@ class DashboardController extends Controller
|
|||||||
$totalWeeklyBillableTime = $dashboardService->totalWeeklyBillableTime($user, $organization);
|
$totalWeeklyBillableTime = $dashboardService->totalWeeklyBillableTime($user, $organization);
|
||||||
$totalWeeklyBillableAmount = $dashboardService->totalWeeklyBillableAmount($user, $organization);
|
$totalWeeklyBillableAmount = $dashboardService->totalWeeklyBillableAmount($user, $organization);
|
||||||
$weeklyProjectOverview = $dashboardService->weeklyProjectOverview($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', [
|
return Inertia::render('Dashboard', [
|
||||||
'weeklyProjectOverview' => $weeklyProjectOverview,
|
'weeklyProjectOverview' => $weeklyProjectOverview,
|
||||||
'latestTasks' => [
|
'latestTasks' => $latestTasks,
|
||||||
// the 4 tasks with the most recent time entries
|
'lastSevenDays' => $lastSevenDays,
|
||||||
[
|
'latestTeamActivity' => $latestTeamActivity,
|
||||||
'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,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'dailyTrackedHours' => $dailyTrackedHours,
|
'dailyTrackedHours' => $dailyTrackedHours,
|
||||||
'totalWeeklyTime' => $totalWeeklyTime,
|
'totalWeeklyTime' => $totalWeeklyTime,
|
||||||
'totalWeeklyBillableTime' => $totalWeeklyBillableTime,
|
'totalWeeklyBillableTime' => $totalWeeklyBillableTime,
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ declare(strict_types=1);
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Database\Factories\TaskFactory;
|
use Database\Factories\TaskFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,6 +22,7 @@ use Illuminate\Support\Carbon;
|
|||||||
* @property Carbon|null $updated_at
|
* @property Carbon|null $updated_at
|
||||||
* @property-read Project $project
|
* @property-read Project $project
|
||||||
* @property-read Organization $organization
|
* @property-read Organization $organization
|
||||||
|
* @property-read Collection<TimeEntry> $timeEntries
|
||||||
*
|
*
|
||||||
* @method static TaskFactory factory()
|
* @method static TaskFactory factory()
|
||||||
*/
|
*/
|
||||||
@@ -52,4 +55,12 @@ class Task extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Organization::class, 'organization_id');
|
return $this->belongsTo(Organization::class, 'organization_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HasMany<TimeEntry>
|
||||||
|
*/
|
||||||
|
public function timeEntries(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(TimeEntry::class, 'task_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ namespace App\Service;
|
|||||||
|
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Task;
|
||||||
use App\Models\TimeEntry;
|
use App\Models\TimeEntry;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
@@ -41,11 +43,11 @@ class DashboardService
|
|||||||
/**
|
/**
|
||||||
* @return Collection<int, string>
|
* @return Collection<int, string>
|
||||||
*/
|
*/
|
||||||
private function daysOfThisWeek(CarbonTimeZone $timeZone, Weekday $weekday): Collection
|
private function daysOfThisWeek(CarbonTimeZone $timeZone, Weekday $startOfWeek): Collection
|
||||||
{
|
{
|
||||||
$result = new Collection();
|
$result = new Collection();
|
||||||
$date = Carbon::now($timeZone);
|
$date = Carbon::now($timeZone);
|
||||||
$start = $date->startOfWeek($weekday->carbonWeekDay());
|
$start = $date->startOfWeek($startOfWeek->carbonWeekDay());
|
||||||
for ($i = 0; $i < 7; $i++) {
|
for ($i = 0; $i < 7; $i++) {
|
||||||
$result->push($start->format('Y-m-d'));
|
$result->push($start->format('Y-m-d'));
|
||||||
$start->addDay();
|
$start->addDay();
|
||||||
@@ -80,6 +82,18 @@ class DashboardService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Builder<TimeEntry> $builder
|
||||||
|
* @return Builder<TimeEntry>
|
||||||
|
*/
|
||||||
|
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
|
* Get the daily tracked hours for the user
|
||||||
* First value: date
|
* First value: date
|
||||||
@@ -234,23 +248,251 @@ class DashboardService
|
|||||||
* @return array<int, array{value: int, name: string, color: string}>
|
* @return array<int, array{value: int, name: string, color: string}>
|
||||||
*/
|
*/
|
||||||
public function weeklyProjectOverview(User $user, Organization $organization): 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<int, object{project_id: string, aggregate: int}> $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<int, array{user_id: string, name: string, description: string|null, time_entry_id: string, task_id: string|null, status: bool }>
|
||||||
|
*/
|
||||||
|
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<int, array{id: string, name: string, project_name: string|null, project_id: string }>
|
||||||
|
*/
|
||||||
|
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<TimeEntry> $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<int, array{ date: string, duration: int, history: array<int> }>
|
||||||
|
*/
|
||||||
|
public function lastSevenDays(User $user, Organization $organization): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
'value' => 120,
|
'date' => '2024-02-26',
|
||||||
'name' => 'Project 11',
|
'duration' => 3600, // in seconds
|
||||||
'color' => '#26a69a',
|
// 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,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'value' => 200,
|
'date' => '2024-02-25',
|
||||||
'name' => 'Project 2',
|
'duration' => 7200, // in seconds
|
||||||
'color' => '#d4e157',
|
'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,
|
'date' => '2024-02-24',
|
||||||
'name' => 'Project 3',
|
'duration' => 10800, // in seconds
|
||||||
'color' => '#ff7043',
|
'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,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Tests\Unit\Model;
|
|||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
|
use App\Models\TimeEntry;
|
||||||
|
|
||||||
class TaskModelTest extends ModelTestAbstract
|
class TaskModelTest extends ModelTestAbstract
|
||||||
{
|
{
|
||||||
@@ -39,4 +40,20 @@ class TaskModelTest extends ModelTestAbstract
|
|||||||
$this->assertNotNull($projectRel);
|
$this->assertNotNull($projectRel);
|
||||||
$this->assertTrue($projectRel->is($project));
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ namespace Tests\Unit\Service;
|
|||||||
|
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Task;
|
||||||
use App\Models\TimeEntry;
|
use App\Models\TimeEntry;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\DashboardService;
|
use App\Service\DashboardService;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -251,4 +254,282 @@ class DashboardServiceTest extends TestCase
|
|||||||
'currency' => $currency,
|
'currency' => $currency,
|
||||||
], $result);
|
], $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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user