mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-15 19:52:15 +01:00
Migrated endpoints from user to member; Renamed membership to member
This commit is contained in:
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
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;
|
||||
@@ -32,7 +32,7 @@ class UpdateMemberRole
|
||||
}
|
||||
|
||||
$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.');
|
||||
}
|
||||
|
||||
@@ -108,11 +108,15 @@ class OrganizationResource extends Resource
|
||||
->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')
|
||||
|
||||
@@ -5,8 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
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
|
||||
{
|
||||
@@ -29,4 +32,19 @@ class Controller extends \App\Http\Controllers\Controller
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class InvitationController extends Controller
|
||||
$this->checkPermission($organization, 'invitations:create');
|
||||
|
||||
app(InvitesTeamMembers::class)->invite(
|
||||
$request->user(),
|
||||
$this->user(),
|
||||
$organization,
|
||||
$request->input('email'),
|
||||
$request->input('role')
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Exceptions\Api\EntityStillInUseApiException;
|
||||
use App\Exceptions\Api\UserNotPlaceholderApiException;
|
||||
use App\Http\Requests\V1\Member\MemberIndexRequest;
|
||||
@@ -11,7 +12,7 @@ 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;
|
||||
@@ -23,10 +24,10 @@ use Laravel\Jetstream\Contracts\InvitesTeamMembers;
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -57,15 +58,15 @@ class MemberController extends Controller
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,18 +76,18 @@ class MemberController extends Controller
|
||||
*
|
||||
* @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');
|
||||
}
|
||||
|
||||
$membership->delete();
|
||||
$member->delete();
|
||||
|
||||
return response()
|
||||
->json(null, 204);
|
||||
@@ -99,20 +100,20 @@ class MemberController extends Controller
|
||||
*
|
||||
* @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);
|
||||
|
||||
@@ -17,7 +17,6 @@ use App\Models\User;
|
||||
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
|
||||
@@ -43,8 +42,7 @@ class ProjectController extends Controller
|
||||
{
|
||||
$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');
|
||||
|
||||
@@ -10,10 +10,10 @@ 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\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;
|
||||
@@ -62,17 +62,18 @@ class ProjectMemberController extends Controller
|
||||
{
|
||||
$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();
|
||||
|
||||
|
||||
@@ -12,11 +12,9 @@ use App\Http\Resources\V1\Task\TaskCollection;
|
||||
use App\Http\Resources\V1\Task\TaskResource;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Task;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TaskController extends Controller
|
||||
{
|
||||
@@ -41,8 +39,7 @@ class TaskController extends Controller
|
||||
{
|
||||
$this->checkPermission($organization, 'tasks:view');
|
||||
$canViewAllTasks = $this->hasPermission($organization, 'tasks:view:all');
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$user = $this->user();
|
||||
|
||||
$projectId = $request->input('project_id');
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Http\Requests\V1\TimeEntry\TimeEntryStoreRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateRequest;
|
||||
use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
|
||||
use App\Http\Resources\V1\TimeEntry\TimeEntryResource;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Service\TimeEntryFilter;
|
||||
@@ -22,9 +23,9 @@ use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Ramsey\Uuid\Type\Time;
|
||||
|
||||
class TimeEntryController extends Controller
|
||||
{
|
||||
@@ -48,7 +49,9 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource
|
||||
{
|
||||
if ($request->has('user_id') && $request->get('user_id') === Auth::id()) {
|
||||
/** @var Member|null $member */
|
||||
$member = $request->has('member_id') ? Member::query()->findOrFail($request->get('member_id')) : null;
|
||||
if ($member !== null && $member->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:view:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:view:all');
|
||||
@@ -62,7 +65,8 @@ class TimeEntryController extends Controller
|
||||
$filter->addBeforeFilter($request->input('before'));
|
||||
$filter->addAfterFilter($request->input('after'));
|
||||
$filter->addActiveFilter($request->input('active'));
|
||||
$filter->addUserIdFilter($request->input('user_id'));
|
||||
$filter->addMemberIdFilter($member);
|
||||
$filter->addMemberIdsFilter($request->input('member_ids'));
|
||||
$filter->addProjectIdsFilter($request->input('project_ids'));
|
||||
$filter->addTagIdsFilter($request->input('tag_ids'));
|
||||
$filter->addTaskIdsFilter($request->input('task_ids'));
|
||||
@@ -78,7 +82,7 @@ class TimeEntryController extends Controller
|
||||
$timeEntries = $timeEntriesQuery->get();
|
||||
|
||||
if ($timeEntries->count() === $limit && $request->has('only_full_dates') && (bool) $request->get('only_full_dates') === true) {
|
||||
$user = Auth::user();
|
||||
$user = $this->user();
|
||||
$timezone = app(TimezoneService::class)->getTimezoneFromUser($user);
|
||||
$lastDate = null;
|
||||
/** @var TimeEntry $timeEntry */
|
||||
@@ -113,11 +117,38 @@ class TimeEntryController extends Controller
|
||||
/**
|
||||
* Get aggregated time entries in organization
|
||||
*
|
||||
* This endpoint allows you to filter time entries and aggregate them by different criteria.
|
||||
* The parameters `group` and `sub_group` allow you to group the time entries by different criteria.
|
||||
* If the group parameters are all set to `null` or are all missing, the endpoint will aggregate all filtered time entries.
|
||||
*
|
||||
* @operationId getAggregatedTimeEntries
|
||||
*
|
||||
* @return array{
|
||||
* data: array{
|
||||
* grouped_data: null|array<array{
|
||||
* type: string,
|
||||
* value: string|null,
|
||||
* aggregate: int,
|
||||
* cost: int,
|
||||
* grouped_data: null|array<array{
|
||||
* type: string,
|
||||
* value: string|null,
|
||||
* aggregate: int,
|
||||
* cost: int
|
||||
* }>
|
||||
* }>,
|
||||
* aggregate: int,
|
||||
* cost: int
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function aggregate(Organization $organization, TimeEntryAggregateRequest $request): array
|
||||
{
|
||||
if ($request->has('user_id') && $request->get('user_id') === Auth::id()) {
|
||||
/** @var Member|null $member */
|
||||
$member = $request->has('member_id') ? Member::query()->findOrFail($request->get('member_id')) : null;
|
||||
if ($member !== null && $member->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:view:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:view:all');
|
||||
@@ -130,7 +161,8 @@ class TimeEntryController extends Controller
|
||||
$filter->addBeforeFilter($request->input('before'));
|
||||
$filter->addAfterFilter($request->input('after'));
|
||||
$filter->addActiveFilter($request->input('active'));
|
||||
$filter->addUserIdFilter($request->input('user_id'));
|
||||
$filter->addMemberIdFilter($member);
|
||||
$filter->addMemberIdsFilter($request->input('member_ids'));
|
||||
$filter->addProjectIdsFilter($request->input('project_ids'));
|
||||
$filter->addTagIdsFilter($request->input('tag_ids'));
|
||||
$filter->addTaskIdsFilter($request->input('task_ids'));
|
||||
@@ -138,10 +170,12 @@ class TimeEntryController extends Controller
|
||||
$filter->addBillableFilter($request->input('billable'));
|
||||
$timeEntriesQuery = $filter->get();
|
||||
|
||||
$user = Auth::user();
|
||||
$user = $this->user();
|
||||
|
||||
$group1Type = $request->get('group_1');
|
||||
$group2Type = $request->get('group_2');
|
||||
/** @var string|null $group1Type */
|
||||
$group1Type = $request->get('group');
|
||||
/** @var string|null $group2Type */
|
||||
$group2Type = $request->get('sub_group');
|
||||
|
||||
$group1Select = null;
|
||||
$group2Select = null;
|
||||
@@ -178,11 +212,15 @@ class TimeEntryController extends Controller
|
||||
$group1ResponseSum = 0;
|
||||
$group1ResponseCost = 0;
|
||||
foreach ($groupedAggregates as $group1 => $group1Aggregates) {
|
||||
/** @var string $group1 */
|
||||
$group2Response = [];
|
||||
if ($group2Select !== null) {
|
||||
$group2ResponseSum = 0;
|
||||
$group2ResponseCost = 0;
|
||||
foreach ($group1Aggregates as $group2 => $aggregate) {
|
||||
/** @var string $group2 */
|
||||
/** @var Collection<int, object{aggregate: int, cost: int}> $aggregate */
|
||||
/** @var string $group2Type */
|
||||
$group2Response[] = [
|
||||
'type' => $group2Type,
|
||||
'value' => $group2 === '' ? null : $group2,
|
||||
@@ -193,15 +231,18 @@ class TimeEntryController extends Controller
|
||||
$group2ResponseCost += (int) $aggregate->get(0)->cost;
|
||||
}
|
||||
} else {
|
||||
/** @var Collection<int, object{aggregate: int, cost: int}> $group1Aggregates */
|
||||
$group2ResponseSum = (int) $group1Aggregates->get(0)->aggregate;
|
||||
$group2ResponseCost = (int) $group1Aggregates->get(0)->cost;
|
||||
$group2Response = null;
|
||||
}
|
||||
|
||||
/** @var string $group1Type */
|
||||
$group1Response[] = [
|
||||
'type' => $group1Type,
|
||||
'value' => $group1 === '' ? null : $group1,
|
||||
'aggregate' => $group2ResponseSum,
|
||||
'cost' => $group2ResponseCost,
|
||||
'grouped_data' => $group2Response,
|
||||
];
|
||||
$group1ResponseSum += $group2ResponseSum;
|
||||
@@ -209,6 +250,7 @@ class TimeEntryController extends Controller
|
||||
}
|
||||
} else {
|
||||
$group1Response = null;
|
||||
/** @var Collection<int, object{aggregate: int, cost: int}> $timeEntriesAggregates */
|
||||
$group1ResponseSum = (int) $timeEntriesAggregates->get(0)->aggregate;
|
||||
$group1ResponseCost = (int) $timeEntriesAggregates->get(0)->cost;
|
||||
}
|
||||
@@ -266,18 +308,21 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
public function store(Organization $organization, TimeEntryStoreRequest $request): JsonResource
|
||||
{
|
||||
if ($request->get('user_id') === Auth::id()) {
|
||||
/** @var Member $member */
|
||||
$member = Member::query()->findOrFail($request->get('member_id'));
|
||||
if ($member->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:create:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:create:all');
|
||||
}
|
||||
|
||||
if ($request->get('end') === null && TimeEntry::query()->where('user_id', $request->get('user_id'))->where('end', null)->exists()) {
|
||||
if ($request->get('end') === null && TimeEntry::query()->whereBelongsTo($member, 'member')->where('end', null)->exists()) {
|
||||
throw new TimeEntryStillRunningApiException();
|
||||
}
|
||||
|
||||
$timeEntry = new TimeEntry();
|
||||
$timeEntry->fill($request->validated());
|
||||
$timeEntry->user_id = $member->user_id;
|
||||
$timeEntry->description = $request->get('description') ?? '';
|
||||
$timeEntry->organization()->associate($organization);
|
||||
$timeEntry->setComputedAttributeValue('billable_rate');
|
||||
@@ -295,10 +340,12 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource
|
||||
{
|
||||
if ($timeEntry->user_id === Auth::id() && $request->get('user_id') === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:update:own', $timeEntry);
|
||||
/** @var Member|null $member */
|
||||
$member = $request->has('member_id') ? Member::query()->findOrFail($request->get('member_id')) : null;
|
||||
if ($timeEntry->member->user_id === Auth::id() && $member?->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:update:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
|
||||
$this->checkPermission($organization, 'time-entries:update:all');
|
||||
}
|
||||
|
||||
if ($timeEntry->end !== null && $request->has('end') && $request->get('end') === null) {
|
||||
@@ -321,7 +368,7 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse
|
||||
{
|
||||
if ($timeEntry->user_id === Auth::id()) {
|
||||
if ($timeEntry->member->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:delete:own', $timeEntry);
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:delete:all', $timeEntry);
|
||||
|
||||
@@ -10,7 +10,6 @@ use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class UserTimeEntryController extends Controller
|
||||
@@ -24,8 +23,7 @@ class UserTimeEntryController extends Controller
|
||||
*/
|
||||
public function myActive(): JsonResource
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$user = $this->user();
|
||||
|
||||
$activeTimeEntriesOfUser = TimeEntry::query()
|
||||
->whereBelongsTo($user, 'user')
|
||||
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\ProjectMember;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
@@ -24,12 +24,12 @@ class ProjectMemberStoreRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => [
|
||||
'member_id' => [
|
||||
'required',
|
||||
'uuid',
|
||||
new ExistsEloquent(User::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<User> $builder */
|
||||
return $builder->belongsToOrganization($this->organization);
|
||||
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
'billable_rate' => [
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\Tag;
|
||||
@@ -28,14 +29,38 @@ class TimeEntryAggregateRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'group_1' => [
|
||||
'group' => [
|
||||
'nullable',
|
||||
'required_with:group_2',
|
||||
'in:day,week,month,year,user,project,task,client,billable',
|
||||
],
|
||||
|
||||
'group_2' => [
|
||||
'sub_group' => [
|
||||
'nullable',
|
||||
'in:day,week,month,year,user,project,task,client,billable',
|
||||
],
|
||||
// Filter by member ID
|
||||
'member_id' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
// Filter by multiple member IDs, member IDs are OR combined, but AND combined with the member_id parameter
|
||||
'member_ids' => [
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'member_ids.*' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
|
||||
// Filter by user ID
|
||||
'user_id' => [
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Task;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
@@ -28,13 +28,26 @@ class TimeEntryIndexRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// Filter by user ID
|
||||
'user_id' => [
|
||||
// Filter by member ID
|
||||
'member_id' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(User::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<User> $builder */
|
||||
return $builder->belongsToOrganization($this->organization);
|
||||
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
// Filter by multiple member IDs, member IDs are OR combined, but AND combined with the member_id parameter
|
||||
'member_ids' => [
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'member_ids.*' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
// Filter by project IDs, project IDs are OR combined
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Task;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
@@ -27,14 +27,14 @@ class TimeEntryStoreRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// ID of the user that the time entry should belong to
|
||||
'user_id' => [
|
||||
// ID of the organization member that the time entry should belong to
|
||||
'member_id' => [
|
||||
'required',
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(User::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<User> $builder */
|
||||
return $builder->belongsToOrganization($this->organization);
|
||||
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
'project_id' => [
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\Tag;
|
||||
@@ -26,6 +27,16 @@ class TimeEntryUpdateRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// ID of the organization member that the time entry should belong to
|
||||
'member_id' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
// ID of the project that the time entry should belong to
|
||||
'project_id' => [
|
||||
'nullable',
|
||||
'string',
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Http\Resources\V1\Member;
|
||||
|
||||
use App\Http\Resources\V1\BaseResource;
|
||||
use App\Models\Membership;
|
||||
use App\Models\Member;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -21,12 +21,12 @@ class MemberPivotResource extends BaseResource
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
/** @var Membership $membership */
|
||||
$membership = $this->resource->getRelationValue('membership');
|
||||
/** @var Member $member */
|
||||
$member = $this->resource->getRelationValue('membership');
|
||||
|
||||
return [
|
||||
/** @var string $id ID of membership */
|
||||
'id' => $membership->id,
|
||||
'id' => $member->id,
|
||||
/** @var string $id ID of user */
|
||||
'user_id' => $this->resource->id,
|
||||
/** @var string $name Name */
|
||||
@@ -34,11 +34,11 @@ class MemberPivotResource extends BaseResource
|
||||
/** @var string $email Email */
|
||||
'email' => $this->resource->email,
|
||||
/** @var string $role Role */
|
||||
'role' => $membership->role,
|
||||
'role' => $member->role,
|
||||
/** @var bool $is_placeholder Placeholder user for imports, user might not really exist and does not know about this placeholder membership */
|
||||
'is_placeholder' => $this->resource->is_placeholder,
|
||||
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||
'billable_rate' => $membership->billable_rate,
|
||||
'billable_rate' => $member->billable_rate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ declare(strict_types=1);
|
||||
namespace App\Http\Resources\V1\Member;
|
||||
|
||||
use App\Http\Resources\V1\BaseResource;
|
||||
use App\Models\Membership;
|
||||
use App\Models\Member;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* @property Membership $resource
|
||||
* @property Member $resource
|
||||
*/
|
||||
class MemberResource extends BaseResource
|
||||
{
|
||||
|
||||
@@ -25,8 +25,8 @@ class ProjectMemberResource extends BaseResource
|
||||
'id' => $this->resource->id,
|
||||
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||
'billable_rate' => $this->resource->billable_rate,
|
||||
/** @var string $user_id ID of the user */
|
||||
'user_id' => $this->resource->user_id,
|
||||
/** @var string $member_id ID of the organization member */
|
||||
'member_id' => $this->resource->member_id,
|
||||
/** @var string $project_id ID of the project */
|
||||
'project_id' => $this->resource->project_id,
|
||||
];
|
||||
|
||||
@@ -4,8 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Member;
|
||||
use App\Service\UserService;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Laravel\Jetstream\Events\TeamMemberAdded;
|
||||
|
||||
class RemovePlaceholder
|
||||
@@ -17,15 +18,21 @@ class RemovePlaceholder
|
||||
{
|
||||
/** @var UserService $userService */
|
||||
$userService = app(UserService::class);
|
||||
$placeholders = User::query()
|
||||
->where('is_placeholder', '=', true)
|
||||
->where('email', '=', $event->user->email)
|
||||
->belongsToOrganization($event->team)
|
||||
$placeholders = Member::query()
|
||||
->whereHas('user', function (Builder $query) use ($event) {
|
||||
$query->where('is_placeholder', '=', true)
|
||||
->where('email', '=', $event->user->email);
|
||||
})
|
||||
->whereBelongsTo($event->team, 'organization')
|
||||
->with(['user'])
|
||||
->get();
|
||||
|
||||
foreach ($placeholders as $placeholder) {
|
||||
$userService->assignOrganizationEntitiesToDifferentUser($event->team, $placeholder, $event->user);
|
||||
/** @var Member $placeholder */
|
||||
$placeholderUser = $placeholder->user;
|
||||
$userService->assignOrganizationEntitiesToDifferentUser($event->team, $placeholderUser, $event->user);
|
||||
$placeholder->delete();
|
||||
$placeholderUser->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\HasUuids;
|
||||
use Database\Factories\MembershipFactory;
|
||||
use Database\Factories\MemberFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Laravel\Jetstream\Membership as JetstreamMembership;
|
||||
@@ -21,9 +21,9 @@ use Laravel\Jetstream\Membership as JetstreamMembership;
|
||||
* @property-read Organization $organization
|
||||
* @property-read User $user
|
||||
*
|
||||
* @method static MembershipFactory factory()
|
||||
* @method static MemberFactory factory()
|
||||
*/
|
||||
class Membership extends JetstreamMembership
|
||||
class Member extends JetstreamMembership
|
||||
{
|
||||
use HasFactory;
|
||||
use HasUuids;
|
||||
@@ -33,10 +33,10 @@ class Membership extends JetstreamMembership
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'organization_user';
|
||||
protected $table = 'members';
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, Membership>
|
||||
* @return BelongsTo<User, Member>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
@@ -44,7 +44,7 @@ class Membership extends JetstreamMembership
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Organization, Membership>
|
||||
* @return BelongsTo<Organization, Member>
|
||||
*/
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
@@ -30,7 +30,7 @@ use Laravel\Jetstream\Team as JetstreamTeam;
|
||||
* @property Collection<int, User> $users
|
||||
* @property Collection<int, User> $realUsers
|
||||
* @property-read Collection<int, OrganizationInvitation> $teamInvitations
|
||||
* @property Membership $membership
|
||||
* @property Member $membership
|
||||
*
|
||||
* @method HasMany<OrganizationInvitation> teamInvitations()
|
||||
* @method static OrganizationFactory factory()
|
||||
|
||||
@@ -14,9 +14,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
/**
|
||||
* @property string $id
|
||||
* @property int|null $billable_rate
|
||||
* @property string $project_id
|
||||
* @property string $user_id
|
||||
* @property string $project_id Project ID
|
||||
* @property string $member_id Member ID
|
||||
* @property string $user_id User ID (legacy)
|
||||
* @property-read Project $project
|
||||
* @property-read Member $member
|
||||
* @property-read User $user
|
||||
*
|
||||
* @method static Builder<ProjectMember> whereBelongsToOrganization(Organization $organization)
|
||||
@@ -45,6 +47,8 @@ class ProjectMember extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use member relationship instead
|
||||
*
|
||||
* @return BelongsTo<User, ProjectMember>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
@@ -52,6 +56,14 @@ class ProjectMember extends Model
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Member, ProjectMember>
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class, 'member_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<ProjectMember> $builder
|
||||
*/
|
||||
|
||||
@@ -65,9 +65,14 @@ class Task extends Model
|
||||
return $this->hasMany(TimeEntry::class, 'task_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<Task> $builder
|
||||
* @return Builder<Task>
|
||||
*/
|
||||
public function scopeVisibleByUser(Builder $builder, User $user): Builder
|
||||
{
|
||||
return $builder->whereHas('project', function (Builder $builder) use ($user): Builder {
|
||||
/** @var Builder<Project> $builder */
|
||||
return $builder->visibleByUser($user);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ use Korridor\LaravelComputedAttributes\ComputedAttributes;
|
||||
* @property bool $billable
|
||||
* @property array $tags
|
||||
* @property string $user_id
|
||||
* @property string $member_id
|
||||
* @property-read User $user
|
||||
* @property-read Member $member
|
||||
* @property string $organization_id
|
||||
* @property-read Organization $organization
|
||||
* @property string|null $project_id
|
||||
@@ -91,6 +93,14 @@ class TimeEntry extends Model
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Member, TimeEntry>
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class, 'member_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Organization, TimeEntry>
|
||||
*/
|
||||
|
||||
@@ -43,7 +43,7 @@ use Laravel\Passport\HasApiTokens;
|
||||
* @property string $current_team_id
|
||||
* @property Collection<int, Organization> $organizations
|
||||
* @property Collection<int, TimeEntry> $timeEntries
|
||||
* @property Membership $membership
|
||||
* @property Member $membership
|
||||
*
|
||||
* @method HasMany<Organization> ownedTeams()
|
||||
* @method static UserFactory factory()
|
||||
@@ -136,7 +136,7 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail
|
||||
*/
|
||||
public function organizations(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Organization::class, Membership::class)
|
||||
return $this->belongsToMany(Organization::class, Member::class)
|
||||
->withPivot([
|
||||
'id',
|
||||
'role',
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Membership;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\OrganizationInvitation;
|
||||
use App\Models\Project;
|
||||
@@ -51,7 +51,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
|
||||
Model::preventAccessingMissingAttributes(! $this->app->isProduction());
|
||||
Relation::enforceMorphMap([
|
||||
'membership' => Membership::class,
|
||||
'membership' => Member::class,
|
||||
'organization' => Organization::class,
|
||||
'organization-invitation' => OrganizationInvitation::class,
|
||||
'user' => User::class,
|
||||
@@ -85,7 +85,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
return new PermissionStore();
|
||||
});
|
||||
|
||||
Route::model('member', Membership::class);
|
||||
Route::model('member', Member::class);
|
||||
Route::model('invitation', OrganizationInvitation::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Actions\Jetstream\UpdateMemberRole;
|
||||
use App\Actions\Jetstream\UpdateOrganization;
|
||||
use App\Enums\Role;
|
||||
use App\Enums\Weekday;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\OrganizationInvitation;
|
||||
use App\Models\User;
|
||||
@@ -50,6 +51,7 @@ class JetstreamServiceProvider extends ServiceProvider
|
||||
Jetstream::deleteTeamsUsing(DeleteOrganization::class);
|
||||
Jetstream::deleteUsersUsing(DeleteUser::class);
|
||||
Jetstream::useTeamModel(Organization::class);
|
||||
Jetstream::useMembershipModel(Member::class);
|
||||
Jetstream::useTeamInvitationModel(OrganizationInvitation::class);
|
||||
app()->singleton(UpdateTeamMemberRole::class, UpdateMemberRole::class);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Models\Membership;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectMember;
|
||||
@@ -36,13 +36,13 @@ class BillableRateService
|
||||
}
|
||||
}
|
||||
// Member rate
|
||||
/** @var Membership|null $membership */
|
||||
$membership = Membership::query()
|
||||
/** @var Member|null $member */
|
||||
$member = Member::query()
|
||||
->where('user_id', '=', $timeEntry->user_id)
|
||||
->where('organization_id', '=', $timeEntry->organization_id)
|
||||
->first();
|
||||
if ($membership !== null && $membership->billable_rate !== null) {
|
||||
return $membership->billable_rate;
|
||||
if ($member !== null && $member->billable_rate !== null) {
|
||||
return $member->billable_rate;
|
||||
}
|
||||
|
||||
// Organization rate
|
||||
|
||||
@@ -335,20 +335,22 @@ class DashboardService
|
||||
}
|
||||
|
||||
/**
|
||||
* Rhe 4 most recently active members of your team with user_id, name, description of the latest time entry, time_entry_id, task_id and a boolean status if the team member is currently working
|
||||
* Rhe 4 most recently active members of your team with member_id, name, description of the latest time entry, time_entry_id, task_id and a boolean status if the team member is currently working
|
||||
*
|
||||
* @return array<int, array{user_id: string, name: string, description: string|null, time_entry_id: string, task_id: string|null, status: bool }>
|
||||
* @return array<int, array{member_id: string, name: string, description: string|null, time_entry_id: string, task_id: string|null, status: bool }>
|
||||
*/
|
||||
public function latestTeamActivity(Organization $organization): array
|
||||
{
|
||||
$timeEntries = TimeEntry::query()
|
||||
->select(DB::raw('distinct on (user_id) user_id, description, id, task_id, start, "end"'))
|
||||
->select(DB::raw('distinct on (member_id) member_id, description, id, task_id, start, "end"'))
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
->orderBy('user_id')
|
||||
->orderBy('member_id')
|
||||
->orderBy('start', 'desc')
|
||||
// Note: limit here does not work because of the distinct on
|
||||
->with([
|
||||
'user',
|
||||
'member' => [
|
||||
'user',
|
||||
],
|
||||
])
|
||||
->get()
|
||||
->sortByDesc('start')
|
||||
@@ -358,8 +360,8 @@ class DashboardService
|
||||
|
||||
foreach ($timeEntries as $timeEntry) {
|
||||
$response[] = [
|
||||
'user_id' => $timeEntry->user_id,
|
||||
'name' => $timeEntry->user->name,
|
||||
'member_id' => $timeEntry->member_id,
|
||||
'name' => $timeEntry->member->user->name,
|
||||
'description' => $timeEntry->description,
|
||||
'time_entry_id' => $timeEntry->id,
|
||||
'task_id' => $timeEntry->task_id,
|
||||
|
||||
@@ -56,6 +56,10 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
||||
'timezone' => 'UTC',
|
||||
'is_placeholder' => true,
|
||||
]);
|
||||
$memberId = $this->memberImportHelper->getKey([
|
||||
'user_id' => $userId,
|
||||
'organization_id' => $this->organization->getKey(),
|
||||
]);
|
||||
$clientId = null;
|
||||
if ($record['Client'] !== '') {
|
||||
$clientId = $this->clientImportHelper->getKey([
|
||||
@@ -83,6 +87,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
||||
}
|
||||
$timeEntry = new TimeEntry();
|
||||
$timeEntry->user_id = $userId;
|
||||
$timeEntry->member_id = $memberId;
|
||||
$timeEntry->task_id = $taskId;
|
||||
$timeEntry->project_id = $projectId;
|
||||
$timeEntry->organization_id = $this->organization->id;
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Service\Import\Importers;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\Client;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectMember;
|
||||
@@ -26,6 +27,11 @@ abstract class DefaultImporter implements ImporterContract
|
||||
*/
|
||||
protected ImportDatabaseHelper $userImportHelper;
|
||||
|
||||
/**
|
||||
* @var ImportDatabaseHelper<Member>
|
||||
*/
|
||||
protected ImportDatabaseHelper $memberImportHelper;
|
||||
|
||||
/**
|
||||
* @var ImportDatabaseHelper<Project>
|
||||
*/
|
||||
@@ -77,6 +83,10 @@ abstract class DefaultImporter implements ImporterContract
|
||||
'timezone:all',
|
||||
],
|
||||
]);
|
||||
$this->memberImportHelper = new ImportDatabaseHelper(Member::class, ['user_id', 'organization_id'], true, function (Builder $builder) {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
});
|
||||
$this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'organization_id'], true, function (Builder $builder) {
|
||||
/** @var Builder<Project> $builder */
|
||||
return $builder->where('organization_id', $this->organization->id);
|
||||
@@ -90,7 +100,7 @@ abstract class DefaultImporter implements ImporterContract
|
||||
'integer',
|
||||
],
|
||||
]);
|
||||
$this->projectMemberImportHelper = new ImportDatabaseHelper(ProjectMember::class, ['project_id', 'user_id'], true, function (Builder $builder) {
|
||||
$this->projectMemberImportHelper = new ImportDatabaseHelper(ProjectMember::class, ['project_id', 'member_id'], true, function (Builder $builder) {
|
||||
/** @var Builder<ProjectMember> $builder */
|
||||
return $builder->whereBelongsToOrganization($this->organization);
|
||||
}, validate: [
|
||||
|
||||
@@ -74,13 +74,17 @@ class TogglDataImporter extends DefaultImporter
|
||||
}
|
||||
|
||||
foreach ($workspaceUsers as $workspaceUser) {
|
||||
$this->userImportHelper->getKey([
|
||||
$userId = $this->userImportHelper->getKey([
|
||||
'email' => $workspaceUser->email,
|
||||
], [
|
||||
'name' => $workspaceUser->name,
|
||||
'timezone' => $workspaceUser->timezone ?? 'UTC',
|
||||
'is_placeholder' => true,
|
||||
], (string) $workspaceUser->uid);
|
||||
$memberId = $this->memberImportHelper->getKey([
|
||||
'user_id' => $userId,
|
||||
'organization_id' => $this->organization->getKey(),
|
||||
], [], $userId);
|
||||
}
|
||||
|
||||
foreach ($projects as $project) {
|
||||
@@ -114,10 +118,12 @@ class TogglDataImporter extends DefaultImporter
|
||||
}
|
||||
$projectMembers = json_decode($projectMembersFileContent);
|
||||
foreach ($projectMembers as $projectMember) {
|
||||
$userId = $this->userImportHelper->getKeyByExternalIdentifier((string) $projectMember->user_id);
|
||||
$this->projectMemberImportHelper->getKey([
|
||||
'project_id' => $projectId,
|
||||
'user_id' => $this->userImportHelper->getKeyByExternalIdentifier((string) $projectMember->user_id),
|
||||
'member_id' => $this->memberImportHelper->getKeyByExternalIdentifier($userId),
|
||||
], [
|
||||
'user_id' => $userId,
|
||||
'billable_rate' => $projectMember->rate !== null ? (int) ($projectMember->rate * 100) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ class TogglTimeEntriesImporter extends DefaultImporter
|
||||
'timezone' => 'UTC',
|
||||
'is_placeholder' => true,
|
||||
]);
|
||||
$memberId = $this->memberImportHelper->getKey([
|
||||
'user_id' => $userId,
|
||||
'organization_id' => $this->organization->getKey(),
|
||||
]);
|
||||
$clientId = null;
|
||||
if ($record['Client'] !== '') {
|
||||
$clientId = $this->clientImportHelper->getKey([
|
||||
@@ -83,6 +87,7 @@ class TogglTimeEntriesImporter extends DefaultImporter
|
||||
}
|
||||
$timeEntry = new TimeEntry();
|
||||
$timeEntry->user_id = $userId;
|
||||
$timeEntry->member_id = $memberId;
|
||||
$timeEntry->task_id = $taskId;
|
||||
$timeEntry->project_id = $projectId;
|
||||
$timeEntry->organization_id = $this->organization->id;
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\TimeEntry;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -59,12 +60,25 @@ class TimeEntryFilter
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addUserIdFilter(?string $userId): self
|
||||
public function addMemberIdFilter(?Member $member): self
|
||||
{
|
||||
if ($userId === null) {
|
||||
if ($member === null) {
|
||||
return $this;
|
||||
}
|
||||
$this->builder->where('user_id', $userId);
|
||||
$this->builder->where('member_id', $member->getKey());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $memberIds
|
||||
*/
|
||||
public function addMemberIdsFilter(?array $memberIds): self
|
||||
{
|
||||
if ($memberIds === null) {
|
||||
return $this;
|
||||
}
|
||||
$this->builder->whereIn('member_id', $memberIds);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Service;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\Membership;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\ProjectMember;
|
||||
use App\Models\TimeEntry;
|
||||
@@ -19,12 +19,22 @@ class UserService
|
||||
*/
|
||||
public function assignOrganizationEntitiesToDifferentUser(Organization $organization, User $fromUser, User $toUser): void
|
||||
{
|
||||
/** @var Member|null $toMember */
|
||||
$toMember = Member::query()
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
->whereBelongsTo($toUser, 'user')
|
||||
->first();
|
||||
if ($toMember === null) {
|
||||
throw new \InvalidArgumentException('User is not a member of the organization');
|
||||
}
|
||||
|
||||
// Time entries
|
||||
TimeEntry::query()
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
->whereBelongsTo($fromUser, 'user')
|
||||
->update([
|
||||
'user_id' => $toUser->getKey(),
|
||||
'member_id' => $toMember->getKey(),
|
||||
]);
|
||||
|
||||
// Project members
|
||||
@@ -33,6 +43,7 @@ class UserService
|
||||
->whereBelongsTo($fromUser, 'user')
|
||||
->update([
|
||||
'user_id' => $toUser->getKey(),
|
||||
'member_id' => $toMember->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -45,13 +56,17 @@ class UserService
|
||||
$organization->update([
|
||||
'user_id' => $newOwner->getKey(),
|
||||
]);
|
||||
$userMembership = Membership::query()
|
||||
/** @var Member|null $userMembership */
|
||||
$userMembership = Member::query()
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
->whereBelongsTo($newOwner, 'user')
|
||||
->first();
|
||||
if ($userMembership === null) {
|
||||
throw new \InvalidArgumentException('User is not a member of the organization');
|
||||
}
|
||||
$userMembership->role = Role::Owner->value;
|
||||
$userMembership->save();
|
||||
$oldOwners = Membership::query()
|
||||
$oldOwners = Member::query()
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
->where('role', '=', Role::Owner->value)
|
||||
->where('user_id', '!=', $newOwner->getKey())
|
||||
|
||||
Reference in New Issue
Block a user