Skip to content

Commit 0ec3b73

Browse files
committed
allow employee manage task setting to organization
1 parent b1bb724 commit 0ec3b73

File tree

12 files changed

+509
-26
lines changed

12 files changed

+509
-26
lines changed

app/Http/Controllers/Api/V1/OrganizationController.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public function update(Organization $organization, OrganizationUpdateRequest $re
4646
if ($request->getEmployeesCanSeeBillableRates() !== null) {
4747
$organization->employees_can_see_billable_rates = $request->getEmployeesCanSeeBillableRates();
4848
}
49+
if ($request->getEmployeesCanManageTasks() !== null) {
50+
$organization->employees_can_manage_tasks = $request->getEmployeesCanManageTasks();
51+
}
4952
if ($request->getNumberFormat() !== null) {
5053
$organization->number_format = $request->getNumberFormat();
5154
}

app/Http/Controllers/Api/V1/TaskController.php

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use App\Http\Resources\V1\Task\TaskCollection;
1212
use App\Http\Resources\V1\Task\TaskResource;
1313
use App\Models\Organization;
14+
use App\Models\Project;
1415
use App\Models\Task;
1516
use Illuminate\Auth\Access\AuthorizationException;
1617
use Illuminate\Http\JsonResponse;
@@ -27,6 +28,26 @@ protected function checkPermission(Organization $organization, string $permissio
2728
}
2829
}
2930

31+
/**
32+
* Check scoped permission and verify user has access to the project
33+
*
34+
* @throws AuthorizationException
35+
*/
36+
private function checkScopedPermissionForProject(Organization $organization, Project $project, string $permission): void
37+
{
38+
$this->checkPermission($organization, $permission);
39+
40+
$user = $this->user();
41+
$hasAccess = Project::query()
42+
->where('id', $project->id)
43+
->visibleByEmployee($user)
44+
->exists();
45+
46+
if (! $hasAccess) {
47+
throw new AuthorizationException('You do not have permission to '.$permission.' in this project.');
48+
}
49+
}
50+
3051
/**
3152
* Get tasks
3253
*
@@ -75,7 +96,15 @@ public function index(Organization $organization, TaskIndexRequest $request): Ta
7596
*/
7697
public function store(Organization $organization, TaskStoreRequest $request): JsonResource
7798
{
78-
$this->checkPermission($organization, 'tasks:create');
99+
/** @var Project $project */
100+
$project = Project::query()->findOrFail($request->input('project_id'));
101+
102+
if ($this->hasPermission($organization, 'tasks:create:all')) {
103+
$this->checkPermission($organization, 'tasks:create:all');
104+
} else {
105+
$this->checkScopedPermissionForProject($organization, $project, 'tasks:create');
106+
}
107+
79108
$task = new Task;
80109
$task->name = $request->input('name');
81110
$task->project_id = $request->input('project_id');
@@ -97,7 +126,17 @@ public function store(Organization $organization, TaskStoreRequest $request): Js
97126
*/
98127
public function update(Organization $organization, Task $task, TaskUpdateRequest $request): JsonResource
99128
{
100-
$this->checkPermission($organization, 'tasks:update', $task);
129+
// Check task belongs to organization
130+
if ($task->organization_id !== $organization->id) {
131+
throw new AuthorizationException('Task does not belong to organization');
132+
}
133+
134+
if ($this->hasPermission($organization, 'tasks:update:all')) {
135+
$this->checkPermission($organization, 'tasks:update:all');
136+
} else {
137+
$this->checkScopedPermissionForProject($organization, $task->project, 'tasks:update');
138+
}
139+
101140
$task->name = $request->input('name');
102141
if ($this->canAccessPremiumFeatures($organization) && $request->has('estimated_time')) {
103142
$task->estimated_time = $request->getEstimatedTime();
@@ -119,7 +158,16 @@ public function update(Organization $organization, Task $task, TaskUpdateRequest
119158
*/
120159
public function destroy(Organization $organization, Task $task): JsonResponse
121160
{
122-
$this->checkPermission($organization, 'tasks:delete', $task);
161+
// Check task belongs to organization
162+
if ($task->organization_id !== $organization->id) {
163+
throw new AuthorizationException('Task does not belong to organization');
164+
}
165+
166+
if ($this->hasPermission($organization, 'tasks:delete:all')) {
167+
$this->checkPermission($organization, 'tasks:delete:all');
168+
} else {
169+
$this->checkScopedPermissionForProject($organization, $task->project, 'tasks:delete');
170+
}
123171

124172
if ($task->timeEntries()->exists()) {
125173
throw new EntityStillInUseApiException('task', 'time_entry');

app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public function rules(): array
3939
'employees_can_see_billable_rates' => [
4040
'boolean',
4141
],
42+
'employees_can_manage_tasks' => [
43+
'boolean',
44+
],
4245
'prevent_overlapping_time_entries' => [
4346
'boolean',
4447
],
@@ -102,6 +105,11 @@ public function getEmployeesCanSeeBillableRates(): ?bool
102105
return $this->has('employees_can_see_billable_rates') ? $this->boolean('employees_can_see_billable_rates') : null;
103106
}
104107

108+
public function getEmployeesCanManageTasks(): ?bool
109+
{
110+
return $this->has('employees_can_manage_tasks') ? $this->boolean('employees_can_manage_tasks') : null;
111+
}
112+
105113
public function getPreventOverlappingTimeEntries(): ?bool
106114
{
107115
return $this->has('prevent_overlapping_time_entries') ? $this->boolean('prevent_overlapping_time_entries') : null;

app/Http/Resources/V1/Organization/OrganizationResource.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public function toArray(Request $request): array
5353
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null,
5454
/** @var bool $employees_can_see_billable_rates Can members of the organization with role "employee" see the billable rates */
5555
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
56+
/** @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 */
57+
'employees_can_manage_tasks' => $this->resource->employees_can_manage_tasks,
5658
/** @var bool $prevent_overlapping_time_entries Prevent creating overlapping time entries (only new entries) */
5759
'prevent_overlapping_time_entries' => $this->resource->prevent_overlapping_time_entries,
5860
/** @var string $currency Currency code (ISO 4217) */

app/Models/Organization.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* @property int|null $billable_rate
3636
* @property string $user_id
3737
* @property bool $employees_can_see_billable_rates
38+
* @property bool $employees_can_manage_tasks
3839
* @property User $owner
3940
* @property Carbon|null $created_at
4041
* @property Carbon|null $updated_at
@@ -70,6 +71,7 @@ class Organization extends JetstreamTeam implements AuditableContract
7071
'personal_team' => 'boolean',
7172
'currency' => 'string',
7273
'employees_can_see_billable_rates' => 'boolean',
74+
'employees_can_manage_tasks' => 'boolean',
7375
'prevent_overlapping_time_entries' => 'boolean',
7476
'number_format' => NumberFormat::class,
7577
'currency_format' => CurrencyFormat::class,

app/Providers/JetstreamServiceProvider.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,11 @@ protected function configurePermissions(): void
9494
'tasks:view',
9595
'tasks:view:all',
9696
'tasks:create',
97+
'tasks:create:all',
9798
'tasks:update',
99+
'tasks:update:all',
98100
'tasks:delete',
101+
'tasks:delete:all',
99102
'time-entries:view:all',
100103
'time-entries:create:all',
101104
'time-entries:update:all',
@@ -158,8 +161,11 @@ protected function configurePermissions(): void
158161
'tasks:view',
159162
'tasks:view:all',
160163
'tasks:create',
164+
'tasks:create:all',
161165
'tasks:update',
166+
'tasks:update:all',
162167
'tasks:delete',
168+
'tasks:delete:all',
163169
'time-entries:view:all',
164170
'time-entries:create:all',
165171
'time-entries:update:all',
@@ -219,8 +225,11 @@ protected function configurePermissions(): void
219225
'tasks:view',
220226
'tasks:view:all',
221227
'tasks:create',
228+
'tasks:create:all',
222229
'tasks:update',
230+
'tasks:update:all',
223231
'tasks:delete',
232+
'tasks:delete:all',
224233
'time-entries:view:all',
225234
'time-entries:create:all',
226235
'time-entries:update:all',

app/Service/PermissionStore.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,19 @@ private function getPermissionsByUser(Organization $organization, User $user): a
7171
/** @var Role|null $roleObj */
7272
$roleObj = Jetstream::findRole($role);
7373

74-
return $roleObj->permissions ?? [];
74+
$permissions = $roleObj->permissions ?? [];
75+
76+
// If the organization allows employees to manage tasks and the user is an employee,
77+
// add the task management permissions for accessible projects
78+
if ($role === \App\Enums\Role::Employee->value && $organization->employees_can_manage_tasks) {
79+
$permissions = array_merge($permissions, [
80+
'tasks:create',
81+
'tasks:update',
82+
'tasks:delete',
83+
]);
84+
}
85+
86+
return $permissions;
7587
}
7688

7789
/**
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Database\Schema\Blueprint;
7+
use Illuminate\Support\Facades\Schema;
8+
9+
return new class extends Migration
10+
{
11+
/**
12+
* Run the migrations.
13+
*/
14+
public function up(): void
15+
{
16+
Schema::table('organizations', function (Blueprint $table): void {
17+
$table->boolean('employees_can_manage_tasks')->default(false)->after('employees_can_see_billable_rates');
18+
});
19+
}
20+
21+
/**
22+
* Reverse the migrations.
23+
*/
24+
public function down(): void
25+
{
26+
Schema::table('organizations', function (Blueprint $table): void {
27+
$table->dropColumn('employees_can_manage_tasks');
28+
});
29+
}
30+
};

resources/js/Pages/Teams/Partials/OrganizationTimeEntrySettings.vue

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@ const { updateOrganization } = store;
1414
const { organization } = storeToRefs(store);
1515
const queryClient = useQueryClient();
1616
17-
const form = ref<{ prevent_overlapping_time_entries: boolean }>({
17+
const form = ref<{
18+
prevent_overlapping_time_entries: boolean;
19+
employees_can_manage_tasks: boolean;
20+
}>({
1821
prevent_overlapping_time_entries: false,
22+
employees_can_manage_tasks: false,
1923
});
2024
2125
onMounted(async () => {
2226
form.value.prevent_overlapping_time_entries =
2327
organization.value?.prevent_overlapping_time_entries ?? false;
28+
form.value.employees_can_manage_tasks = organization.value?.employees_can_manage_tasks ?? false;
2429
});
2530
2631
const mutation = useMutation({
@@ -33,22 +38,22 @@ const mutation = useMutation({
3338
async function submit() {
3439
await mutation.mutateAsync({
3540
prevent_overlapping_time_entries: form.value.prevent_overlapping_time_entries,
41+
employees_can_manage_tasks: form.value.employees_can_manage_tasks,
3642
});
3743
}
3844
</script>
3945

4046
<template>
4147
<FormSection>
42-
<template #title>Time Entry Settings</template>
48+
<template #title>Organization Settings</template>
4349
<template #description>
44-
Disallow overlapping time entries for members of this organization. When enabled, users
45-
cannot create new time entries that overlap with their existing ones. This only affects
46-
newly created entries.
50+
Configure various settings for your organization, including time entry and task
51+
management permissions.
4752
</template>
4853

4954
<template #form>
5055
<div class="col-span-6">
51-
<div class="col-span-6 sm:col-span-4">
56+
<div class="col-span-6 sm:col-span-4 space-y-4">
5257
<div class="flex items-center space-x-2">
5358
<Checkbox
5459
id="preventOverlappingTimeEntries"
@@ -57,6 +62,14 @@ async function submit() {
5762
for="preventOverlappingTimeEntries"
5863
value="Prevent overlapping time entries (new entries only)" />
5964
</div>
65+
<div class="flex items-center space-x-2">
66+
<Checkbox
67+
id="employeesCanManageTasks"
68+
v-model:checked="form.employees_can_manage_tasks" />
69+
<InputLabel
70+
for="employeesCanManageTasks"
71+
value="Allow Employees to manage tasks" />
72+
</div>
6073
</div>
6174
</div>
6275
</template>

resources/js/packages/api/src/openapi.json.client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ const OrganizationResource = z
317317
is_personal: z.boolean(),
318318
billable_rate: z.union([z.number(), z.null()]),
319319
employees_can_see_billable_rates: z.boolean(),
320+
employees_can_manage_tasks: z.boolean(),
320321
prevent_overlapping_time_entries: z.boolean(),
321322
currency: z.string(),
322323
currency_symbol: z.string(),
@@ -332,6 +333,7 @@ const OrganizationUpdateRequest = z
332333
name: z.string().max(255),
333334
billable_rate: z.union([z.number(), z.null()]),
334335
employees_can_see_billable_rates: z.boolean(),
336+
employees_can_manage_tasks: z.boolean(),
335337
prevent_overlapping_time_entries: z.boolean(),
336338
number_format: NumberFormat,
337339
currency_format: CurrencyFormat,

0 commit comments

Comments
 (0)