mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 13:32:43 +01:00
Added timezone for user; Moved dashboard to controller
This commit is contained in:
@@ -23,6 +23,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
|
||||
'timezone' => ['required', 'timezone:all'],
|
||||
])->validateWithBag('updateProfileInformation');
|
||||
|
||||
if (isset($input['photo'])) {
|
||||
@@ -36,6 +37,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'timezone' => $input['timezone'],
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
9
app/Http/Controllers/Web/Controller.php
Normal file
9
app/Http/Controllers/Web/Controller.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
abstract class Controller extends \App\Http\Controllers\Controller
|
||||
{
|
||||
}
|
||||
222
app/Http/Controllers/Web/DashboardController.php
Normal file
222
app/Http/Controllers/Web/DashboardController.php
Normal file
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Service\DashboardService;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function dashboard(DashboardService $dashboardService): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$dailyTrackedHours = $dashboardService->getDailyTrackedHours($user, 60);
|
||||
$weeklyHistory = $dashboardService->getWeeklyHistory($user);
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
'weeklyProjectOverview' => [
|
||||
[
|
||||
'value' => 120,
|
||||
'name' => 'Project 11',
|
||||
'color' => '#26a69a',
|
||||
],
|
||||
[
|
||||
'value' => 200,
|
||||
'name' => 'Project 2',
|
||||
'color' => '#d4e157',
|
||||
],
|
||||
[
|
||||
'value' => 150,
|
||||
'name' => 'Project 3',
|
||||
'color' => '#ff7043',
|
||||
],
|
||||
],
|
||||
'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,
|
||||
],
|
||||
],
|
||||
'dailyTrackedHours' => $dailyTrackedHours,
|
||||
'totalWeeklyTime' => 400,
|
||||
'totalWeeklyBillableTime' => 300,
|
||||
'totalWeeklyBillableAmount' => [
|
||||
'value' => 300.5,
|
||||
'currency' => 'USD',
|
||||
],
|
||||
'weeklyHistory' => $weeklyHistory,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ use Laravel\Passport\HasApiTokens;
|
||||
* @property string $email
|
||||
* @property string|null $email_verified_at
|
||||
* @property string|null $password
|
||||
* @property string $timezone
|
||||
* @property bool $is_placeholder
|
||||
* @property Collection<Organization> $organizations
|
||||
* @property Collection<TimeEntry> $timeEntries
|
||||
|
||||
@@ -13,6 +13,8 @@ use App\Actions\Jetstream\RemoveOrganizationMember;
|
||||
use App\Actions\Jetstream\UpdateOrganization;
|
||||
use App\Models\Organization;
|
||||
use App\Models\OrganizationInvitation;
|
||||
use App\Service\TimezoneService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
|
||||
@@ -112,5 +114,14 @@ class JetstreamServiceProvider extends ServiceProvider
|
||||
|
||||
Jetstream::role('placeholder', 'Placeholder', [
|
||||
])->description('Placeholders are used for importing data. They cannot log in and have no permissions.');
|
||||
|
||||
Jetstream::inertia()->whenRendering(
|
||||
'Profile/Show',
|
||||
function (Request $request, array $data) {
|
||||
return array_merge($data, [
|
||||
'timezones' => $this->app->get(TimezoneService::class)->getSelectOptions(),
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
106
app/Service/DashboardService.php
Normal file
106
app/Service/DashboardService.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonTimeZone;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DashboardService
|
||||
{
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function lastDays(int $days, CarbonTimeZone $timeZone): array
|
||||
{
|
||||
$result = [];
|
||||
$date = Carbon::now($timeZone);
|
||||
for ($i = 0; $i < $days; $i++) {
|
||||
$result[] = $date->format('Y-m-d');
|
||||
$date = $date->subDay();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the daily tracked hours for the user
|
||||
* First value: date
|
||||
* Second value: seconds
|
||||
*
|
||||
* @return array<int, array{0: string, 1: int}>
|
||||
*/
|
||||
public function getDailyTrackedHours(User $user, int $days): array
|
||||
{
|
||||
$timezone = new CarbonTimeZone($user->timezone);
|
||||
$timezoneShift = $timezone->getOffset(new \DateTime('now', new \DateTimeZone('UTC')));
|
||||
|
||||
if ($timezoneShift > 0) {
|
||||
$dateWithTimeZone = 'start + INTERVAL \''.$timezoneShift.' second\'';
|
||||
} elseif ($timezoneShift < 0) {
|
||||
$dateWithTimeZone = 'start - INTERVAL \''.abs($timezoneShift).' second\'';
|
||||
} else {
|
||||
$dateWithTimeZone = 'start';
|
||||
}
|
||||
|
||||
$resultDb = TimeEntry::query()
|
||||
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as value'))
|
||||
->where('user_id', '=', $user->id)
|
||||
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')'))
|
||||
->orderBy('date')
|
||||
->get()
|
||||
->pluck('value', 'date');
|
||||
|
||||
$result = [];
|
||||
$lastDays = $this->lastDays($days, $timezone);
|
||||
|
||||
foreach ($lastDays as $day) {
|
||||
$result[] = [$day, (int) ($resultDb->get($day) ?? 0)];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistics for the current week starting at Monday / Sunday
|
||||
*
|
||||
* @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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
35
app/Service/TimezoneService.php
Normal file
35
app/Service/TimezoneService.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Carbon\CarbonTimeZone;
|
||||
use DateTimeZone;
|
||||
|
||||
class TimezoneService
|
||||
{
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getTimezones(): array
|
||||
{
|
||||
$tzlist = CarbonTimeZone::listIdentifiers(DateTimeZone::ALL);
|
||||
|
||||
return $tzlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getSelectOptions(): array
|
||||
{
|
||||
$tzlist = $this->getTimezones();
|
||||
$options = [];
|
||||
foreach ($tzlist as $tz) {
|
||||
$options[$tz] = $tz;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Extensions\Scramble\ApiExceptionTypeToSchema;
|
||||
use App\Extensions\Scramble\PaginatedResourceCollectionTypeToSchema;
|
||||
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
|
||||
|
||||
return [
|
||||
@@ -74,5 +76,8 @@ return [
|
||||
RestrictedDocsAccess::class,
|
||||
],
|
||||
|
||||
'extensions' => [],
|
||||
'extensions' => [
|
||||
ApiExceptionTypeToSchema::class,
|
||||
PaginatedResourceCollectionTypeToSchema::class,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -32,9 +32,19 @@ class UserFactory extends Factory
|
||||
'profile_photo_path' => null,
|
||||
'current_team_id' => null,
|
||||
'is_placeholder' => false,
|
||||
'timezone' => 'Europe/Vienna',
|
||||
];
|
||||
}
|
||||
|
||||
public function randomTimeZone(): static
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
return [
|
||||
'timezone' => $this->faker->timezone(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function placeholder(bool $placeholder = true): static
|
||||
{
|
||||
return $this->state(function (array $attributes) use ($placeholder): array {
|
||||
|
||||
@@ -23,6 +23,7 @@ return new class extends Migration
|
||||
$table->boolean('is_placeholder')->default(false);
|
||||
$table->foreignUuid('current_team_id')->nullable();
|
||||
$table->string('profile_photo_path', 2048)->nullable();
|
||||
$table->string('timezone')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->uniqueIndex('email')
|
||||
|
||||
@@ -20,6 +20,7 @@ const form = useForm({
|
||||
name: props.user.name,
|
||||
email: props.user.email,
|
||||
photo: null as File | null,
|
||||
timezone: props.user.timezone,
|
||||
});
|
||||
|
||||
const verificationLinkSent = ref<boolean | null>(null);
|
||||
@@ -198,6 +199,17 @@ const page = usePage<{
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timezone -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel for="timezone" value="Timezone" />
|
||||
<select name="timezone" id="timezone" v-model="form.timezone" class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
|
||||
<option v-for="timezone in $page.props.timezones" :value="timezone">
|
||||
{{ timezone }}
|
||||
</option>
|
||||
</select>
|
||||
<InputError :message="form.errors.timezone" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
|
||||
@@ -110,6 +110,7 @@ export interface User {
|
||||
two_factor_secret?: string | null;
|
||||
two_factor_recovery_codes?: string | null;
|
||||
two_factor_confirmed_at: string | null;
|
||||
timezone: string;
|
||||
// mutators
|
||||
profile_photo_url: string;
|
||||
// relations
|
||||
|
||||
308
routes/web.php
308
routes/web.php
@@ -2,9 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Http\Controllers\Web\DashboardController;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
|
||||
/*
|
||||
@@ -32,309 +32,5 @@ Route::middleware([
|
||||
config('jetstream.auth_session'),
|
||||
'verified',
|
||||
])->group(function () {
|
||||
Route::get('/dashboard', function () {
|
||||
return Inertia::render('Dashboard', [
|
||||
'weeklyProjectOverview' => [
|
||||
[
|
||||
'value' => 120,
|
||||
'name' => 'Project 11',
|
||||
'color' => '#26a69a',
|
||||
],
|
||||
[
|
||||
'value' => 200,
|
||||
'name' => 'Project 2',
|
||||
'color' => '#d4e157',
|
||||
],
|
||||
[
|
||||
'value' => 150,
|
||||
'name' => 'Project 3',
|
||||
'color' => '#ff7043',
|
||||
],
|
||||
],
|
||||
'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,
|
||||
],
|
||||
],
|
||||
'dailyTrackedHours' => [
|
||||
// not really sure how many days we need here but probably around 60
|
||||
// the second value is the duration in seconds
|
||||
['2024-01-21', 10],
|
||||
['2024-01-22', 10],
|
||||
['2024-01-23', 20],
|
||||
['2024-01-24', 10],
|
||||
['2024-01-25', 10],
|
||||
['2024-01-26', 10],
|
||||
['2024-01-27', 20],
|
||||
['2024-01-28', 10],
|
||||
['2024-01-29', 20],
|
||||
['2024-01-30', 10],
|
||||
['2024-01-31', 10],
|
||||
['2024-02-01', 20],
|
||||
['2024-02-02', 20],
|
||||
['2024-02-03', 10],
|
||||
['2024-02-04', 30],
|
||||
['2024-02-05', 10],
|
||||
['2024-02-06', 20],
|
||||
['2024-02-07', 10],
|
||||
['2024-02-08', 30],
|
||||
['2024-02-09', 10],
|
||||
['2024-02-10', 10],
|
||||
['2024-02-11', 10],
|
||||
['2024-02-12', 10],
|
||||
['2024-02-13', 10],
|
||||
['2024-02-14', 20],
|
||||
['2024-02-15', 10],
|
||||
['2024-02-16', 10],
|
||||
['2024-02-17', 10],
|
||||
['2024-02-18', 10],
|
||||
['2024-02-19', 10],
|
||||
['2024-02-20', 30],
|
||||
['2024-02-21', 20],
|
||||
['2024-02-22', 20],
|
||||
['2024-02-23', 30],
|
||||
['2024-02-24', 20],
|
||||
['2024-02-25', 10],
|
||||
['2024-02-26', 10],
|
||||
['2024-02-27', 10],
|
||||
['2024-02-28', 20],
|
||||
['2024-02-29', 10],
|
||||
['2024-03-01', 10],
|
||||
['2024-03-02', 20],
|
||||
['2024-03-03', 10],
|
||||
['2024-03-04', 30],
|
||||
['2024-03-05', 10],
|
||||
['2024-03-06', 20],
|
||||
['2024-03-07', 30],
|
||||
['2024-03-08', 10],
|
||||
['2024-03-09', 20],
|
||||
['2024-03-10', 10],
|
||||
['2024-03-11', 10],
|
||||
['2024-03-12', 10],
|
||||
['2024-03-13', 10],
|
||||
['2024-03-14', 10],
|
||||
['2024-03-15', 10],
|
||||
['2024-03-16', 10],
|
||||
['2024-03-17', 10],
|
||||
['2024-03-18', 10],
|
||||
['2024-03-19', 10],
|
||||
['2024-03-20', 10],
|
||||
['2024-03-21', 10],
|
||||
['2024-03-22', 10],
|
||||
['2024-03-23', 10],
|
||||
['2024-03-24', 10],
|
||||
['2024-03-25', 10],
|
||||
['2024-03-26', 10],
|
||||
['2024-03-27', 10],
|
||||
['2024-03-28', 10],
|
||||
['2024-03-29', 10],
|
||||
['2024-03-30', 10],
|
||||
['2024-03-31', 10],
|
||||
],
|
||||
'totalWeeklyTime' => 400,
|
||||
'totalWeeklyBillableTime' => 300,
|
||||
'totalWeeklyBillableAmount' => [
|
||||
'value' => 300.5,
|
||||
'currency' => 'USD',
|
||||
],
|
||||
'weeklyHistory' => [
|
||||
// statistics for the current week starting at Monday / Sunday
|
||||
[
|
||||
'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,
|
||||
],
|
||||
],
|
||||
]);
|
||||
})->name('dashboard');
|
||||
Route::get('/dashboard', [DashboardController::class, 'dashboard'])->name('dashboard');
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Service\TimezoneService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -14,13 +15,18 @@ class ProfileInformationTest extends TestCase
|
||||
|
||||
public function test_profile_information_can_be_updated(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
// Arrange
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act
|
||||
$response = $this->put('/user/profile-information', [
|
||||
'name' => 'Test Name',
|
||||
'email' => 'test@example.com',
|
||||
'timezone' => app(TimezoneService::class)->getTimezones()[0],
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$this->assertEquals('Test Name', $user->fresh()->name);
|
||||
$this->assertEquals('test@example.com', $user->fresh()->email);
|
||||
}
|
||||
|
||||
43
tests/Unit/Service/DashboardServiceTest.php
Normal file
43
tests/Unit/Service/DashboardServiceTest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Service;
|
||||
|
||||
use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
use App\Service\DashboardService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DashboardServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_daily_tracked_hours_returns_correct_values(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->travelTo(Carbon::create(2024, 1, 1, 12, 0, 0, 'UTC'));
|
||||
$user = User::factory()->create([
|
||||
'timezone' => 'Europe/Vienna',
|
||||
]);
|
||||
$timeEntry = TimeEntry::factory()->forUser($user)->create([
|
||||
'start' => Carbon::create(2023, 12, 31, 0, 0, 0, 'UTC'),
|
||||
'end' => Carbon::create(2023, 12, 31, 0, 0, 40, 'UTC'),
|
||||
]);
|
||||
|
||||
// Act
|
||||
$service = new DashboardService();
|
||||
$result = $service->getDailyTrackedHours($user, 5);
|
||||
|
||||
// Assert
|
||||
$this->assertSame([
|
||||
['2024-01-01', 0],
|
||||
['2023-12-31', 40],
|
||||
['2023-12-30', 0],
|
||||
['2023-12-29', 0],
|
||||
['2023-12-28', 0],
|
||||
], $result);
|
||||
}
|
||||
}
|
||||
26
tests/Unit/Service/TimezoneServiceTest.php
Normal file
26
tests/Unit/Service/TimezoneServiceTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Service;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class TimezoneServiceTest extends TestCase
|
||||
{
|
||||
public function test_get_timezones_returns_all_available_timezones(): void
|
||||
{
|
||||
// Arrange
|
||||
$service = new \App\Service\TimezoneService();
|
||||
|
||||
// Act
|
||||
$result = $service->getTimezones();
|
||||
|
||||
// Assert
|
||||
$this->assertIsArray($result);
|
||||
$this->assertCount(419, $result);
|
||||
$this->assertContains('Europe/Vienna', $result);
|
||||
$this->assertContains('Europe/Berlin', $result);
|
||||
$this->assertContains('Europe/London', $result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user