mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 20:22:15 +01:00
Add ability to set task to done, fixes ST-244
This commit is contained in:
committed by
Gregor Vostrak
parent
75e739f6fb
commit
364168debd
@@ -15,6 +15,7 @@ use App\Models\Task;
|
|||||||
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;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class TaskController extends Controller
|
class TaskController extends Controller
|
||||||
{
|
{
|
||||||
@@ -53,6 +54,12 @@ class TaskController extends Controller
|
|||||||
if (! $canViewAllTasks) {
|
if (! $canViewAllTasks) {
|
||||||
$query->visibleByEmployee($user);
|
$query->visibleByEmployee($user);
|
||||||
}
|
}
|
||||||
|
$doneFilter = $request->getFilterDone();
|
||||||
|
if ($doneFilter === 'true') {
|
||||||
|
$query->whereNotNull('done_at');
|
||||||
|
} elseif ($doneFilter === 'false') {
|
||||||
|
$query->whereNull('done_at');
|
||||||
|
}
|
||||||
|
|
||||||
$tasks = $query->paginate(config('app.pagination_per_page_default'));
|
$tasks = $query->paginate(config('app.pagination_per_page_default'));
|
||||||
|
|
||||||
@@ -89,6 +96,9 @@ class TaskController extends Controller
|
|||||||
{
|
{
|
||||||
$this->checkPermission($organization, 'tasks:update', $task);
|
$this->checkPermission($organization, 'tasks:update', $task);
|
||||||
$task->name = $request->input('name');
|
$task->name = $request->input('name');
|
||||||
|
if ($request->has('is_done')) {
|
||||||
|
$task->done_at = $request->getIsDone() ? Carbon::now() : null;
|
||||||
|
}
|
||||||
$task->save();
|
$task->save();
|
||||||
|
|
||||||
return new TaskResource($task);
|
return new TaskResource($task);
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ class TaskIndexRequest extends FormRequest
|
|||||||
return $builder;
|
return $builder;
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
'done' => [
|
||||||
|
'string',
|
||||||
|
'in:true,false,all',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFilterDone(): string
|
||||||
|
{
|
||||||
|
return $this->input('done', 'false');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,16 @@ class TaskUpdateRequest extends FormRequest
|
|||||||
return $builder->where('project_id', '=', $this->task->project_id);
|
return $builder->where('project_id', '=', $this->task->project_id);
|
||||||
}))->ignore($this->task->getKey())->withCustomTranslation('validation.task_name_already_exists'),
|
}))->ignore($this->task->getKey())->withCustomTranslation('validation.task_name_already_exists'),
|
||||||
],
|
],
|
||||||
|
'is_done' => [
|
||||||
|
'boolean',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getIsDone(): bool
|
||||||
|
{
|
||||||
|
assert($this->has('is_done'));
|
||||||
|
|
||||||
|
return $this->boolean('is_done');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class TaskResource extends BaseResource
|
|||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** @var string $name Name */
|
/** @var string $name Name */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
|
/** @var bool $is_done Whether the task is done */
|
||||||
|
'is_done' => $this->resource->is_done,
|
||||||
/** @var string $project_id ID of the project */
|
/** @var string $project_id ID of the project */
|
||||||
'project_id' => $this->resource->project_id,
|
'project_id' => $this->resource->project_id,
|
||||||
/** @var string $created_at When the tag was created */
|
/** @var string $created_at When the tag was created */
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace App\Models;
|
|||||||
use App\Models\Concerns\HasUuids;
|
use App\Models\Concerns\HasUuids;
|
||||||
use Database\Factories\TaskFactory;
|
use Database\Factories\TaskFactory;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -19,11 +20,13 @@ use Illuminate\Support\Carbon;
|
|||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $project_id
|
* @property string $project_id
|
||||||
* @property string $organization_id
|
* @property string $organization_id
|
||||||
|
* @property Carbon|null $done_at
|
||||||
* @property Carbon|null $created_at
|
* @property Carbon|null $created_at
|
||||||
* @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<int, TimeEntry> $timeEntries
|
* @property-read Collection<int, TimeEntry> $timeEntries
|
||||||
|
* @property-read bool $is_done
|
||||||
*
|
*
|
||||||
* @method static TaskFactory factory()
|
* @method static TaskFactory factory()
|
||||||
*/
|
*/
|
||||||
@@ -76,4 +79,14 @@ class Task extends Model
|
|||||||
return $builder->visibleByEmployee($user);
|
return $builder->visibleByEmployee($user);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Attribute<bool, never>
|
||||||
|
*/
|
||||||
|
public function isDone(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn (mixed $value, array $attributes) => isset($attributes['done_at']),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class TaskFactory extends Factory
|
|||||||
'name' => $this->faker->word(),
|
'name' => $this->faker->word(),
|
||||||
'project_id' => Project::factory(),
|
'project_id' => Project::factory(),
|
||||||
'organization_id' => Organization::factory(),
|
'organization_id' => Organization::factory(),
|
||||||
|
'done_at' => null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +38,15 @@ class TaskFactory extends Factory
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isDone(): self
|
||||||
|
{
|
||||||
|
return $this->state(function (array $attributes) {
|
||||||
|
return [
|
||||||
|
'done_at' => $this->faker->dateTime('now', 'UTC'),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function forOrganization(Organization $organization): self
|
public function forOrganization(Organization $organization): self
|
||||||
{
|
{
|
||||||
return $this->state(function (array $attributes) use ($organization) {
|
return $this->state(function (array $attributes) use ($organization) {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('tasks', function (Blueprint $table): void {
|
||||||
|
$table->dateTime('done_at')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('tasks', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('done_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ use App\Models\Project;
|
|||||||
use App\Models\ProjectMember;
|
use App\Models\ProjectMember;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use App\Models\TimeEntry;
|
use App\Models\TimeEntry;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Laravel\Passport\Passport;
|
use Laravel\Passport\Passport;
|
||||||
use PHPUnit\Framework\Attributes\UsesClass;
|
use PHPUnit\Framework\Attributes\UsesClass;
|
||||||
|
|
||||||
@@ -76,6 +77,85 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$response->assertJsonCount(4, 'data');
|
$response->assertJsonCount(4, 'data');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_without_filter_done_returns_list_of_all_tasks_of_organization(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tasks:view',
|
||||||
|
'tasks:view:all',
|
||||||
|
]);
|
||||||
|
$notDoneTasks = Task::factory()->forOrganization($data->organization)->createMany(2);
|
||||||
|
$doneTasks = Task::factory()->forOrganization($data->organization)->isDone()->createMany(2);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.tasks.index', [$data->organization->getKey()]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(2, 'data');
|
||||||
|
$this->assertEqualsCanonicalizing($notDoneTasks->pluck('id')->toArray(), $response->json('data.*.id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_with_filter_done_true_returns_list_of_all_done_tasks_of_organization(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tasks:view',
|
||||||
|
'tasks:view:all',
|
||||||
|
]);
|
||||||
|
$notDoneTasks = Task::factory()->forOrganization($data->organization)->createMany(2);
|
||||||
|
$doneTasks = Task::factory()->forOrganization($data->organization)->isDone()->createMany(2);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.tasks.index', [$data->organization->getKey(), 'done' => 'true']));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(2, 'data');
|
||||||
|
$this->assertEqualsCanonicalizing($doneTasks->pluck('id')->toArray(), $response->json('data.*.id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_with_filter_done_false_returns_list_of_all_not_done_tasks_of_organization(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tasks:view',
|
||||||
|
'tasks:view:all',
|
||||||
|
]);
|
||||||
|
$notDoneTasks = Task::factory()->forOrganization($data->organization)->createMany(2);
|
||||||
|
$doneTasks = Task::factory()->forOrganization($data->organization)->isDone()->createMany(2);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.tasks.index', [$data->organization->getKey(), 'done' => 'false']));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(2, 'data');
|
||||||
|
$this->assertEqualsCanonicalizing($notDoneTasks->pluck('id')->toArray(), $response->json('data.*.id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_with_filter_done_all_returns_list_of_all_tasks_of_organization(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tasks:view',
|
||||||
|
'tasks:view:all',
|
||||||
|
]);
|
||||||
|
$notDoneTasks = Task::factory()->forOrganization($data->organization)->createMany(2);
|
||||||
|
$doneTasks = Task::factory()->forOrganization($data->organization)->isDone()->createMany(2);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.tasks.index', [$data->organization->getKey(), 'done' => 'all']));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(4, 'data');
|
||||||
|
}
|
||||||
|
|
||||||
public function test_index_endpoint_returns_list_of_all_tasks_with_access_of_organization_if_user_has_no_all_permission(): void
|
public function test_index_endpoint_returns_list_of_all_tasks_with_access_of_organization_if_user_has_no_all_permission(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -395,6 +475,54 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_update_endpoint_can_set_task_to_done(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$now = Carbon::now();
|
||||||
|
$this->travelTo($now);
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tasks:update',
|
||||||
|
]);
|
||||||
|
$task = Task::factory()->forOrganization($data->organization)->create();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->putJson(route('api.v1.tasks.update', [$data->organization->getKey(), $task->getKey()]), [
|
||||||
|
'name' => $task->name,
|
||||||
|
'is_done' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$this->assertDatabaseHas(Task::class, [
|
||||||
|
'id' => $task->getKey(),
|
||||||
|
'done_at' => $now->toDateTimeString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_endpoint_can_set_task_to_not_done(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tasks:update',
|
||||||
|
]);
|
||||||
|
$task = Task::factory()->forOrganization($data->organization)->isDone()->create();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->putJson(route('api.v1.tasks.update', [$data->organization->getKey(), $task->getKey()]), [
|
||||||
|
'name' => $task->name,
|
||||||
|
'is_done' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$this->assertDatabaseHas(Task::class, [
|
||||||
|
'id' => $task->getKey(),
|
||||||
|
'done_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_delete_endpoint_deletes_tasks_if_user_has_permission(): void
|
public function test_delete_endpoint_deletes_tasks_if_user_has_permission(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
@@ -90,4 +90,28 @@ class TaskModelTest extends ModelTestAbstract
|
|||||||
$taskPrivateButMember->getKey(),
|
$taskPrivateButMember->getKey(),
|
||||||
], $allTasks);
|
], $allTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_accessor_is_done_is_true_if_done_at_is_not_null(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$task = Task::factory()->isDone()->create();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$task->refresh();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertTrue($task->is_done);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_accessor_is_done_is_false_if_done_at_is_null(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$task = Task::factory()->create();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$task->refresh();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertFalse($task->is_done);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user