Skip to content

Commit

Permalink
Added billable rates; Added project members; Added visibility to proj…
Browse files Browse the repository at this point in the history
…ects
  • Loading branch information
korridor committed Mar 28, 2024
1 parent b68e6df commit b4c5203
Show file tree
Hide file tree
Showing 72 changed files with 3,401 additions and 337 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ yarn-error.log
/playwright/.cache/
/coverage
/extensions/*
/modules_statuses.json
2 changes: 2 additions & 0 deletions app/Actions/Fortify/CreateNewUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Actions\Fortify;

use App\Enums\Weekday;
use App\Models\Organization;
use App\Models\User;
use App\Service\TimezoneService;
Expand Down Expand Up @@ -60,6 +61,7 @@ public function create(array $input): User
'email' => $input['email'],
'password' => Hash::make($input['password']),
'timezone' => $timezone,
'week_start' => Weekday::Monday,
]), function (User $user) {
$this->createTeam($user);
});
Expand Down
13 changes: 12 additions & 1 deletion app/Actions/Jetstream/UpdateOrganization.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Models\Organization;
use App\Models\User;
use App\Rules\CurrencyRule;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
Expand All @@ -27,11 +28,21 @@ public function update(User $user, Organization $organization, array $input): vo
Gate::forUser($user)->authorize('update', $organization);

Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'name' => [
'required',
'string',
'max:255',
],
'currency' => [
'required',
'string',
new CurrencyRule(),
],
])->validateWithBag('updateTeamName');

$organization->forceFill([
'name' => $input['name'],
'currency' => $input['currency'],
])->save();
}
}
2 changes: 0 additions & 2 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,5 @@ protected function schedule(Schedule $schedule): void
protected function commands(): void
{
$this->load(__DIR__.'/Commands');

require base_path('routes/console.php');
}
}
10 changes: 10 additions & 0 deletions app/Exceptions/Api/TimeEntryCanNotBeRestartedApiException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace App\Exceptions\Api;

class TimeEntryCanNotBeRestartedApiException extends ApiException
{
public const string KEY = 'time_entry_can_not_be_restarted';
}
13 changes: 12 additions & 1 deletion app/Http/Controllers/Api/V1/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,29 @@
namespace App\Http\Controllers\Api\V1;

use App\Models\Organization;
use App\Service\PermissionStore;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Auth;

class Controller extends \App\Http\Controllers\Controller
{
public function __construct(
protected PermissionStore $permissionStore,
) {
}

/**
* @throws AuthorizationException
*/
protected function checkPermission(Organization $organization, string $permission): void
{
if (! Auth::user()->hasTeamPermission($organization, $permission)) {
if (! $this->permissionStore->has($organization, $permission)) {
throw new AuthorizationException();
}
}

protected function hasPermission(Organization $organization, string $permission): bool
{
return $this->permissionStore->has($organization, $permission);
}
}
12 changes: 6 additions & 6 deletions app/Http/Controllers/Api/V1/MemberController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace App\Http\Controllers\Api\V1;

use App\Exceptions\Api\UserNotPlaceholderApiException;
use App\Http\Requests\V1\User\UserIndexRequest;
use App\Http\Requests\V1\Member\MemberIndexRequest;
use App\Http\Resources\V1\User\MemberCollection;
use App\Http\Resources\V1\User\MemberResource;
use App\Models\Organization;
Expand All @@ -24,14 +24,14 @@ class MemberController extends Controller
*
* @throws AuthorizationException
*/
public function index(Organization $organization, UserIndexRequest $request): MemberCollection
public function index(Organization $organization, MemberIndexRequest $request): MemberCollection
{
$this->checkPermission($organization, 'users:view');
$this->checkPermission($organization, 'members:view');

$users = $organization->users()
$members = $organization->users()
->paginate();

return MemberCollection::make($users);
return MemberCollection::make($members);
}

/**
Expand All @@ -41,7 +41,7 @@ public function index(Organization $organization, UserIndexRequest $request): Me
*/
public function invitePlaceholder(Organization $organization, User $user, Request $request): JsonResponse
{
$this->checkPermission($organization, 'users:invite-placeholder');
$this->checkPermission($organization, 'members:invite-placeholder');

if (! $user->is_placeholder) {
throw new UserNotPlaceholderApiException();
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/Api/V1/OrganizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public function update(Organization $organization, OrganizationUpdateRequest $re
$this->checkPermission($organization, 'organizations:update');

$organization->name = $request->input('name');
$organization->billable_rate = $request->input('billable_rate');
$organization->save();

return new OrganizationResource($organization);
Expand Down
27 changes: 23 additions & 4 deletions app/Http/Controllers/Api/V1/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
use App\Http\Resources\V1\Project\ProjectResource;
use App\Models\Organization;
use App\Models\Project;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;

class ProjectController extends Controller
{
Expand All @@ -36,9 +39,23 @@ protected function checkPermission(Organization $organization, string $permissio
public function index(Organization $organization): ProjectCollection
{
$this->checkPermission($organization, 'projects:view');
$projects = Project::query()
->whereBelongsTo($organization, 'organization')
->paginate();
$canViewAllProjects = $this->hasPermission($organization, 'projects:view:all');
/** @var User $user */
$user = Auth::user();

$projectsQuery = Project::query()
->whereBelongsTo($organization, 'organization');

if (! $canViewAllProjects) {
$projectsQuery->where(function (Builder $builder) use ($user): Builder {
return $builder->where('is_public', '=', true)
->orWhereHas('members', function (Builder $builder) use ($user): Builder {
return $builder->whereBelongsTo($user, 'user');
});
});
}

$projects = $projectsQuery->paginate();

return new ProjectCollection($projects);
}
Expand Down Expand Up @@ -72,6 +89,7 @@ public function store(Organization $organization, ProjectStoreRequest $request):
$project = new Project();
$project->name = $request->input('name');
$project->color = $request->input('color');
$project->billable_rate = $request->input('billable_rate');
$project->client_id = $request->input('client_id');
$project->organization()->associate($organization);
$project->save();
Expand All @@ -91,7 +109,8 @@ public function update(Organization $organization, Project $project, ProjectUpda
$this->checkPermission($organization, 'projects:update', $project);
$project->name = $request->input('name');
$project->color = $request->input('color');
$project->client_id = $request->input('project_id');
$project->billable_rate = $request->input('billable_rate');
$project->client_id = $request->input('client_id');
$project->save();

return new ProjectResource($project);
Expand Down
102 changes: 102 additions & 0 deletions app/Http/Controllers/Api/V1/ProjectMemberController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1;

use App\Http\Requests\V1\ProjectMember\ProjectMemberStoreRequest;
use App\Http\Requests\V1\ProjectMember\ProjectMemberUpdateRequest;
use App\Http\Resources\V1\ProjectMember\ProjectMemberCollection;
use App\Http\Resources\V1\ProjectMember\ProjectMemberResource;
use App\Models\Organization;
use App\Models\Project;
use App\Models\ProjectMember;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;

class ProjectMemberController extends Controller
{
protected function checkPermission(Organization $organization, string $permission, ?Project $project = null, ?ProjectMember $projectMember = null): void
{
parent::checkPermission($organization, $permission);
if ($project !== null && $project->organization_id !== $organization->id) {
throw new AuthorizationException('Project does not belong to organization');
}
if ($projectMember !== null && $projectMember->project->organization_id !== $organization->id) {
throw new AuthorizationException('Project member does not belong to organization');
}
}

/**
* Get project members for project
*
* @return ProjectMemberCollection<ProjectMemberResource>
*
* @throws AuthorizationException
*
* @operationId getProjectMembers
*/
public function index(Organization $organization, Project $project): ProjectMemberCollection
{
$this->checkPermission($organization, 'project-members:view', $project);

$projectMembers = ProjectMember::query()
->whereBelongsTo($project, 'project')
->paginate();

return new ProjectMemberCollection($projectMembers);
}

/**
* Add project member to project
*
* @throws AuthorizationException
*
* @operationId createProjectMember
*/
public function store(Organization $organization, Project $project, ProjectMemberStoreRequest $request): JsonResource
{
$this->checkPermission($organization, 'project-members:create', $project);
$projectMember = new ProjectMember();
$projectMember->user_id = $request->input('user_id');
$projectMember->billable_rate = $request->input('billable_rate');
$projectMember->project()->associate($project);
$projectMember->save();

return new ProjectMemberResource($projectMember);
}

/**
* Update project member
*
* @throws AuthorizationException
*
* @operationId updateProjectMember
*/
public function update(Organization $organization, ProjectMember $projectMember, ProjectMemberUpdateRequest $request): JsonResource
{
$this->checkPermission($organization, 'project-members:update', projectMember: $projectMember);
$projectMember->billable_rate = $request->input('billable_rate');
$projectMember->save();

return new ProjectMemberResource($projectMember);
}

/**
* Delete project member
*
* @throws AuthorizationException
*
* @operationId deleteProjectMember
*/
public function destroy(Organization $organization, ProjectMember $projectMember): JsonResponse
{
$this->checkPermission($organization, 'project-members:delete', projectMember: $projectMember);

$projectMember->delete();

return response()
->json(null, 204);
}
}
9 changes: 7 additions & 2 deletions app/Http/Controllers/Api/V1/TimeEntryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Http\Controllers\Api\V1;

use App\Exceptions\Api\TimeEntryCanNotBeRestartedApiException;
use App\Exceptions\Api\TimeEntryStillRunningApiException;
use App\Http\Requests\V1\TimeEntry\TimeEntryIndexRequest;
use App\Http\Requests\V1\TimeEntry\TimeEntryStoreRequest;
Expand Down Expand Up @@ -71,6 +72,7 @@ public function index(Organization $organization, TimeEntryIndexRequest $request
$timeEntries = $timeEntriesQuery->get();

if ($timeEntries->count() === $limit && $request->has('only_full_dates') && (bool) $request->get('only_full_dates') === true) {
// TODO: handle user timezone!
$lastDate = null;
/** @var TimeEntry $timeEntry */
foreach ($timeEntries as $timeEntry) {
Expand Down Expand Up @@ -125,6 +127,7 @@ public function store(Organization $organization, TimeEntryStoreRequest $request
$timeEntry->fill($request->validated());
$timeEntry->description = $request->get('description') ?? '';
$timeEntry->organization()->associate($organization);
$timeEntry->setComputedAttributeValue('billable_rate');
$timeEntry->save();

return new TimeEntryResource($timeEntry);
Expand All @@ -133,7 +136,7 @@ public function store(Organization $organization, TimeEntryStoreRequest $request
/**
* Update time entry
*
* @throws AuthorizationException
* @throws AuthorizationException|TimeEntryCanNotBeRestartedApiException
*
* @operationId updateTimeEntry
*/
Expand All @@ -145,7 +148,9 @@ public function update(Organization $organization, TimeEntry $timeEntry, TimeEnt
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
}

// TODO: TimeEntryStillRunningApiException
if ($timeEntry->end !== null && $request->has('end') && $request->get('end') === null) {
throw new TimeEntryCanNotBeRestartedApiException();
}

$timeEntry->fill($request->validated());
$timeEntry->description = $request->get('description', $timeEntry->description) ?? '';
Expand Down
Loading

0 comments on commit b4c5203

Please sign in to comment.