Add ability to update billable rate of existing time entries

This commit is contained in:
Constantin Graf
2024-06-14 16:46:39 +02:00
committed by Gregor Vostrak
parent 7c26cee1ea
commit 2184b3c835
22 changed files with 1001 additions and 25 deletions

View File

@@ -17,6 +17,7 @@ use App\Models\Member;
use App\Models\Organization;
use App\Models\ProjectMember;
use App\Models\TimeEntry;
use App\Service\BillableRateService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -59,14 +60,22 @@ class MemberController extends Controller
*
* @operationId updateMember
*/
public function update(Organization $organization, Member $member, MemberUpdateRequest $request): JsonResource
public function update(Organization $organization, Member $member, MemberUpdateRequest $request, BillableRateService $billableRateService): JsonResource
{
$this->checkPermission($organization, 'members:update', $member);
$member->billable_rate = $request->input('billable_rate');
$member->role = $request->input('role');
if ($request->has('billable_rate')) {
$member->billable_rate = $request->getBillableRate();
}
if ($request->has('role')) {
$member->role = $request->input('role');
}
$member->save();
if ($request->getBillableRateUpdateTimeEntries()) {
$billableRateService->updateTimeEntriesBillableRateForMember($member);
}
return new MemberResource($member);
}

View File

@@ -7,6 +7,7 @@ namespace App\Http\Controllers\Api\V1;
use App\Http\Requests\V1\Organization\OrganizationUpdateRequest;
use App\Http\Resources\V1\Organization\OrganizationResource;
use App\Models\Organization;
use App\Service\BillableRateService;
use Illuminate\Auth\Access\AuthorizationException;
class OrganizationController extends Controller
@@ -32,7 +33,7 @@ class OrganizationController extends Controller
*
* @throws AuthorizationException
*/
public function update(Organization $organization, OrganizationUpdateRequest $request): OrganizationResource
public function update(Organization $organization, OrganizationUpdateRequest $request, BillableRateService $billableRateService): OrganizationResource
{
$this->checkPermission($organization, 'organizations:update');
@@ -40,6 +41,10 @@ class OrganizationController extends Controller
$organization->billable_rate = $request->getBillableRate();
$organization->save();
if ($request->getBillableRateUpdateTimeEntries()) {
$billableRateService->updateTimeEntriesBillableRateForOrganization($organization);
}
return new OrganizationResource($organization);
}
}

View File

@@ -14,6 +14,7 @@ use App\Models\Organization;
use App\Models\Project;
use App\Models\ProjectMember;
use App\Models\User;
use App\Service\BillableRateService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -101,7 +102,7 @@ class ProjectController extends Controller
*
* @operationId updateProject
*/
public function update(Organization $organization, Project $project, ProjectUpdateRequest $request): JsonResource
public function update(Organization $organization, Project $project, ProjectUpdateRequest $request, BillableRateService $billableRateService): JsonResource
{
$this->checkPermission($organization, 'projects:update', $project);
$project->name = $request->input('name');
@@ -111,6 +112,10 @@ class ProjectController extends Controller
$project->client_id = $request->input('client_id');
$project->save();
if ($request->getBillableRateUpdateTimeEntries()) {
$billableRateService->updateTimeEntriesBillableRateForProject($project);
}
return new ProjectResource($project);
}

View File

@@ -14,6 +14,7 @@ use App\Models\Member;
use App\Models\Organization;
use App\Models\Project;
use App\Models\ProjectMember;
use App\Service\BillableRateService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -87,12 +88,16 @@ class ProjectMemberController extends Controller
*
* @operationId updateProjectMember
*/
public function update(Organization $organization, ProjectMember $projectMember, ProjectMemberUpdateRequest $request): JsonResource
public function update(Organization $organization, ProjectMember $projectMember, ProjectMemberUpdateRequest $request, BillableRateService $billableRateService): JsonResource
{
$this->checkPermission($organization, 'project-members:update', projectMember: $projectMember);
$projectMember->billable_rate = $request->getBillableRate();
$projectMember->save();
if ($request->getBillableRateUpdateTimeEntries()) {
$billableRateService->updateTimeEntriesBillableRateForProjectMember($projectMember);
}
return new ProjectMemberResource($projectMember);
}

View File

@@ -23,16 +23,19 @@ class MemberUpdateRequest extends FormRequest
public function rules(): array
{
return [
'role' => [
'string',
// TODO: placeholder role should not be allowed
Rule::enum(Role::class),
],
'billable_rate' => [
'nullable',
'integer',
'min:0',
],
'role' => [
'required',
'billable_rate_update_time_entries' => [
'string',
// TODO: placeholder role should not be allowed
Rule::enum(Role::class),
'in:true,false',
],
];
}
@@ -43,4 +46,10 @@ class MemberUpdateRequest extends FormRequest
return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
}
public function getBillableRateUpdateTimeEntries(): bool
{
return $this->has('billable_rate_update_time_entries') &&
$this->input('billable_rate_update_time_entries') === 'true';
}
}

View File

@@ -31,6 +31,10 @@ class OrganizationUpdateRequest extends FormRequest
'integer',
'min:0',
],
'billable_rate_update_time_entries' => [
'string',
'in:true,false',
],
];
}
@@ -40,4 +44,10 @@ class OrganizationUpdateRequest extends FormRequest
return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
}
public function getBillableRateUpdateTimeEntries(): bool
{
return $this->has('billable_rate_update_time_entries') &&
$this->input('billable_rate_update_time_entries') === 'true';
}
}

View File

@@ -41,11 +41,6 @@ class ProjectUpdateRequest extends FormRequest
'required',
'boolean',
],
'billable_rate' => [
'nullable',
'integer',
'min:0',
],
'client_id' => [
'nullable',
new ExistsEloquent(Client::class, null, function (Builder $builder): Builder {
@@ -53,6 +48,15 @@ class ProjectUpdateRequest extends FormRequest
return $builder->whereBelongsTo($this->organization, 'organization');
}),
],
'billable_rate' => [
'nullable',
'integer',
'min:0',
],
'billable_rate_update_time_entries' => [
'string',
'in:true,false',
],
];
}
@@ -62,4 +66,10 @@ class ProjectUpdateRequest extends FormRequest
return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
}
public function getBillableRateUpdateTimeEntries(): bool
{
return $this->has('billable_rate_update_time_entries') &&
$this->input('billable_rate_update_time_entries') === 'true';
}
}

View File

@@ -26,6 +26,10 @@ class ProjectMemberUpdateRequest extends FormRequest
'integer',
'min:0',
],
'billable_rate_update_time_entries' => [
'string',
'in:true,false',
],
];
}
@@ -35,4 +39,10 @@ class ProjectMemberUpdateRequest extends FormRequest
return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
}
public function getBillableRateUpdateTimeEntries(): bool
{
return $this->has('billable_rate_update_time_entries') &&
$this->input('billable_rate_update_time_entries') === 'true';
}
}

View File

@@ -8,6 +8,7 @@ use App\Models\Concerns\HasUuids;
use Database\Factories\MemberFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Laravel\Jetstream\Membership as JetstreamMembership;
/**
@@ -50,4 +51,12 @@ class Member extends JetstreamMembership
{
return $this->belongsTo(Organization::class, 'organization_id');
}
/**
* @return HasMany<ProjectMember>
*/
public function projectMembers(): HasMany
{
return $this->hasMany(ProjectMember::class, 'member_id');
}
}

View File

@@ -9,9 +9,75 @@ use App\Models\Organization;
use App\Models\Project;
use App\Models\ProjectMember;
use App\Models\TimeEntry;
use Illuminate\Database\Eloquent\Builder;
class BillableRateService
{
public function updateTimeEntriesBillableRateForProjectMember(ProjectMember $projectMember): void
{
TimeEntry::query()
->where('billable', '=', true)
->where('member_id', '=', $projectMember->member_id)
->where('project_id', '=', $projectMember->project_id)
->update(['billable_rate' => $projectMember->billable_rate]);
}
public function updateTimeEntriesBillableRateForProject(Project $project): void
{
TimeEntry::query()
->where('billable', '=', true)
->where('organization_id', '=', $project->organization_id)
->whereBelongsTo($project, 'project')
->whereDoesntHave('member', function (Builder $query) use ($project) {
/** @var Builder<Member> $query */
$query->whereHas('projectMembers', function (Builder $query) use ($project) {
/** @var Builder<ProjectMember> $query */
$query->whereBelongsTo($project, 'project')
->whereNotNull('billable_rate');
});
})
->update(['billable_rate' => $project->billable_rate]);
}
public function updateTimeEntriesBillableRateForMember(Member $member): void
{
TimeEntry::query()
->where('billable', '=', true)
->where('organization_id', '=', $member->organization_id)
->where('member_id', '=', $member->getKey())
->whereDoesntHave('project', function (Builder $builder) use ($member): void {
/** @var Builder<Project> $builder */
$builder->whereNotNull('billable_rate')
->orWhereHas('members', function (Builder $builder) use ($member): void {
/** @var Builder<ProjectMember> $builder */
$builder->whereNotNull('billable_rate')
->where('member_id', '=', $member->getKey());
});
})
->update(['billable_rate' => $member->billable_rate]);
}
public function updateTimeEntriesBillableRateForOrganization(Organization $organization): void
{
TimeEntry::query()
->where('billable', '=', true)
->where('organization_id', '=', $organization->getKey())
->whereDoesntHave('member', function (Builder $builder) {
/** @var Builder<Member> $builder */
$builder->whereNotNull('billable_rate');
})
->whereDoesntHave('project', function (Builder $builder): void {
/** @var Builder<Project> $builder */
$builder->whereNotNull('billable_rate')
->orWhereHas('members', function (Builder $builder): void {
/** @var Builder<ProjectMember> $builder */
$builder->whereNotNull('billable_rate')
->whereRaw('member_id = time_entries.member_id');
});
})
->update(['billable_rate' => $organization->billable_rate]);
}
public function getBillableRateForTimeEntryWithGivenRelations(TimeEntry $timeEntry, ?ProjectMember $projectMember, ?Project $project, ?Member $member, ?Organization $organization): ?int
{
if (! $timeEntry->billable) {