mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-05-07 20:32:26 +00:00
allow employee manage task setting to organization
This commit is contained in:
@@ -46,6 +46,9 @@ class OrganizationController extends Controller
|
||||
if ($request->getEmployeesCanSeeBillableRates() !== null) {
|
||||
$organization->employees_can_see_billable_rates = $request->getEmployeesCanSeeBillableRates();
|
||||
}
|
||||
if ($request->getEmployeesCanManageTasks() !== null) {
|
||||
$organization->employees_can_manage_tasks = $request->getEmployeesCanManageTasks();
|
||||
}
|
||||
if ($request->getNumberFormat() !== null) {
|
||||
$organization->number_format = $request->getNumberFormat();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Http\Requests\V1\Task\TaskUpdateRequest;
|
||||
use App\Http\Resources\V1\Task\TaskCollection;
|
||||
use App\Http\Resources\V1\Task\TaskResource;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\Task;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -27,6 +28,26 @@ class TaskController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check scoped permission and verify user has access to the project
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
private function checkScopedPermissionForProject(Organization $organization, Project $project, string $permission): void
|
||||
{
|
||||
$this->checkPermission($organization, $permission);
|
||||
|
||||
$user = $this->user();
|
||||
$hasAccess = Project::query()
|
||||
->where('id', $project->id)
|
||||
->visibleByEmployee($user)
|
||||
->exists();
|
||||
|
||||
if (! $hasAccess) {
|
||||
throw new AuthorizationException('You do not have permission to '.$permission.' in this project.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks
|
||||
*
|
||||
@@ -75,7 +96,15 @@ class TaskController extends Controller
|
||||
*/
|
||||
public function store(Organization $organization, TaskStoreRequest $request): JsonResource
|
||||
{
|
||||
$this->checkPermission($organization, 'tasks:create');
|
||||
/** @var Project $project */
|
||||
$project = Project::query()->findOrFail($request->input('project_id'));
|
||||
|
||||
if ($this->hasPermission($organization, 'tasks:create:all')) {
|
||||
$this->checkPermission($organization, 'tasks:create:all');
|
||||
} else {
|
||||
$this->checkScopedPermissionForProject($organization, $project, 'tasks:create');
|
||||
}
|
||||
|
||||
$task = new Task;
|
||||
$task->name = $request->input('name');
|
||||
$task->project_id = $request->input('project_id');
|
||||
@@ -97,7 +126,17 @@ class TaskController extends Controller
|
||||
*/
|
||||
public function update(Organization $organization, Task $task, TaskUpdateRequest $request): JsonResource
|
||||
{
|
||||
$this->checkPermission($organization, 'tasks:update', $task);
|
||||
// Check task belongs to organization
|
||||
if ($task->organization_id !== $organization->id) {
|
||||
throw new AuthorizationException('Task does not belong to organization');
|
||||
}
|
||||
|
||||
if ($this->hasPermission($organization, 'tasks:update:all')) {
|
||||
$this->checkPermission($organization, 'tasks:update:all');
|
||||
} else {
|
||||
$this->checkScopedPermissionForProject($organization, $task->project, 'tasks:update');
|
||||
}
|
||||
|
||||
$task->name = $request->input('name');
|
||||
if ($this->canAccessPremiumFeatures($organization) && $request->has('estimated_time')) {
|
||||
$task->estimated_time = $request->getEstimatedTime();
|
||||
@@ -119,7 +158,16 @@ class TaskController extends Controller
|
||||
*/
|
||||
public function destroy(Organization $organization, Task $task): JsonResponse
|
||||
{
|
||||
$this->checkPermission($organization, 'tasks:delete', $task);
|
||||
// Check task belongs to organization
|
||||
if ($task->organization_id !== $organization->id) {
|
||||
throw new AuthorizationException('Task does not belong to organization');
|
||||
}
|
||||
|
||||
if ($this->hasPermission($organization, 'tasks:delete:all')) {
|
||||
$this->checkPermission($organization, 'tasks:delete:all');
|
||||
} else {
|
||||
$this->checkScopedPermissionForProject($organization, $task->project, 'tasks:delete');
|
||||
}
|
||||
|
||||
if ($task->timeEntries()->exists()) {
|
||||
throw new EntityStillInUseApiException('task', 'time_entry');
|
||||
|
||||
@@ -39,6 +39,9 @@ class OrganizationUpdateRequest extends BaseFormRequest
|
||||
'employees_can_see_billable_rates' => [
|
||||
'boolean',
|
||||
],
|
||||
'employees_can_manage_tasks' => [
|
||||
'boolean',
|
||||
],
|
||||
'prevent_overlapping_time_entries' => [
|
||||
'boolean',
|
||||
],
|
||||
@@ -102,6 +105,11 @@ class OrganizationUpdateRequest extends BaseFormRequest
|
||||
return $this->has('employees_can_see_billable_rates') ? $this->boolean('employees_can_see_billable_rates') : null;
|
||||
}
|
||||
|
||||
public function getEmployeesCanManageTasks(): ?bool
|
||||
{
|
||||
return $this->has('employees_can_manage_tasks') ? $this->boolean('employees_can_manage_tasks') : null;
|
||||
}
|
||||
|
||||
public function getPreventOverlappingTimeEntries(): ?bool
|
||||
{
|
||||
return $this->has('prevent_overlapping_time_entries') ? $this->boolean('prevent_overlapping_time_entries') : null;
|
||||
|
||||
@@ -53,6 +53,8 @@ class OrganizationResource extends BaseResource
|
||||
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null,
|
||||
/** @var bool $employees_can_see_billable_rates Can members of the organization with role "employee" see the billable rates */
|
||||
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
|
||||
/** @var bool $employees_can_manage_tasks Can members of the organization with role "employee" manage tasks in public projects and projects they are assigned to */
|
||||
'employees_can_manage_tasks' => $this->resource->employees_can_manage_tasks,
|
||||
/** @var bool $prevent_overlapping_time_entries Prevent creating overlapping time entries (only new entries) */
|
||||
'prevent_overlapping_time_entries' => $this->resource->prevent_overlapping_time_entries,
|
||||
/** @var string $currency Currency code (ISO 4217) */
|
||||
|
||||
@@ -35,6 +35,7 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
* @property int|null $billable_rate
|
||||
* @property string $user_id
|
||||
* @property bool $employees_can_see_billable_rates
|
||||
* @property bool $employees_can_manage_tasks
|
||||
* @property User $owner
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
@@ -70,6 +71,7 @@ class Organization extends JetstreamTeam implements AuditableContract
|
||||
'personal_team' => 'boolean',
|
||||
'currency' => 'string',
|
||||
'employees_can_see_billable_rates' => 'boolean',
|
||||
'employees_can_manage_tasks' => 'boolean',
|
||||
'prevent_overlapping_time_entries' => 'boolean',
|
||||
'number_format' => NumberFormat::class,
|
||||
'currency_format' => CurrencyFormat::class,
|
||||
|
||||
@@ -94,8 +94,11 @@ class JetstreamServiceProvider extends ServiceProvider
|
||||
'tasks:view',
|
||||
'tasks:view:all',
|
||||
'tasks:create',
|
||||
'tasks:create:all',
|
||||
'tasks:update',
|
||||
'tasks:update:all',
|
||||
'tasks:delete',
|
||||
'tasks:delete:all',
|
||||
'time-entries:view:all',
|
||||
'time-entries:create:all',
|
||||
'time-entries:update:all',
|
||||
@@ -158,8 +161,11 @@ class JetstreamServiceProvider extends ServiceProvider
|
||||
'tasks:view',
|
||||
'tasks:view:all',
|
||||
'tasks:create',
|
||||
'tasks:create:all',
|
||||
'tasks:update',
|
||||
'tasks:update:all',
|
||||
'tasks:delete',
|
||||
'tasks:delete:all',
|
||||
'time-entries:view:all',
|
||||
'time-entries:create:all',
|
||||
'time-entries:update:all',
|
||||
@@ -219,8 +225,11 @@ class JetstreamServiceProvider extends ServiceProvider
|
||||
'tasks:view',
|
||||
'tasks:view:all',
|
||||
'tasks:create',
|
||||
'tasks:create:all',
|
||||
'tasks:update',
|
||||
'tasks:update:all',
|
||||
'tasks:delete',
|
||||
'tasks:delete:all',
|
||||
'time-entries:view:all',
|
||||
'time-entries:create:all',
|
||||
'time-entries:update:all',
|
||||
|
||||
@@ -71,7 +71,19 @@ class PermissionStore
|
||||
/** @var Role|null $roleObj */
|
||||
$roleObj = Jetstream::findRole($role);
|
||||
|
||||
return $roleObj->permissions ?? [];
|
||||
$permissions = $roleObj->permissions ?? [];
|
||||
|
||||
// If the organization allows employees to manage tasks and the user is an employee,
|
||||
// add the task management permissions for accessible projects
|
||||
if ($role === \App\Enums\Role::Employee->value && $organization->employees_can_manage_tasks) {
|
||||
$permissions = array_merge($permissions, [
|
||||
'tasks:create',
|
||||
'tasks:update',
|
||||
'tasks:delete',
|
||||
]);
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+30
@@ -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('organizations', function (Blueprint $table): void {
|
||||
$table->boolean('employees_can_manage_tasks')->default(false)->after('employees_can_see_billable_rates');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('organizations', function (Blueprint $table): void {
|
||||
$table->dropColumn('employees_can_manage_tasks');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -14,13 +14,18 @@ const { updateOrganization } = store;
|
||||
const { organization } = storeToRefs(store);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const form = ref<{ prevent_overlapping_time_entries: boolean }>({
|
||||
const form = ref<{
|
||||
prevent_overlapping_time_entries: boolean;
|
||||
employees_can_manage_tasks: boolean;
|
||||
}>({
|
||||
prevent_overlapping_time_entries: false,
|
||||
employees_can_manage_tasks: false,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
form.value.prevent_overlapping_time_entries =
|
||||
organization.value?.prevent_overlapping_time_entries ?? false;
|
||||
form.value.employees_can_manage_tasks = organization.value?.employees_can_manage_tasks ?? false;
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
@@ -33,22 +38,22 @@ const mutation = useMutation({
|
||||
async function submit() {
|
||||
await mutation.mutateAsync({
|
||||
prevent_overlapping_time_entries: form.value.prevent_overlapping_time_entries,
|
||||
employees_can_manage_tasks: form.value.employees_can_manage_tasks,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormSection>
|
||||
<template #title>Time Entry Settings</template>
|
||||
<template #title>Organization Settings</template>
|
||||
<template #description>
|
||||
Disallow overlapping time entries for members of this organization. When enabled, users
|
||||
cannot create new time entries that overlap with their existing ones. This only affects
|
||||
newly created entries.
|
||||
Configure various settings for your organization, including time entry and task
|
||||
management permissions.
|
||||
</template>
|
||||
|
||||
<template #form>
|
||||
<div class="col-span-6">
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<div class="col-span-6 sm:col-span-4 space-y-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="preventOverlappingTimeEntries"
|
||||
@@ -57,6 +62,14 @@ async function submit() {
|
||||
for="preventOverlappingTimeEntries"
|
||||
value="Prevent overlapping time entries (new entries only)" />
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="employeesCanManageTasks"
|
||||
v-model:checked="form.employees_can_manage_tasks" />
|
||||
<InputLabel
|
||||
for="employeesCanManageTasks"
|
||||
value="Allow Employees to manage tasks" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -317,6 +317,7 @@ const OrganizationResource = z
|
||||
is_personal: z.boolean(),
|
||||
billable_rate: z.union([z.number(), z.null()]),
|
||||
employees_can_see_billable_rates: z.boolean(),
|
||||
employees_can_manage_tasks: z.boolean(),
|
||||
prevent_overlapping_time_entries: z.boolean(),
|
||||
currency: z.string(),
|
||||
currency_symbol: z.string(),
|
||||
@@ -332,6 +333,7 @@ const OrganizationUpdateRequest = z
|
||||
name: z.string().max(255),
|
||||
billable_rate: z.union([z.number(), z.null()]),
|
||||
employees_can_see_billable_rates: z.boolean(),
|
||||
employees_can_manage_tasks: z.boolean(),
|
||||
prevent_overlapping_time_entries: z.boolean(),
|
||||
number_format: NumberFormat,
|
||||
currency_format: CurrencyFormat,
|
||||
|
||||
@@ -299,7 +299,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:create',
|
||||
'tasks:create:all',
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
$task = Task::factory()->forOrganization($data->organization)->forProject($project)->create([
|
||||
@@ -324,7 +324,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:create',
|
||||
'tasks:create:all',
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
$otherProject = Project::factory()->forOrganization($data->organization)->create();
|
||||
@@ -352,7 +352,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:create',
|
||||
'tasks:create:all',
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -376,7 +376,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:create',
|
||||
'tasks:create:all',
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -408,7 +408,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:create',
|
||||
'tasks:create:all',
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -465,7 +465,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:update',
|
||||
'tasks:update:all',
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
$name = 'Task 1';
|
||||
@@ -493,7 +493,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:update',
|
||||
'tasks:update:all',
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
$otherProject = Project::factory()->forOrganization($data->organization)->create();
|
||||
@@ -523,7 +523,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:update',
|
||||
'tasks:update:all',
|
||||
]);
|
||||
$task = Task::factory()->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -547,7 +547,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
$now = Carbon::now();
|
||||
$this->travelTo($now);
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:update',
|
||||
'tasks:update:all',
|
||||
]);
|
||||
$task = Task::factory()->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -570,7 +570,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:update',
|
||||
'tasks:update:all',
|
||||
]);
|
||||
$task = Task::factory()->forOrganization($data->organization)->isDone()->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -593,7 +593,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:update',
|
||||
'tasks:update:all',
|
||||
]);
|
||||
$task = Task::factory()->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -621,7 +621,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:update',
|
||||
'tasks:update:all',
|
||||
]);
|
||||
$task = Task::factory()->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -650,7 +650,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:delete',
|
||||
'tasks:delete:all',
|
||||
]);
|
||||
$task = Task::factory()->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -669,7 +669,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:delete',
|
||||
'tasks:delete:all',
|
||||
]);
|
||||
$task = Task::factory()->forOrganization($data->organization)->create();
|
||||
TimeEntry::factory()->forMember($data->member)->forTask($task)->forOrganization($data->organization)->create();
|
||||
@@ -707,10 +707,10 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'tasks:delete',
|
||||
'tasks:delete:all',
|
||||
]);
|
||||
$otherData = $this->createUserWithPermission([
|
||||
'tasks:delete',
|
||||
'tasks:delete:all',
|
||||
]);
|
||||
$task = Task::factory()->forOrganization($otherData->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
@@ -724,4 +724,274 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
|
||||
'id' => $task->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_endpoint_allows_employee_to_create_task_in_public_project_when_employees_can_manage_tasks_is_enabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = true;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPublic()->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.tasks.store', [$data->organization->getKey()]), [
|
||||
'name' => 'Employee Task',
|
||||
'project_id' => $project->getKey(),
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(201);
|
||||
$this->assertDatabaseHas(Task::class, [
|
||||
'name' => 'Employee Task',
|
||||
'project_id' => $project->getKey(),
|
||||
'organization_id' => $data->organization->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_endpoint_allows_employee_to_create_task_in_accessible_private_project_when_employees_can_manage_tasks_is_enabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = true;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPrivate()->create();
|
||||
ProjectMember::factory()->forProject($project)->forMember($data->member)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.tasks.store', [$data->organization->getKey()]), [
|
||||
'name' => 'Employee Task',
|
||||
'project_id' => $project->getKey(),
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(201);
|
||||
$this->assertDatabaseHas(Task::class, [
|
||||
'name' => 'Employee Task',
|
||||
'project_id' => $project->getKey(),
|
||||
'organization_id' => $data->organization->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_endpoint_fails_for_employee_creating_task_in_inaccessible_private_project_when_employees_can_manage_tasks_is_enabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = true;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPrivate()->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.tasks.store', [$data->organization->getKey()]), [
|
||||
'name' => 'Employee Task',
|
||||
'project_id' => $project->getKey(),
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseMissing(Task::class, [
|
||||
'name' => 'Employee Task',
|
||||
'project_id' => $project->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_endpoint_fails_for_employee_when_employees_can_manage_tasks_is_disabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = false;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPublic()->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.tasks.store', [$data->organization->getKey()]), [
|
||||
'name' => 'Employee Task',
|
||||
'project_id' => $project->getKey(),
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseMissing(Task::class, [
|
||||
'name' => 'Employee Task',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_endpoint_allows_employee_to_update_task_in_public_project_when_employees_can_manage_tasks_is_enabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = true;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPublic()->create();
|
||||
$task = Task::factory()->forOrganization($data->organization)->forProject($project)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.tasks.update', [$data->organization->getKey(), $task->getKey()]), [
|
||||
'name' => 'Updated by Employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$this->assertDatabaseHas(Task::class, [
|
||||
'id' => $task->getKey(),
|
||||
'name' => 'Updated by Employee',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_endpoint_allows_employee_to_update_task_in_accessible_private_project_when_employees_can_manage_tasks_is_enabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = true;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPrivate()->create();
|
||||
ProjectMember::factory()->forProject($project)->forMember($data->member)->create();
|
||||
$task = Task::factory()->forOrganization($data->organization)->forProject($project)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.tasks.update', [$data->organization->getKey(), $task->getKey()]), [
|
||||
'name' => 'Updated by Employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$this->assertDatabaseHas(Task::class, [
|
||||
'id' => $task->getKey(),
|
||||
'name' => 'Updated by Employee',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_endpoint_fails_for_employee_updating_task_in_inaccessible_private_project_when_employees_can_manage_tasks_is_enabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = true;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPrivate()->create();
|
||||
$task = Task::factory()->forOrganization($data->organization)->forProject($project)->create();
|
||||
$originalName = $task->name;
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.tasks.update', [$data->organization->getKey(), $task->getKey()]), [
|
||||
'name' => 'Updated by Employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseHas(Task::class, [
|
||||
'id' => $task->getKey(),
|
||||
'name' => $originalName,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_endpoint_fails_for_employee_when_employees_can_manage_tasks_is_disabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = false;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPublic()->create();
|
||||
$task = Task::factory()->forOrganization($data->organization)->forProject($project)->create();
|
||||
$originalName = $task->name;
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.tasks.update', [$data->organization->getKey(), $task->getKey()]), [
|
||||
'name' => 'Updated by Employee',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseHas(Task::class, [
|
||||
'id' => $task->getKey(),
|
||||
'name' => $originalName,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_delete_endpoint_allows_employee_to_delete_task_in_public_project_when_employees_can_manage_tasks_is_enabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = true;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPublic()->create();
|
||||
$task = Task::factory()->forOrganization($data->organization)->forProject($project)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.tasks.destroy', [$data->organization->getKey(), $task->getKey()]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(204);
|
||||
$this->assertDatabaseMissing(Task::class, [
|
||||
'id' => $task->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_delete_endpoint_allows_employee_to_delete_task_in_accessible_private_project_when_employees_can_manage_tasks_is_enabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = true;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPrivate()->create();
|
||||
ProjectMember::factory()->forProject($project)->forMember($data->member)->create();
|
||||
$task = Task::factory()->forOrganization($data->organization)->forProject($project)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.tasks.destroy', [$data->organization->getKey(), $task->getKey()]));
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(204);
|
||||
$this->assertDatabaseMissing(Task::class, [
|
||||
'id' => $task->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_delete_endpoint_fails_for_employee_deleting_task_in_inaccessible_private_project_when_employees_can_manage_tasks_is_enabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = true;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPrivate()->create();
|
||||
$task = Task::factory()->forOrganization($data->organization)->forProject($project)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.tasks.destroy', [$data->organization->getKey(), $task->getKey()]));
|
||||
|
||||
// Assert
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseHas(Task::class, [
|
||||
'id' => $task->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_delete_endpoint_fails_for_employee_when_employees_can_manage_tasks_is_disabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithRole(\App\Enums\Role::Employee);
|
||||
$data->organization->employees_can_manage_tasks = false;
|
||||
$data->organization->save();
|
||||
$project = Project::factory()->forOrganization($data->organization)->isPublic()->create();
|
||||
$task = Task::factory()->forOrganization($data->organization)->forProject($project)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.tasks.destroy', [$data->organization->getKey(), $task->getKey()]));
|
||||
|
||||
// Assert
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseHas(Task::class, [
|
||||
'id' => $task->getKey(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,4 +124,88 @@ class PermissionStoreTest extends TestCase
|
||||
// Assert
|
||||
$this->assertSame(Jetstream::findRole(Role::Employee->value)->permissions, $result);
|
||||
}
|
||||
|
||||
public function test_employee_does_not_have_task_permissions_by_default(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create([
|
||||
'employees_can_manage_tasks' => false,
|
||||
]);
|
||||
$user = User::factory()->create();
|
||||
$organization->users()->attach($user, ['role' => Role::Employee->value]);
|
||||
$permissionStore = new PermissionStore;
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act & Assert
|
||||
$this->assertFalse($permissionStore->has($organization, 'tasks:create'));
|
||||
$this->assertFalse($permissionStore->has($organization, 'tasks:update'));
|
||||
$this->assertFalse($permissionStore->has($organization, 'tasks:delete'));
|
||||
$this->assertFalse($permissionStore->has($organization, 'tasks:create:all'));
|
||||
$this->assertFalse($permissionStore->has($organization, 'tasks:update:all'));
|
||||
$this->assertFalse($permissionStore->has($organization, 'tasks:delete:all'));
|
||||
}
|
||||
|
||||
public function test_employee_has_task_permissions_when_organization_allows_it(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create([
|
||||
'employees_can_manage_tasks' => true,
|
||||
]);
|
||||
$user = User::factory()->create();
|
||||
$organization->users()->attach($user, ['role' => Role::Employee->value]);
|
||||
$permissionStore = new PermissionStore;
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act & Assert
|
||||
$this->assertTrue($permissionStore->has($organization, 'tasks:create'));
|
||||
$this->assertTrue($permissionStore->has($organization, 'tasks:update'));
|
||||
$this->assertTrue($permissionStore->has($organization, 'tasks:delete'));
|
||||
// Should NOT have the :all permissions
|
||||
$this->assertFalse($permissionStore->has($organization, 'tasks:create:all'));
|
||||
$this->assertFalse($permissionStore->has($organization, 'tasks:update:all'));
|
||||
$this->assertFalse($permissionStore->has($organization, 'tasks:delete:all'));
|
||||
}
|
||||
|
||||
public function test_non_employee_roles_are_not_affected_by_employees_can_manage_tasks_setting(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create([
|
||||
'employees_can_manage_tasks' => false,
|
||||
]);
|
||||
$admin = User::factory()->create();
|
||||
$organization->users()->attach($admin, ['role' => Role::Admin->value]);
|
||||
$permissionStore = new PermissionStore;
|
||||
$this->actingAs($admin);
|
||||
|
||||
// Act & Assert - Admin should have task permissions regardless of the setting
|
||||
$this->assertTrue($permissionStore->has($organization, 'tasks:create'));
|
||||
$this->assertTrue($permissionStore->has($organization, 'tasks:update'));
|
||||
$this->assertTrue($permissionStore->has($organization, 'tasks:delete'));
|
||||
$this->assertTrue($permissionStore->has($organization, 'tasks:create:all'));
|
||||
$this->assertTrue($permissionStore->has($organization, 'tasks:update:all'));
|
||||
$this->assertTrue($permissionStore->has($organization, 'tasks:delete:all'));
|
||||
}
|
||||
|
||||
public function test_get_permissions_includes_task_permissions_for_employee_when_enabled(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create([
|
||||
'employees_can_manage_tasks' => true,
|
||||
]);
|
||||
$user = User::factory()->create();
|
||||
$organization->users()->attach($user, ['role' => Role::Employee->value]);
|
||||
$permissionStore = new PermissionStore;
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act
|
||||
$result = $permissionStore->getPermissions($organization);
|
||||
|
||||
// Assert
|
||||
$this->assertContains('tasks:create', $result);
|
||||
$this->assertContains('tasks:update', $result);
|
||||
$this->assertContains('tasks:delete', $result);
|
||||
$this->assertNotContains('tasks:create:all', $result);
|
||||
$this->assertNotContains('tasks:update:all', $result);
|
||||
$this->assertNotContains('tasks:delete:all', $result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user