Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/Http/Controllers/Api/V1/OrganizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public function update(Organization $organization, OrganizationUpdateRequest $re
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();
}
Expand Down
54 changes: 51 additions & 3 deletions app/Http/Controllers/Api/V1/TaskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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;
Expand All @@ -27,6 +28,26 @@ protected function checkPermission(Organization $organization, string $permissio
}
}

/**
* 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
*
Expand Down Expand Up @@ -75,7 +96,15 @@ public function index(Organization $organization, TaskIndexRequest $request): Ta
*/
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');
Expand All @@ -97,7 +126,17 @@ public function store(Organization $organization, TaskStoreRequest $request): Js
*/
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();
Expand All @@ -119,7 +158,16 @@ public function update(Organization $organization, Task $task, TaskUpdateRequest
*/
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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public function rules(): array
'employees_can_see_billable_rates' => [
'boolean',
],
'employees_can_manage_tasks' => [
'boolean',
],
'prevent_overlapping_time_entries' => [
'boolean',
],
Expand Down Expand Up @@ -102,6 +105,11 @@ public function getEmployeesCanSeeBillableRates(): ?bool
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;
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Resources/V1/Organization/OrganizationResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public function toArray(Request $request): array
'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) */
Expand Down
2 changes: 2 additions & 0 deletions app/Models/Organization.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* @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
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions app/Providers/JetstreamServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@ protected function configurePermissions(): void
'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',
Expand Down Expand Up @@ -158,8 +161,11 @@ protected function configurePermissions(): void
'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',
Expand Down Expand Up @@ -219,8 +225,11 @@ protected function configurePermissions(): void
'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',
Expand Down
14 changes: 13 additions & 1 deletion app/Service/PermissionStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,19 @@ private function getPermissionsByUser(Organization $organization, User $user): a
/** @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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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"
Expand All @@ -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>
Expand Down
2 changes: 2 additions & 0 deletions resources/js/packages/api/src/openapi.json.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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,
Expand Down
Loading
Loading