mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 12:42:15 +01:00
Fixed timezones in time entry endpoints
This commit is contained in:
@@ -13,6 +13,7 @@ use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
|
|||||||
use App\Http\Resources\V1\TimeEntry\TimeEntryResource;
|
use App\Http\Resources\V1\TimeEntry\TimeEntryResource;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\TimeEntry;
|
use App\Models\TimeEntry;
|
||||||
|
use App\Service\TimezoneService;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
@@ -56,8 +57,13 @@ class TimeEntryController extends Controller
|
|||||||
$timeEntriesQuery->whereDate('start', '>', $request->input('after'));
|
$timeEntriesQuery->whereDate('start', '>', $request->input('after'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->has('active') && (bool) $request->get('active') === true) {
|
if ($request->has('active')) {
|
||||||
$timeEntriesQuery->whereNull('end');
|
if ($request->get('active') === 'true') {
|
||||||
|
$timeEntriesQuery->whereNull('end');
|
||||||
|
}
|
||||||
|
if ($request->get('active') === 'false') {
|
||||||
|
$timeEntriesQuery->whereNotNull('end');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->has('user_id')) {
|
if ($request->has('user_id')) {
|
||||||
@@ -72,19 +78,19 @@ class TimeEntryController extends Controller
|
|||||||
$timeEntries = $timeEntriesQuery->get();
|
$timeEntries = $timeEntriesQuery->get();
|
||||||
|
|
||||||
if ($timeEntries->count() === $limit && $request->has('only_full_dates') && (bool) $request->get('only_full_dates') === true) {
|
if ($timeEntries->count() === $limit && $request->has('only_full_dates') && (bool) $request->get('only_full_dates') === true) {
|
||||||
// TODO: handle user timezone!
|
$user = Auth::user();
|
||||||
|
$timezone = app(TimezoneService::class)->getTimezoneFromUser($user);
|
||||||
$lastDate = null;
|
$lastDate = null;
|
||||||
/** @var TimeEntry $timeEntry */
|
/** @var TimeEntry $timeEntry */
|
||||||
foreach ($timeEntries as $timeEntry) {
|
foreach ($timeEntries as $timeEntry) {
|
||||||
if ($lastDate === null || abs($lastDate->diffInDays($timeEntry->start->startOfDay())) > 0) {
|
if ($lastDate === null || abs($lastDate->diffInDays($timeEntry->start->toImmutable()->timezone($timezone)->startOfDay())) > 0) {
|
||||||
$lastDate = $timeEntry->start->startOfDay();
|
$lastDate = $timeEntry->start->toImmutable()->timezone($timezone)->startOfDay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$timeEntries = $timeEntries->filter(function (TimeEntry $timeEntry) use ($lastDate): bool {
|
$timeEntries = $timeEntries->filter(function (TimeEntry $timeEntry) use ($lastDate, $timezone): bool {
|
||||||
return $timeEntry->end === null || $timeEntry->start->toDateString() !== $lastDate->toDateString();
|
return $timeEntry->start->toImmutable()->timezone($timezone)->toDateString() !== $lastDate->toDateString();
|
||||||
});
|
});
|
||||||
// TODO: fix edge case with current time entry that is more than one day running
|
|
||||||
|
|
||||||
if ($timeEntries->count() === 0) {
|
if ($timeEntries->count() === 0) {
|
||||||
Log::warning('User has has more than '.$limit.' time entries on one date', [
|
Log::warning('User has has more than '.$limit.' time entries on one date', [
|
||||||
|
|||||||
@@ -153,6 +153,29 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$response->assertJsonPath('data.0.id', $activeTimeEntry->getKey());
|
$response->assertJsonPath('data.0.id', $activeTimeEntry->getKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_returns_only_non_active_time_entries(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'time-entries:view:own',
|
||||||
|
]);
|
||||||
|
$activeTimeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->active()->createMany(3);
|
||||||
|
$nonActiveTimeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.time-entries.index', [
|
||||||
|
$data->organization->getKey(),
|
||||||
|
'active' => 'false',
|
||||||
|
'user_id' => $data->user->getKey(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(1, 'data');
|
||||||
|
$response->assertJsonPath('data.0.id', $nonActiveTimeEntries->getKey());
|
||||||
|
}
|
||||||
|
|
||||||
public function test_index_endpoint_filter_only_full_dates_returns_time_entries_for_the_whole_day_case_less_time_entries_than_limit(): void
|
public function test_index_endpoint_filter_only_full_dates_returns_time_entries_for_the_whole_day_case_less_time_entries_than_limit(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -203,6 +226,57 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$response->assertJsonCount(3, 'data');
|
$response->assertJsonCount(3, 'data');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_filter_only_full_dates_returns_time_entries_for_the_whole_day_case_more_time_entries_than_limit_with_a_timezone_edge_case(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'time-entries:view:own',
|
||||||
|
]);
|
||||||
|
$data->user->timezone = 'America/New_York';
|
||||||
|
$data->user->save();
|
||||||
|
/**
|
||||||
|
* We create in the eyes of the users timezone 2 time entries yesterday, 5 time entries two days ago, and 3 time entries three days ago
|
||||||
|
* The time entries are created in a way that they jump to the next day if the endpoint ignores the users timezone and just uses UTC
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Note: This entry is yesterday in user timezone and yesterday in UTC
|
||||||
|
$timeEntriesDay1InUserTimeZone = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->state([
|
||||||
|
'start' => Carbon::now($data->user->timezone)->subDay()->startOfDay()->utc(),
|
||||||
|
])
|
||||||
|
->createMany(2);
|
||||||
|
//dump($timeEntriesDay1InUserTimeZone->first()->refresh()->start->toImmutable()->timezone('UTC')->toDateString());
|
||||||
|
//dump($timeEntriesDay1InUserTimeZone->first()->refresh()->start->toImmutable()->timezone($data->user->timezone)->toDateString());
|
||||||
|
// Note: This entry is yesterday in UTC timezone, but two days ago in user timezone
|
||||||
|
$timeEntriesDay1InUTC = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->state([
|
||||||
|
'start' => Carbon::now('UTC')->subDay()->startOfDay()->utc(),
|
||||||
|
])
|
||||||
|
->createMany(2);
|
||||||
|
//dump($timeEntriesDay1InUTC->first()->refresh()->start->toImmutable()->timezone('UTC')->toDateString());
|
||||||
|
//dump($timeEntriesDay1InUTC->first()->refresh()->start->toImmutable()->timezone($data->user->timezone)->toDateString());
|
||||||
|
// Note: This entry is two days ago in user timezone
|
||||||
|
$timeEntriesDay2InUserTimeZone = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->state([
|
||||||
|
'start' => Carbon::now($data->user->timezone)->subDays(2)->startOfDay()->utc(),
|
||||||
|
])
|
||||||
|
->createMany(3);
|
||||||
|
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.time-entries.index', [
|
||||||
|
$data->organization->getKey(),
|
||||||
|
'only_full_dates' => 'true',
|
||||||
|
'limit' => 5,
|
||||||
|
'user_id' => $data->user->getKey(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(2, 'data');
|
||||||
|
}
|
||||||
|
|
||||||
public function test_index_endpoint_filter_only_full_dates_returns_time_entries_for_the_whole_day_case_more_time_entries_in_latest_day_than_limit(): void
|
public function test_index_endpoint_filter_only_full_dates_returns_time_entries_for_the_whole_day_case_more_time_entries_in_latest_day_than_limit(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Models\Project;
|
|||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use App\Models\TimeEntry;
|
use App\Models\TimeEntry;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
class TimeEntryModelTest extends ModelTestAbstract
|
class TimeEntryModelTest extends ModelTestAbstract
|
||||||
{
|
{
|
||||||
@@ -97,4 +98,23 @@ class TimeEntryModelTest extends ModelTestAbstract
|
|||||||
// Assert
|
// Assert
|
||||||
$this->assertNull($taskRel);
|
$this->assertNull($taskRel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_eloquent_datetime_columns_remove_timezone_information_during_save(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$timeEntry = TimeEntry::factory()->forTask(null)->create();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$timeEntry->start = Carbon::create(2021, 1, 1, 12, 0, 0, 'UTC')->timezone('+1');
|
||||||
|
$timeEntry->save();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$timeEntry->refresh();
|
||||||
|
$this->assertSame('UTC', $timeEntry->start->getTimezone()->toRegionName());
|
||||||
|
$this->assertSame('2021-01-01 13:00:00', $timeEntry->start->toDateTimeString());
|
||||||
|
$this->assertDatabaseHas(TimeEntry::class, [
|
||||||
|
'id' => $timeEntry->id,
|
||||||
|
'start' => '2021-01-01 13:00:00',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user