Added billable rates; Added project members; Added visibility to projects

This commit is contained in:
Constantin Graf
2024-03-28 18:50:04 +01:00
parent bb42b0940a
commit ba0212ea01
71 changed files with 3236 additions and 174 deletions

View File

@@ -5,18 +5,29 @@ declare(strict_types=1);
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);
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
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;
@@ -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);
}
/**
@@ -41,7 +41,7 @@ class MemberController extends Controller
*/
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();

View File

@@ -33,6 +33,7 @@ class OrganizationController extends Controller
$this->checkPermission($organization, 'organizations:update');
$organization->name = $request->input('name');
$organization->billable_rate = $request->input('billable_rate');
$organization->save();
return new OrganizationResource($organization);

View File

@@ -10,9 +10,12 @@ use App\Http\Resources\V1\Project\ProjectCollection;
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
{
@@ -36,9 +39,23 @@ class ProjectController extends Controller
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);
}
@@ -72,6 +89,7 @@ class ProjectController extends Controller
$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();
@@ -91,7 +109,8 @@ class ProjectController extends Controller
$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);

View File

@@ -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);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
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;
@@ -71,6 +72,7 @@ class TimeEntryController extends Controller
$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) {
@@ -125,6 +127,7 @@ class TimeEntryController extends Controller
$timeEntry->fill($request->validated());
$timeEntry->description = $request->get('description') ?? '';
$timeEntry->organization()->associate($organization);
$timeEntry->setComputedAttributeValue('billable_rate');
$timeEntry->save();
return new TimeEntryResource($timeEntry);
@@ -133,7 +136,7 @@ class TimeEntryController extends Controller
/**
* Update time entry
*
* @throws AuthorizationException
* @throws AuthorizationException|TimeEntryCanNotBeRestartedApiException
*
* @operationId updateTimeEntry
*/
@@ -145,7 +148,9 @@ class TimeEntryController extends Controller
$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) ?? '';

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Http\Controllers\Web;
use App\Models\Organization;
use App\Models\User;
use App\Service\DashboardService;
use Illuminate\Support\Str;
@@ -16,27 +17,17 @@ class DashboardController extends Controller
{
/** @var User $user */
$user = auth()->user();
$dailyTrackedHours = $dashboardService->getDailyTrackedHours($user, 60);
$weeklyHistory = $dashboardService->getWeeklyHistory($user);
/** @var Organization $organization */
$organization = $user->currentTeam;
$dailyTrackedHours = $dashboardService->getDailyTrackedHours($user, $organization, 60);
$weeklyHistory = $dashboardService->getWeeklyHistory($user, $organization);
$totalWeeklyTime = $dashboardService->totalWeeklyTime($user, $organization);
$totalWeeklyBillableTime = $dashboardService->totalWeeklyBillableTime($user, $organization);
$totalWeeklyBillableAmount = $dashboardService->totalWeeklyBillableAmount($user, $organization);
$weeklyProjectOverview = $dashboardService->weeklyProjectOverview($user, $organization);
return Inertia::render('Dashboard', [
'weeklyProjectOverview' => [
[
'value' => 120,
'name' => 'Project 11',
'color' => '#26a69a',
],
[
'value' => 200,
'name' => 'Project 2',
'color' => '#d4e157',
],
[
'value' => 150,
'name' => 'Project 3',
'color' => '#ff7043',
],
],
'weeklyProjectOverview' => $weeklyProjectOverview,
'latestTasks' => [
// the 4 tasks with the most recent time entries
[
@@ -210,12 +201,9 @@ class DashboardController extends Controller
],
],
'dailyTrackedHours' => $dailyTrackedHours,
'totalWeeklyTime' => 400,
'totalWeeklyBillableTime' => 300,
'totalWeeklyBillableAmount' => [
'value' => 300.5,
'currency' => 'USD',
],
'totalWeeklyTime' => $totalWeeklyTime,
'totalWeeklyBillableTime' => $totalWeeklyBillableTime,
'totalWeeklyBillableAmount' => $totalWeeklyBillableAmount,
'weeklyHistory' => $weeklyHistory,
]);
}