Skip to content

Commit

Permalink
Merge pull request #73 from solidtime-io/feature/reporting
Browse files Browse the repository at this point in the history
Reporting
  • Loading branch information
korridor authored May 21, 2024
2 parents 0fde7e5 + 8125e1a commit 98514e4
Show file tree
Hide file tree
Showing 164 changed files with 5,492 additions and 965 deletions.
4 changes: 2 additions & 2 deletions app/Actions/Jetstream/UpdateMemberRole.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace App\Actions\Jetstream;

use App\Enums\Role;
use App\Models\Membership;
use App\Models\Member;
use App\Models\Organization;
use App\Models\User;
use App\Service\PermissionStore;
Expand All @@ -32,7 +32,7 @@ public function update(User $actingUser, Organization $organization, string $use
}

$user = User::where('id', '=', $userId)->firstOrFail();
$member = Membership::whereBelongsTo($user)->whereBelongsTo($organization)->firstOrFail();
$member = Member::whereBelongsTo($user)->whereBelongsTo($organization)->firstOrFail();
if ($member->role === Role::Placeholder->value) {
abort(403, 'Cannot update the role of a placeholder member.');
}
Expand Down
2 changes: 1 addition & 1 deletion app/Console/Commands/Test/TestJobCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class TestJobCommand extends Command
*/
public function handle(): int
{
$user = User::first();
$user = User::firstOrFail();
TestJob::dispatch($user, 'Test job message.');

return self::SUCCESS;
Expand Down
29 changes: 29 additions & 0 deletions app/Enums/TimeEntryAggregationType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum TimeEntryAggregationType: string
{
case Day = 'day';
case Week = 'week';
case Month = 'month';
case Year = 'year';
case User = 'user';
case Project = 'project';
case Task = 'task';
case Client = 'client';
case Billable = 'billable';

public function toInterval(): ?TimeEntryAggregationTypeInterval
{
return match ($this) {
TimeEntryAggregationType::Day => TimeEntryAggregationTypeInterval::Day,
TimeEntryAggregationType::Week => TimeEntryAggregationTypeInterval::Week,
TimeEntryAggregationType::Month => TimeEntryAggregationTypeInterval::Month,
TimeEntryAggregationType::Year => TimeEntryAggregationTypeInterval::Year,
default => null
};
}
}
13 changes: 13 additions & 0 deletions app/Enums/TimeEntryAggregationTypeInterval.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum TimeEntryAggregationTypeInterval: string
{
case Day = 'day';
case Week = 'week';
case Month = 'month';
case Year = 'year';
}
13 changes: 13 additions & 0 deletions app/Enums/Weekday.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ enum Weekday: string
case Saturday = 'saturday';
case Sunday = 'sunday';

public function toEndOfWeek(): self
{
return match ($this) {
Weekday::Monday => Weekday::Sunday,
Weekday::Tuesday => Weekday::Monday,
Weekday::Wednesday => Weekday::Tuesday,
Weekday::Thursday => Weekday::Wednesday,
Weekday::Friday => Weekday::Thursday,
Weekday::Saturday => Weekday::Friday,
Weekday::Sunday => Weekday::Saturday,
};
}

public function carbonWeekDay(): int
{
return match ($this) {
Expand Down
10 changes: 10 additions & 0 deletions app/Exceptions/Api/CanNotRemoveOwnerFromOrganization.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 CanNotRemoveOwnerFromOrganization extends ApiException
{
public const string KEY = 'can_not_remove_owner_from_organization';
}
6 changes: 5 additions & 1 deletion app/Filament/Resources/OrganizationResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,15 @@ public static function table(Table $table): Table
->icon('heroicon-o-inbox-arrow-down')
->action(function (Organization $record, array $data) {
try {
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
if ($file === null) {
throw new \Exception('File not found');
}
/** @var ReportDto $report */
$report = app(ImportService::class)->import(
$record,
$data['type'],
Storage::disk(config('filament.default_filesystem_disk'))->get($data['file'])
$file
);
Notification::make()
->title('Import successful')
Expand Down
49 changes: 49 additions & 0 deletions app/Http/Controllers/Api/V1/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

namespace App\Http\Controllers\Api\V1;

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

class Controller extends \App\Http\Controllers\Controller
{
Expand All @@ -25,8 +29,53 @@ protected function checkPermission(Organization $organization, string $permissio
}
}

/**
* @param array<string> $permissions
*
* @throws AuthorizationException
*/
protected function checkAnyPermission(Organization $organization, array $permissions): void
{
foreach ($permissions as $permission) {
if ($this->permissionStore->has($organization, $permission)) {
return;
}
}
throw new AuthorizationException();
}

protected function hasPermission(Organization $organization, string $permission): bool
{
return $this->permissionStore->has($organization, $permission);
}

/**
* @throws AuthorizationException
*/
protected function user(): User
{
/** @var User|null $user */
$user = Auth::user();
if ($user === null) {
Log::error('This function should only be called in authenticated context');
throw new AuthorizationException();
}

return $user;
}

/**
* @throws AuthorizationException
*/
protected function member(Organization $organization): Member
{
$user = $this->user();
$member = Member::query()->whereBelongsTo($organization, 'organization')->whereBelongsTo($user, 'user')->first();
if ($member === null) {
Log::error('This function should only be called in authenticated context after checking the user is a member of the organization');
throw new AuthorizationException();
}

return $member;
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/V1/InvitationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function store(Organization $organization, InvitationStoreRequest $reques
$this->checkPermission($organization, 'invitations:create');

app(InvitesTeamMembers::class)->invite(
$request->user(),
$this->user(),
$organization,
$request->input('email'),
$request->input('role')
Expand Down
45 changes: 25 additions & 20 deletions app/Http/Controllers/Api/V1/MemberController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@

namespace App\Http\Controllers\Api\V1;

use App\Enums\Role;
use App\Exceptions\Api\CanNotRemoveOwnerFromOrganization;
use App\Exceptions\Api\EntityStillInUseApiException;
use App\Exceptions\Api\UserNotPlaceholderApiException;
use App\Http\Requests\V1\Member\MemberIndexRequest;
use App\Http\Requests\V1\Member\MemberUpdateRequest;
use App\Http\Resources\V1\Member\MemberCollection;
use App\Http\Resources\V1\Member\MemberPivotResource;
use App\Http\Resources\V1\Member\MemberResource;
use App\Models\Membership;
use App\Models\Member;
use App\Models\Organization;
use App\Models\ProjectMember;
use App\Models\TimeEntry;
Expand All @@ -23,10 +25,10 @@

class MemberController extends Controller
{
protected function checkPermission(Organization $organization, string $permission, ?Membership $membership = null): void
protected function checkPermission(Organization $organization, string $permission, ?Member $member = null): void
{
parent::checkPermission($organization, $permission);
if ($membership !== null && $membership->organization_id !== $organization->id) {
if ($member !== null && $member->organization_id !== $organization->id) {
throw new AuthorizationException('Member does not belong to organization');
}
}
Expand Down Expand Up @@ -57,36 +59,39 @@ public function index(Organization $organization, MemberIndexRequest $request):
*
* @operationId updateMember
*/
public function update(Organization $organization, Membership $membership, MemberUpdateRequest $request): JsonResource
public function update(Organization $organization, Member $member, MemberUpdateRequest $request): JsonResource
{
$this->checkPermission($organization, 'members:update', $membership);
$this->checkPermission($organization, 'members:update', $member);

$membership->billable_rate = $request->input('billable_rate');
$membership->role = $request->input('role');
$membership->save();
$member->billable_rate = $request->input('billable_rate');
$member->role = $request->input('role');
$member->save();

return new MemberResource($membership);
return new MemberResource($member);
}

/**
* Remove a member of the organization.
*
* @throws AuthorizationException|EntityStillInUseApiException
* @throws AuthorizationException|EntityStillInUseApiException|CanNotRemoveOwnerFromOrganization
*
* @operationId removeMember
*/
public function destroy(Organization $organization, Membership $membership): JsonResponse
public function destroy(Organization $organization, Member $member): JsonResponse
{
$this->checkPermission($organization, 'members:delete', $membership);
$this->checkPermission($organization, 'members:delete', $member);

if (TimeEntry::query()->where('user_id', $membership->user_id)->whereBelongsTo($organization, 'organization')->exists()) {
if (TimeEntry::query()->where('user_id', $member->user_id)->whereBelongsTo($organization, 'organization')->exists()) {
throw new EntityStillInUseApiException('member', 'time_entry');
}
if (ProjectMember::query()->whereBelongsToOrganization($organization)->where('user_id', $membership->user_id)->exists()) {
if (ProjectMember::query()->whereBelongsToOrganization($organization)->where('user_id', $member->user_id)->exists()) {
throw new EntityStillInUseApiException('member', 'project_member');
}
if ($member->role === Role::Owner->value) {
throw new CanNotRemoveOwnerFromOrganization();
}

$membership->delete();
$member->delete();

return response()
->json(null, 204);
Expand All @@ -99,20 +104,20 @@ public function destroy(Organization $organization, Membership $membership): Jso
*
* @operationId invitePlaceholder
*/
public function invitePlaceholder(Organization $organization, Membership $membership, Request $request): JsonResponse
public function invitePlaceholder(Organization $organization, Member $member, Request $request): JsonResponse
{
$this->checkPermission($organization, 'members:invite-placeholder', $membership);
$user = $membership->user;
$this->checkPermission($organization, 'members:invite-placeholder', $member);
$user = $member->user;

if (! $user->is_placeholder) {
throw new UserNotPlaceholderApiException();
}

app(InvitesTeamMembers::class)->invite(
$request->user(),
$this->user(),
$organization,
$user->email,
'employee'
Role::Employee->value,
);

return response()->json(null, 204);
Expand Down
8 changes: 3 additions & 5 deletions app/Http/Controllers/Api/V1/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;

class ProjectController extends Controller
Expand All @@ -43,14 +42,13 @@ public function index(Organization $organization, ProjectIndexRequest $request):
{
$this->checkPermission($organization, 'projects:view');
$canViewAllProjects = $this->hasPermission($organization, 'projects:view:all');
/** @var User $user */
$user = Auth::user();
$user = $this->user();

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

if (! $canViewAllProjects) {
$projectsQuery->visibleByUser($user);
$projectsQuery->visibleByEmployee($user);
}

$projects = $projectsQuery->paginate(config('app.pagination_per_page_default'));
Expand Down Expand Up @@ -133,7 +131,7 @@ public function destroy(Organization $organization, Project $project): JsonRespo
}

DB::transaction(function () use (&$project) {
$project->members()->each(function (ProjectMember $member) {
$project->members->each(function (ProjectMember $member) {
$member->delete();
});

Expand Down
11 changes: 6 additions & 5 deletions app/Http/Controllers/Api/V1/ProjectMemberController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
use App\Http\Requests\V1\ProjectMember\ProjectMemberUpdateRequest;
use App\Http\Resources\V1\ProjectMember\ProjectMemberCollection;
use App\Http\Resources\V1\ProjectMember\ProjectMemberResource;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Project;
use App\Models\ProjectMember;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
Expand Down Expand Up @@ -62,17 +62,18 @@ public function store(Organization $organization, Project $project, ProjectMembe
{
$this->checkPermission($organization, 'project-members:create', $project);

$user = User::findOrFail((string) $request->input('user_id'));
if ($user->is_placeholder) {
$member = Member::findOrFail((string) $request->input('member_id'));
if ($member->user->is_placeholder) {
throw new InactiveUserCanNotBeUsedApiException();
}
if (ProjectMember::whereBelongsTo($project, 'project')->whereBelongsTo($user, 'user')->exists()) {
if (ProjectMember::whereBelongsTo($project, 'project')->whereBelongsTo($member, 'member')->exists()) {
throw new UserIsAlreadyMemberOfProjectApiException();
}

$projectMember = new ProjectMember();
$projectMember->billable_rate = $request->input('billable_rate');
$projectMember->user()->associate($user);
$projectMember->member()->associate($member);
$projectMember->user()->associate($member->user);
$projectMember->project()->associate($project);
$projectMember->save();

Expand Down
Loading

0 comments on commit 98514e4

Please sign in to comment.