diff --git a/.gitignore b/.gitignore index ffdee140..825fdc91 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ yarn-error.log /playwright/.cache/ /coverage /extensions/* +/modules_statuses.json diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 9fc33f6a..665888ee 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -4,6 +4,7 @@ namespace App\Actions\Fortify; +use App\Enums\Weekday; use App\Models\Organization; use App\Models\User; use App\Service\TimezoneService; @@ -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); }); diff --git a/app/Actions/Jetstream/UpdateOrganization.php b/app/Actions/Jetstream/UpdateOrganization.php index ff0e10da..d47de23f 100644 --- a/app/Actions/Jetstream/UpdateOrganization.php +++ b/app/Actions/Jetstream/UpdateOrganization.php @@ -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; @@ -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(); } } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 1f4f61ee..3b12bc9c 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -23,7 +23,5 @@ protected function schedule(Schedule $schedule): void protected function commands(): void { $this->load(__DIR__.'/Commands'); - - require base_path('routes/console.php'); } } diff --git a/app/Exceptions/Api/TimeEntryCanNotBeRestartedApiException.php b/app/Exceptions/Api/TimeEntryCanNotBeRestartedApiException.php new file mode 100644 index 00000000..34e73fb5 --- /dev/null +++ b/app/Exceptions/Api/TimeEntryCanNotBeRestartedApiException.php @@ -0,0 +1,10 @@ +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); + } } diff --git a/app/Http/Controllers/Api/V1/MemberController.php b/app/Http/Controllers/Api/V1/MemberController.php index 5e958884..d3b8f447 100644 --- a/app/Http/Controllers/Api/V1/MemberController.php +++ b/app/Http/Controllers/Api/V1/MemberController.php @@ -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; @@ -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 @@ 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(); diff --git a/app/Http/Controllers/Api/V1/OrganizationController.php b/app/Http/Controllers/Api/V1/OrganizationController.php index 8d6a1e5d..8346e143 100644 --- a/app/Http/Controllers/Api/V1/OrganizationController.php +++ b/app/Http/Controllers/Api/V1/OrganizationController.php @@ -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); diff --git a/app/Http/Controllers/Api/V1/ProjectController.php b/app/Http/Controllers/Api/V1/ProjectController.php index 7a7e0ccd..63d2ff16 100644 --- a/app/Http/Controllers/Api/V1/ProjectController.php +++ b/app/Http/Controllers/Api/V1/ProjectController.php @@ -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 { @@ -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); } @@ -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(); @@ -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); diff --git a/app/Http/Controllers/Api/V1/ProjectMemberController.php b/app/Http/Controllers/Api/V1/ProjectMemberController.php new file mode 100644 index 00000000..3c0cf5ae --- /dev/null +++ b/app/Http/Controllers/Api/V1/ProjectMemberController.php @@ -0,0 +1,102 @@ +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 + * + * @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); + } +} diff --git a/app/Http/Controllers/Api/V1/TimeEntryController.php b/app/Http/Controllers/Api/V1/TimeEntryController.php index 0ccc0eb4..10c5d338 100644 --- a/app/Http/Controllers/Api/V1/TimeEntryController.php +++ b/app/Http/Controllers/Api/V1/TimeEntryController.php @@ -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; @@ -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) { @@ -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); @@ -133,7 +136,7 @@ public function store(Organization $organization, TimeEntryStoreRequest $request /** * Update time entry * - * @throws AuthorizationException + * @throws AuthorizationException|TimeEntryCanNotBeRestartedApiException * * @operationId updateTimeEntry */ @@ -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) ?? ''; diff --git a/app/Http/Controllers/Web/DashboardController.php b/app/Http/Controllers/Web/DashboardController.php index d3efd9e4..9ba17cf9 100644 --- a/app/Http/Controllers/Web/DashboardController.php +++ b/app/Http/Controllers/Web/DashboardController.php @@ -4,6 +4,7 @@ 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 @@ public function dashboard(DashboardService $dashboardService): Response { /** @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 @@ public function dashboard(DashboardService $dashboardService): Response ], ], 'dailyTrackedHours' => $dailyTrackedHours, - 'totalWeeklyTime' => 400, - 'totalWeeklyBillableTime' => 300, - 'totalWeeklyBillableAmount' => [ - 'value' => 300.5, - 'currency' => 'USD', - ], + 'totalWeeklyTime' => $totalWeeklyTime, + 'totalWeeklyBillableTime' => $totalWeeklyBillableTime, + 'totalWeeklyBillableAmount' => $totalWeeklyBillableAmount, 'weeklyHistory' => $weeklyHistory, ]); } diff --git a/app/Http/Requests/V1/User/UserIndexRequest.php b/app/Http/Requests/V1/Member/MemberIndexRequest.php similarity index 83% rename from app/Http/Requests/V1/User/UserIndexRequest.php rename to app/Http/Requests/V1/Member/MemberIndexRequest.php index f600d01b..77c4a592 100644 --- a/app/Http/Requests/V1/User/UserIndexRequest.php +++ b/app/Http/Requests/V1/Member/MemberIndexRequest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Http\Requests\V1\User; +namespace App\Http\Requests\V1\Member; use App\Models\Organization; use Illuminate\Contracts\Validation\ValidationRule; @@ -11,7 +11,7 @@ /** * @property Organization $organization */ -class UserIndexRequest extends FormRequest +class MemberIndexRequest extends FormRequest { /** * Get the validation rules that apply to the request. diff --git a/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php b/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php index 6bc6f6c3..69981194 100644 --- a/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php +++ b/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php @@ -26,6 +26,11 @@ public function rules(): array 'string', 'max:255', ], + 'billable_rate' => [ + 'nullable', + 'integer', + 'min:0', + ], ]; } } diff --git a/app/Http/Requests/V1/Project/ProjectStoreRequest.php b/app/Http/Requests/V1/Project/ProjectStoreRequest.php index ca57a537..e8b34136 100644 --- a/app/Http/Requests/V1/Project/ProjectStoreRequest.php +++ b/app/Http/Requests/V1/Project/ProjectStoreRequest.php @@ -38,6 +38,11 @@ public function rules(): array 'max:255', new ColorRule(), ], + 'billable_rate' => [ + 'nullable', + 'integer', + 'min:0', + ], 'client_id' => [ 'nullable', new ExistsEloquent(Client::class, null, function (Builder $builder): Builder { diff --git a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php index 82259815..e421cd9e 100644 --- a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php +++ b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php @@ -37,6 +37,11 @@ public function rules(): array 'max:255', new ColorRule(), ], + 'billable_rate' => [ + 'nullable', + 'integer', + 'min:0', + ], 'client_id' => [ 'nullable', new ExistsEloquent(Client::class, null, function (Builder $builder): Builder { diff --git a/app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php b/app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php new file mode 100644 index 00000000..adb37e53 --- /dev/null +++ b/app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php @@ -0,0 +1,42 @@ +> + */ + public function rules(): array + { + return [ + 'user_id' => [ + 'required', + 'uuid', + new ExistsEloquent(User::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->belongsToOrganization($this->organization); + }), + ], + 'billable_rate' => [ + 'nullable', + 'integer', + 'min:0', + ], + ]; + } +} diff --git a/app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php b/app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php new file mode 100644 index 00000000..7401e635 --- /dev/null +++ b/app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php @@ -0,0 +1,31 @@ +> + */ + public function rules(): array + { + return [ + 'billable_rate' => [ + 'nullable', + 'integer', + 'min:0', + ], + ]; + } +} diff --git a/app/Http/Requests/V1/Task/TaskUpdateRequest.php b/app/Http/Requests/V1/Task/TaskUpdateRequest.php index ad5783a5..bd6a4a75 100644 --- a/app/Http/Requests/V1/Task/TaskUpdateRequest.php +++ b/app/Http/Requests/V1/Task/TaskUpdateRequest.php @@ -5,11 +5,8 @@ namespace App\Http\Requests\V1\Task; use App\Models\Organization; -use App\Models\Project; use Illuminate\Contracts\Validation\ValidationRule; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Foundation\Http\FormRequest; -use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent; /** * @property Organization $organization Organization from model binding @@ -31,12 +28,6 @@ public function rules(): array 'min:1', 'max:255', ], - 'project_id' => [ - new ExistsEloquent(Project::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - }), - ], ]; } } diff --git a/app/Http/Resources/V1/Organization/OrganizationResource.php b/app/Http/Resources/V1/Organization/OrganizationResource.php index a43f85b1..071f563a 100644 --- a/app/Http/Resources/V1/Organization/OrganizationResource.php +++ b/app/Http/Resources/V1/Organization/OrganizationResource.php @@ -27,6 +27,8 @@ public function toArray(Request $request): array 'name' => $this->resource->name, /** @var string $color Personal organizations automatically created after registration */ 'is_personal' => $this->resource->personal_team, + /** @var int|null $billable_rate Billable rate in cents per hour */ + 'billable_rate' => $this->resource->billable_rate, ]; } } diff --git a/app/Http/Resources/V1/Project/ProjectResource.php b/app/Http/Resources/V1/Project/ProjectResource.php index 218ab6c7..97fa742e 100644 --- a/app/Http/Resources/V1/Project/ProjectResource.php +++ b/app/Http/Resources/V1/Project/ProjectResource.php @@ -29,6 +29,8 @@ public function toArray(Request $request): array 'color' => $this->resource->color, /** @var string|null $client_id ID of client */ 'client_id' => $this->resource->client_id, + /** @var int|null $billable_rate Billable rate in cents per hour */ + 'billable_rate' => $this->resource->billable_rate, ]; } } diff --git a/app/Http/Resources/V1/ProjectMember/ProjectMemberCollection.php b/app/Http/Resources/V1/ProjectMember/ProjectMemberCollection.php new file mode 100644 index 00000000..be17c753 --- /dev/null +++ b/app/Http/Resources/V1/ProjectMember/ProjectMemberCollection.php @@ -0,0 +1,18 @@ + + */ + public function toArray(Request $request): array + { + return [ + /** @var string $id ID of project member */ + '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 $project_id ID of the project */ + 'project_id' => $this->resource->project_id, + ]; + } +} diff --git a/app/Http/Resources/V1/User/MemberResource.php b/app/Http/Resources/V1/User/MemberResource.php index f1a99ec9..a70d8fb3 100644 --- a/app/Http/Resources/V1/User/MemberResource.php +++ b/app/Http/Resources/V1/User/MemberResource.php @@ -35,6 +35,8 @@ public function toArray(Request $request): array 'role' => $membership->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, ]; } } diff --git a/app/Models/Membership.php b/app/Models/Membership.php index fa7c627d..d8cb3d6d 100644 --- a/app/Models/Membership.php +++ b/app/Models/Membership.php @@ -10,6 +10,7 @@ /** * @property string $id * @property string $role + * @property int|null $billable_rate * @property string $organization_id * @property string $user_id * @property string $created_at diff --git a/app/Models/Organization.php b/app/Models/Organization.php index 5e6a99c3..d03faedb 100644 --- a/app/Models/Organization.php +++ b/app/Models/Organization.php @@ -20,6 +20,8 @@ * @property string $id * @property string $name * @property bool $personal_team + * @property string $currency + * @property int|null $billable_rate * @property User $owner * @property Collection $users * @property Collection $realUsers @@ -40,6 +42,7 @@ class Organization extends JetstreamTeam protected $casts = [ 'name' => 'string', 'personal_team' => 'boolean', + 'currency' => 'string', ]; /** @@ -63,6 +66,15 @@ class Organization extends JetstreamTeam 'deleted' => TeamDeleted::class, ]; + /** + * The model's default values for attributes. + * + * @var array + */ + protected $attributes = [ + 'currency' => 'EUR', + ]; + /** * Get all the non-placeholder users of the organization including its owner. * @@ -88,7 +100,10 @@ public function hasRealUserWithEmail(string $email): bool public function users(): BelongsToMany { return $this->belongsToMany(Jetstream::userModel(), Jetstream::membershipModel()) - ->withPivot('role') + ->withPivot([ + 'role', + 'billable_rate', + ]) ->withTimestamps() ->as('membership'); } diff --git a/app/Models/Project.php b/app/Models/Project.php index 6e772c41..043d3172 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -18,6 +18,7 @@ * @property string $color * @property string $organization_id * @property string $client_id + * @property int|null $billable_rate * @property-read Organization $organization * @property-read Client|null $client * @property-read Collection $tasks @@ -55,6 +56,14 @@ public function client(): BelongsTo return $this->belongsTo(Client::class, 'client_id'); } + /** + * @return HasMany + */ + public function members(): HasMany + { + return $this->hasMany(ProjectMember::class); + } + /** * @return HasMany */ diff --git a/app/Models/ProjectMember.php b/app/Models/ProjectMember.php new file mode 100644 index 00000000..cc1a26f4 --- /dev/null +++ b/app/Models/ProjectMember.php @@ -0,0 +1,52 @@ + + */ + protected $casts = [ + 'billable_rate' => 'int', + ]; + + /** + * @return BelongsTo + */ + public function project(): BelongsTo + { + return $this->belongsTo(Project::class, 'project_id'); + } + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } +} diff --git a/app/Models/TimeEntry.php b/app/Models/TimeEntry.php index 5e8e44df..47af77c9 100644 --- a/app/Models/TimeEntry.php +++ b/app/Models/TimeEntry.php @@ -4,6 +4,7 @@ namespace App\Models; +use App\Service\BillableRateService; use Carbon\CarbonInterval; use Database\Factories\TimeEntryFactory; use Illuminate\Database\Eloquent\Concerns\HasUuids; @@ -11,12 +12,14 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Carbon; +use Korridor\LaravelComputedAttributes\ComputedAttributes; /** * @property string $id * @property string $description * @property Carbon $start * @property Carbon|null $end + * @property int $billable_rate Billable rate per hour in cents * @property bool $billable * @property array $tags * @property string $user_id @@ -32,6 +35,7 @@ */ class TimeEntry extends Model { + use ComputedAttributes; use HasFactory; use HasUuids; @@ -46,8 +50,24 @@ class TimeEntry extends Model 'end' => 'datetime', 'billable' => 'bool', 'tags' => 'array', + 'billable_rate' => 'int', ]; + /** + * The attributes that are computed. (f.e. for performance reasons) + * These attributes can be regenerated at any time. + * + * @var string[] + */ + protected array $computed = [ + 'billable_rate', + ]; + + public function getBillableRateComputed(): ?int + { + return app(BillableRateService::class)->getBillableRateForTimeEntry($this); + } + public function getDuration(): ?CarbonInterval { return $this->end === null ? null : $this->start->diffAsCarbonInterval($this->end); diff --git a/app/Models/User.php b/app/Models/User.php index 9f1891e1..07e3f096 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -115,6 +115,7 @@ public function organizations(): BelongsToMany return $this->belongsToMany(Organization::class, Membership::class) ->withPivot([ 'role', + 'billable_rate', ]) ->withTimestamps() ->as('membership'); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 2674c1bf..67984f3d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -13,6 +13,7 @@ use App\Models\Task; use App\Models\TimeEntry; use App\Models\User; +use App\Service\PermissionStore; use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\SecurityScheme; @@ -20,6 +21,7 @@ use Filament\Forms\Components\Section; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Foundation\Application; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; @@ -77,5 +79,9 @@ public function boot(): void URL::forceScheme('https'); request()->server->set('HTTPS', request()->header('X-Forwarded-Proto', 'https') === 'https' ? 'on' : 'off'); } + + $this->app->scoped(PermissionStore::class, function (Application $app): PermissionStore { + return new PermissionStore(); + }); } } diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php deleted file mode 100644 index 027e187f..00000000 --- a/app/Providers/BroadcastServiceProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -description('Administrator users can perform any action.'); Jetstream::role('manager', 'Manager', [ 'projects:view', + 'projects:view:all', 'projects:create', 'projects:update', 'projects:delete', + 'project-members:view', + 'project-members:create', + 'project-members:update', + 'project-members:delete', 'tasks:view', 'tasks:create', 'tasks:update', @@ -108,7 +120,7 @@ protected function configurePermissions(): void 'tags:update', 'tags:delete', 'organizations:view', - 'users:view', + 'members:view', ])->description('Managers have the ability to read, create, and update their own time entries as well as those of their team.'); Jetstream::role('employee', 'Employee', [ @@ -125,14 +137,25 @@ protected function configurePermissions(): void Jetstream::role('placeholder', 'Placeholder', [ ])->description('Placeholders are used for importing data. They cannot log in and have no permissions.'); - Jetstream::inertia()->whenRendering( - 'Profile/Show', - function (Request $request, array $data) { - return array_merge($data, [ - 'timezones' => $this->app->get(TimezoneService::class)->getSelectOptions(), - 'weekdays' => Weekday::toSelectArray(), - ]); - } - ); + Jetstream::inertia() + ->whenRendering( + 'Profile/Show', + function (Request $request, array $data): array { + return array_merge($data, [ + 'timezones' => $this->app->get(TimezoneService::class)->getSelectOptions(), + 'weekdays' => Weekday::toSelectArray(), + ]); + } + ) + ->whenRendering( + 'Teams/Show', + function (Request $request, array $data): array { + return array_merge($data, [ + 'currencies' => array_map(function (Currency $currency): string { + return $currency->getName(); + }, ISOCurrencyProvider::getInstance()->getAvailableCurrencies()), + ]); + } + ); } } diff --git a/app/Rules/CurrencyRule.php b/app/Rules/CurrencyRule.php new file mode 100644 index 00000000..a6cd850d --- /dev/null +++ b/app/Rules/CurrencyRule.php @@ -0,0 +1,34 @@ +getAvailableCurrencies(); + if (array_key_exists($value, $currencies)) { + return; + } + + $fail(__('validation.currency')); + } +} diff --git a/app/Service/BillableRateService.php b/app/Service/BillableRateService.php new file mode 100644 index 00000000..791ec5eb --- /dev/null +++ b/app/Service/BillableRateService.php @@ -0,0 +1,59 @@ +billable) { + return null; + } + if ($timeEntry->project_id !== null) { + // Project member rate + /** @var ProjectMember|null $projectMember */ + $projectMember = ProjectMember::query() + ->where('user_id', '=', $timeEntry->user_id) + ->where('project_id', '=', $timeEntry->project_id) + ->first(); + if ($projectMember !== null && $projectMember->billable_rate !== null) { + return $projectMember->billable_rate; + } + + // Project rate + /** @var Project|null $project */ + $project = Project::find($timeEntry->project_id); + if ($project !== null && $project->billable_rate !== null) { + return $project->billable_rate; + } + } + // Member rate + /** @var Membership|null $membership */ + $membership = Membership::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; + } + + // Organization rate + /** @var Organization|null $organization */ + $organization = Organization::query() + ->where('id', '=', $timeEntry->organization_id) + ->first(); + if ($organization !== null && $organization->billable_rate !== null) { + return $organization->billable_rate; + } + + return null; + } +} diff --git a/app/Service/DashboardService.php b/app/Service/DashboardService.php index fcc2cbc8..164edbc2 100644 --- a/app/Service/DashboardService.php +++ b/app/Service/DashboardService.php @@ -5,6 +5,7 @@ namespace App\Service; use App\Enums\Weekday; +use App\Models\Organization; use App\Models\TimeEntry; use App\Models\User; use Carbon\Carbon; @@ -86,7 +87,7 @@ private function constrainDateByPossibleDates(Builder $builder, Collection $poss * * @return array */ - public function getDailyTrackedHours(User $user, int $days): array + public function getDailyTrackedHours(User $user, Organization $organization, int $days): array { $timezone = $this->timezoneService->getTimezoneFromUser($user); $timezoneShift = $this->timezoneService->getShiftFromUtc($timezone); @@ -103,7 +104,8 @@ public function getDailyTrackedHours(User $user, int $days): array $query = TimeEntry::query() ->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) - ->where('user_id', '=', $user->id) + ->where('user_id', '=', $user->getKey()) + ->where('organization_id', '=', $organization->getKey()) ->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) ->orderBy('date'); @@ -128,7 +130,7 @@ public function getDailyTrackedHours(User $user, int $days): array * * @return array */ - public function getWeeklyHistory(User $user): array + public function getWeeklyHistory(User $user, Organization $organization): array { $timezone = $this->timezoneService->getTimezoneFromUser($user); $timezoneShift = $this->timezoneService->getShiftFromUtc($timezone); @@ -143,7 +145,8 @@ public function getWeeklyHistory(User $user): array $query = TimeEntry::query() ->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) - ->where('user_id', '=', $user->id) + ->where('user_id', '=', $user->getKey()) + ->where('organization_id', '=', $organization->getKey()) ->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) ->orderBy('date'); @@ -162,4 +165,92 @@ public function getWeeklyHistory(User $user): array return $result; } + + public function totalWeeklyTime(User $user, Organization $organization): int + { + $timezone = $this->timezoneService->getTimezoneFromUser($user); + $possibleDays = $this->daysOfThisWeek($timezone, $user->week_start); + + $query = TimeEntry::query() + ->select(DB::raw('round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) + ->where('user_id', '=', $user->getKey()) + ->where('organization_id', '=', $organization->getKey()); + + $query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone); + /** @var Collection $resultDb */ + $resultDb = $query->get(); + + return (int) $resultDb->get(0)->aggregate; + } + + public function totalWeeklyBillableTime(User $user, Organization $organization): int + { + $timezone = $this->timezoneService->getTimezoneFromUser($user); + $possibleDays = $this->daysOfThisWeek($timezone, $user->week_start); + + $query = TimeEntry::query() + ->select(DB::raw('round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) + ->where('billable', '=', true) + ->where('user_id', '=', $user->getKey()) + ->where('organization_id', '=', $organization->getKey()); + + $query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone); + /** @var Collection $resultDb */ + $resultDb = $query->get(); + + return (int) $resultDb->get(0)->aggregate; + } + + /** + * @return array{value: int, currency: string} + */ + public function totalWeeklyBillableAmount(User $user, Organization $organization): array + { + $timezone = $this->timezoneService->getTimezoneFromUser($user); + $possibleDays = $this->daysOfThisWeek($timezone, $user->week_start); + + $query = TimeEntry::query() + ->select(DB::raw(' + round( + sum( + extract(epoch from (coalesce("end", now()) - start)) * (billable_rate::float/60/60) + ) + ) as aggregate')) + ->where('billable', '=', true) + ->whereNotNull('billable_rate') + ->where('user_id', '=', $user->id); + + $query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone); + /** @var Collection $resultDb */ + $resultDb = $query->get(); + + return [ + 'value' => (int) $resultDb->get(0)->aggregate, + 'currency' => $organization->currency, + ]; + } + + /** + * @return array + */ + public function weeklyProjectOverview(User $user, Organization $organization): array + { + return [ + [ + 'value' => 120, + 'name' => 'Project 11', + 'color' => '#26a69a', + ], + [ + 'value' => 200, + 'name' => 'Project 2', + 'color' => '#d4e157', + ], + [ + 'value' => 150, + 'name' => 'Project 3', + 'color' => '#ff7043', + ], + ]; + } } diff --git a/app/Service/PermissionStore.php b/app/Service/PermissionStore.php new file mode 100644 index 00000000..63025518 --- /dev/null +++ b/app/Service/PermissionStore.php @@ -0,0 +1,43 @@ +> + */ + private array $permissionCache = []; + + public function has(Organization $organization, string $permission): bool + { + /** @var User|null $user */ + $user = Auth::user(); + if ($user === null) { + return false; + } + + if (! isset($this->permissionCache[$user->getKey().'|'.$organization->getKey()])) { + if ($user->ownsTeam($organization)) { + return true; + } + + if (! $user->belongsToTeam($organization)) { + return false; + } + + $permissions = $user->teamPermissions($organization); + $this->permissionCache[$user->getKey().'|'.$organization->getKey()] = $permissions; + } else { + $permissions = $this->permissionCache[$user->getKey().'|'.$organization->getKey()]; + } + + return in_array($permission, $permissions, true); + } +} diff --git a/app/Service/TimezoneService.php b/app/Service/TimezoneService.php index 26267e67..6714c775 100644 --- a/app/Service/TimezoneService.php +++ b/app/Service/TimezoneService.php @@ -5,9 +5,8 @@ namespace App\Service; use App\Models\User; +use Carbon\Carbon; use Carbon\CarbonTimeZone; -use DateTime; -use DateTimeZone; use Illuminate\Support\Facades\Log; class TimezoneService @@ -17,9 +16,7 @@ class TimezoneService */ public function getTimezones(): array { - $tzlist = CarbonTimeZone::listIdentifiers(DateTimeZone::ALL); - - return $tzlist; + return CarbonTimeZone::listIdentifiers(); } public function getTimezoneFromUser(User $user): CarbonTimeZone @@ -57,8 +54,6 @@ public function isValid(string $timezone): bool public function getShiftFromUtc(CarbonTimeZone $timeZone): int { - $timezoneShift = $timeZone->getOffset(new DateTime('now', new DateTimeZone('UTC'))); - - return $timezoneShift; + return $timeZone->getOffset(Carbon::now()); } } diff --git a/composer.json b/composer.json index 05df6f08..026bbb15 100644 --- a/composer.json +++ b/composer.json @@ -8,10 +8,12 @@ "require": { "php": "8.3.*", "ext-zip": "*", + "brick/money": "^0.8.1", "dedoc/scramble": "dev-main", "filament/filament": "^3.2", "guzzlehttp/guzzle": "^7.2", "inertiajs/inertia-laravel": "^1.0", + "korridor/laravel-computed-attributes": "^3.1", "korridor/laravel-model-validation-rules": "^3.0", "laravel/framework": "^11.0", "laravel/jetstream": "^5.0", @@ -104,7 +106,8 @@ "sort-packages": true, "allow-plugins": { "pestphp/pest-plugin": true, - "php-http/discovery": true + "php-http/discovery": true, + "wikimedia/composer-merge-plugin": true } }, "minimum-stability": "stable", diff --git a/composer.lock b/composer.lock index b764576c..a9e01ab3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5fba69cdc5c5883dc10412fb2f747740", + "content-hash": "a3df47d5f26d9c144ca78def159cb562", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -331,35 +331,647 @@ ], "time": "2023-01-15T23:15:59+00:00" }, + { + "name": "brick/money", + "version": "0.8.1", + "source": { + "type": "git", + "url": "https://github.com/brick/money.git", + "reference": "25f484a347756b7f3fbe7ad63ed9ad2d87b20004" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/money/zipball/25f484a347756b7f3fbe7ad63ed9ad2d87b20004", + "reference": "25f484a347756b7f3fbe7ad63ed9ad2d87b20004", + "shasum": "" + }, + "require": { + "brick/math": "~0.10.1 || ~0.11.0", + "ext-json": "*", + "php": "^8.0" + }, + "require-dev": { + "brick/varexporter": "~0.3.0", + "ext-dom": "*", + "ext-pdo": "*", + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^9.4.3", + "vimeo/psalm": "5.14.1" + }, + "suggest": { + "ext-intl": "Required to format Money objects" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Money\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Money and currency library", + "keywords": [ + "brick", + "currency", + "money" + ], + "support": { + "issues": "https://github.com/brick/money/issues", + "source": "https://github.com/brick/money/tree/0.8.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-09-23T21:17:11+00:00" + }, { "name": "carbonphp/carbon-doctrine-types", "version": "2.1.0", "source": { "type": "git", - "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", - "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" + }, + "require-dev": { + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2023-12-11T17:09:12+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "psr/log": "^1.0", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-03-15T14:00:32+00:00" + }, + { + "name": "composer/class-map-generator", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/composer/class-map-generator.git", + "reference": "8286a62d243312ed99b3eee20d5005c961adb311" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8286a62d243312ed99b3eee20d5005c961adb311", + "reference": "8286a62d243312ed99b3eee20d5005c961adb311", + "shasum": "" + }, + "require": { + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.6", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/filesystem": "^5.4 || ^6", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\ClassMapGenerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Utilities to scan PHP code and generate class maps.", + "keywords": [ + "classmap" + ], + "support": { + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.1.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-03-15T12:53:41+00:00" + }, + { + "name": "composer/composer", + "version": "2.7.2", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "b826edb791571ab1eaf281eb1bd6e181a1192adc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/b826edb791571ab1eaf281eb1bd6e181a1192adc", + "reference": "b826edb791571ab1eaf281eb1bd6e181a1192adc", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "composer/class-map-generator": "^1.0", + "composer/metadata-minifier": "^1.0", + "composer/pcre": "^2.1 || ^3.1", + "composer/semver": "^3.2.5", + "composer/spdx-licenses": "^1.5.7", + "composer/xdebug-handler": "^2.0.2 || ^3.0.3", + "justinrainbow/json-schema": "^5.2.11", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "react/promise": "^2.8 || ^3", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.2", + "seld/signal-handler": "^2.0", + "symfony/console": "^5.4.11 || ^6.0.11 || ^7", + "symfony/filesystem": "^5.4 || ^6.0 || ^7", + "symfony/finder": "^5.4 || ^6.0 || ^7", + "symfony/polyfill-php73": "^1.24", + "symfony/polyfill-php80": "^1.24", + "symfony/polyfill-php81": "^1.24", + "symfony/process": "^5.4 || ^6.0 || ^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.9.3", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1", + "phpstan/phpstan-symfony": "^1.2.10", + "symfony/phpunit-bridge": "^6.4.1 || ^7.0.1" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.7-dev" + }, + "phpstan": { + "includes": [ + "phpstan/rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "https://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/composer/issues", + "security": "https://github.com/composer/composer/security/policy", + "source": "https://github.com/composer/composer/tree/2.7.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-03-11T16:12:18+00:00" + }, + { + "name": "composer/metadata-minifier", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/metadata-minifier.git", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2", + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\MetadataMinifier\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Small utility library that handles metadata minification and expansion.", + "keywords": [ + "composer", + "compression" + ], + "support": { + "issues": "https://github.com/composer/metadata-minifier/issues", + "source": "https://github.com/composer/metadata-minifier/tree/1.0.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-04-07T13:37:33+00:00" + }, + { + "name": "composer/pcre", + "version": "3.1.3", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-03-19T10:26:25+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", - "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, - "conflict": { - "doctrine/dbal": "<3.7.0 || >=4.0.0" + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-08-31T09:50:34+00:00" + }, + { + "name": "composer/spdx-licenses", + "version": "1.5.8", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", + "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "doctrine/dbal": "^3.7.0", - "nesbot/carbon": "^2.71.0 || ^3.0.0", - "phpunit/phpunit": "^10.3" + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, "autoload": { "psr-4": { - "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + "Composer\\Spdx\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -368,37 +980,113 @@ ], "authors": [ { - "name": "KyleKatarn", - "email": "kylekatarnls@gmail.com" + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" } ], - "description": "Types to use Carbon in Doctrine", + "description": "SPDX licenses list and validation library.", "keywords": [ - "carbon", - "date", - "datetime", - "doctrine", - "time" + "license", + "spdx", + "validator" ], "support": { - "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", - "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.5.8" }, "funding": [ { - "url": "https://github.com/kylekatarnls", + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", "type": "github" }, { - "url": "https://opencollective.com/Carbon", - "type": "open_collective" + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-11-20T07:44:33+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" }, { - "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2023-12-11T17:09:12+00:00" + "time": "2024-03-26T18:29:49+00:00" }, { "name": "danharrin/date-format-converter", @@ -1431,16 +2119,16 @@ }, { "name": "filament/actions", - "version": "v3.2.60", + "version": "v3.2.61", "source": { "type": "git", "url": "https://github.com/filamentphp/actions.git", - "reference": "8523fabf8d48301d80023d3955bc2d21e52b1e2f" + "reference": "95c8842023399cdf2508a86c2af3db579b125cc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/actions/zipball/8523fabf8d48301d80023d3955bc2d21e52b1e2f", - "reference": "8523fabf8d48301d80023d3955bc2d21e52b1e2f", + "url": "https://api.github.com/repos/filamentphp/actions/zipball/95c8842023399cdf2508a86c2af3db579b125cc7", + "reference": "95c8842023399cdf2508a86c2af3db579b125cc7", "shasum": "" }, "require": { @@ -1480,20 +2168,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-03-21T22:43:16+00:00" + "time": "2024-03-27T12:36:15+00:00" }, { "name": "filament/filament", - "version": "v3.2.60", + "version": "v3.2.61", "source": { "type": "git", "url": "https://github.com/filamentphp/panels.git", - "reference": "08673dba066dcb54cf9019596ffea3d79997496c" + "reference": "6aa99147005785e0528e9eca77ee029c7c57d620" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/panels/zipball/08673dba066dcb54cf9019596ffea3d79997496c", - "reference": "08673dba066dcb54cf9019596ffea3d79997496c", + "url": "https://api.github.com/repos/filamentphp/panels/zipball/6aa99147005785e0528e9eca77ee029c7c57d620", + "reference": "6aa99147005785e0528e9eca77ee029c7c57d620", "shasum": "" }, "require": { @@ -1545,20 +2233,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-03-23T20:58:51+00:00" + "time": "2024-03-27T12:36:01+00:00" }, { "name": "filament/forms", - "version": "v3.2.60", + "version": "v3.2.61", "source": { "type": "git", "url": "https://github.com/filamentphp/forms.git", - "reference": "c864c8ac34e1372964d2d4e9595d10ddaabf88c8" + "reference": "1237e83354fd28a1a4cf5d6585f57adc56954e6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/forms/zipball/c864c8ac34e1372964d2d4e9595d10ddaabf88c8", - "reference": "c864c8ac34e1372964d2d4e9595d10ddaabf88c8", + "url": "https://api.github.com/repos/filamentphp/forms/zipball/1237e83354fd28a1a4cf5d6585f57adc56954e6c", + "reference": "1237e83354fd28a1a4cf5d6585f57adc56954e6c", "shasum": "" }, "require": { @@ -1601,20 +2289,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-03-23T23:27:26+00:00" + "time": "2024-03-27T12:35:57+00:00" }, { "name": "filament/infolists", - "version": "v3.2.60", + "version": "v3.2.61", "source": { "type": "git", "url": "https://github.com/filamentphp/infolists.git", - "reference": "64d1ed73e05eebb688e343348e126a038169c7d0" + "reference": "aa2f266ce113b13cf24e8752970bed9d2fb62b4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/infolists/zipball/64d1ed73e05eebb688e343348e126a038169c7d0", - "reference": "64d1ed73e05eebb688e343348e126a038169c7d0", + "url": "https://api.github.com/repos/filamentphp/infolists/zipball/aa2f266ce113b13cf24e8752970bed9d2fb62b4f", + "reference": "aa2f266ce113b13cf24e8752970bed9d2fb62b4f", "shasum": "" }, "require": { @@ -1652,20 +2340,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-03-23T23:27:28+00:00" + "time": "2024-03-27T12:36:20+00:00" }, { "name": "filament/notifications", - "version": "v3.2.60", + "version": "v3.2.61", "source": { "type": "git", "url": "https://github.com/filamentphp/notifications.git", - "reference": "f9a790ee2d5103ea87c88e6d545ea6ceb770cbe5" + "reference": "069a37c9f260918741fd80167859d7578ea18887" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/notifications/zipball/f9a790ee2d5103ea87c88e6d545ea6ceb770cbe5", - "reference": "f9a790ee2d5103ea87c88e6d545ea6ceb770cbe5", + "url": "https://api.github.com/repos/filamentphp/notifications/zipball/069a37c9f260918741fd80167859d7578ea18887", + "reference": "069a37c9f260918741fd80167859d7578ea18887", "shasum": "" }, "require": { @@ -1704,20 +2392,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-03-19T00:28:12+00:00" + "time": "2024-03-27T12:36:01+00:00" }, { "name": "filament/support", - "version": "v3.2.60", + "version": "v3.2.61", "source": { "type": "git", "url": "https://github.com/filamentphp/support.git", - "reference": "05c0c56bdb66226dc8d239ac91bc973a0dd33edb" + "reference": "6f7f6fe72f8c206bd28297fbf41f863358c95c07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/support/zipball/05c0c56bdb66226dc8d239ac91bc973a0dd33edb", - "reference": "05c0c56bdb66226dc8d239ac91bc973a0dd33edb", + "url": "https://api.github.com/repos/filamentphp/support/zipball/6f7f6fe72f8c206bd28297fbf41f863358c95c07", + "reference": "6f7f6fe72f8c206bd28297fbf41f863358c95c07", "shasum": "" }, "require": { @@ -1761,20 +2449,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-03-23T20:59:06+00:00" + "time": "2024-03-27T12:36:25+00:00" }, { "name": "filament/tables", - "version": "v3.2.60", + "version": "v3.2.61", "source": { "type": "git", "url": "https://github.com/filamentphp/tables.git", - "reference": "b58dbfd4b3f4da6708e67a4ca0512bc914faa991" + "reference": "38e01c5d5e841d36365d2acf581ed60095b0f768" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/tables/zipball/b58dbfd4b3f4da6708e67a4ca0512bc914faa991", - "reference": "b58dbfd4b3f4da6708e67a4ca0512bc914faa991", + "url": "https://api.github.com/repos/filamentphp/tables/zipball/38e01c5d5e841d36365d2acf581ed60095b0f768", + "reference": "38e01c5d5e841d36365d2acf581ed60095b0f768", "shasum": "" }, "require": { @@ -1814,11 +2502,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-03-23T20:59:09+00:00" + "time": "2024-03-27T12:36:41+00:00" }, { "name": "filament/widgets", - "version": "v3.2.60", + "version": "v3.2.61", "source": { "type": "git", "url": "https://github.com/filamentphp/widgets.git", @@ -2542,41 +3230,177 @@ ], "time": "2024-03-09T00:30:58+00:00" }, + { + "name": "justinrainbow/json-schema", + "version": "v5.2.13", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13" + }, + "time": "2023-09-26T02:20:38+00:00" + }, { "name": "kirschbaum-development/eloquent-power-joins", "version": "3.5.5", "source": { "type": "git", - "url": "https://github.com/kirschbaum-development/eloquent-power-joins.git", - "reference": "ed5af4788f0dad3ce35bb883cecac4293496139a" + "url": "https://github.com/kirschbaum-development/eloquent-power-joins.git", + "reference": "ed5af4788f0dad3ce35bb883cecac4293496139a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/ed5af4788f0dad3ce35bb883cecac4293496139a", + "reference": "ed5af4788f0dad3ce35bb883cecac4293496139a", + "shasum": "" + }, + "require": { + "illuminate/database": "^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0", + "php": "^8.0" + }, + "require-dev": { + "laravel/legacy-factories": "^1.0@dev", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", + "phpunit/phpunit": "^8.0|^9.0|^10.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Kirschbaum\\PowerJoins\\PowerJoinsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Kirschbaum\\PowerJoins\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luis Dalmolin", + "email": "luis.nh@gmail.com", + "role": "Developer" + } + ], + "description": "The Laravel magic applied to joins.", + "homepage": "https://github.com/kirschbaum-development/eloquent-power-joins", + "keywords": [ + "eloquent", + "join", + "laravel", + "mysql" + ], + "support": { + "issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues", + "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/3.5.5" + }, + "time": "2024-03-27T11:14:14+00:00" + }, + { + "name": "korridor/laravel-computed-attributes", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/korridor/laravel-computed-attributes.git", + "reference": "8229bbccfede7d17a85e86b0384ac9dc231975b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/ed5af4788f0dad3ce35bb883cecac4293496139a", - "reference": "ed5af4788f0dad3ce35bb883cecac4293496139a", + "url": "https://api.github.com/repos/korridor/laravel-computed-attributes/zipball/8229bbccfede7d17a85e86b0384ac9dc231975b9", + "reference": "8229bbccfede7d17a85e86b0384ac9dc231975b9", "shasum": "" }, "require": { - "illuminate/database": "^8.0|^9.0|^10.0|^11.0", - "illuminate/support": "^8.0|^9.0|^10.0|^11.0", - "php": "^8.0" + "composer/composer": "^2", + "illuminate/console": "^10|^11", + "illuminate/database": "^10|^11", + "illuminate/support": "^10|^11", + "php": ">=8.1" }, "require-dev": { - "laravel/legacy-factories": "^1.0@dev", - "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", - "phpunit/phpunit": "^8.0|^9.0|^10.0" + "friendsofphp/php-cs-fixer": "^3", + "larastan/larastan": "^2.9", + "orchestra/testbench": "^8|^9", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.5" }, "type": "library", "extra": { "laravel": { "providers": [ - "Kirschbaum\\PowerJoins\\PowerJoinsServiceProvider" + "Korridor\\LaravelComputedAttributes\\LaravelComputedAttributesServiceProvider" ] } }, "autoload": { "psr-4": { - "Kirschbaum\\PowerJoins\\": "src" + "Korridor\\LaravelComputedAttributes\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2585,24 +3409,27 @@ ], "authors": [ { - "name": "Luis Dalmolin", - "email": "luis.nh@gmail.com", - "role": "Developer" + "name": "korridor", + "email": "26689068+korridor@users.noreply.github.com" } ], - "description": "The Laravel magic applied to joins.", - "homepage": "https://github.com/kirschbaum-development/eloquent-power-joins", + "description": "Laravel package that adds computed attributes to eloquent models. A computed attribute is an accessor were the computed value is saved in the database.", + "homepage": "https://github.com/korridor/laravel-computed-attributes", "keywords": [ + "accessor", + "attribute", + "caching", + "computed", "eloquent", - "join", "laravel", - "mysql" + "model", + "performance" ], "support": { - "issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues", - "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/3.5.5" + "issues": "https://github.com/korridor/laravel-computed-attributes/issues", + "source": "https://github.com/korridor/laravel-computed-attributes/tree/3.1.0" }, - "time": "2024-03-27T11:14:14+00:00" + "time": "2024-03-01T14:15:47+00:00" }, { "name": "korridor/laravel-model-validation-rules", @@ -4508,16 +5335,16 @@ }, { "name": "nesbot/carbon", - "version": "3.1.1", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "34ccf6f6b49c915421c7886c88c0cb77f3ebbfd2" + "reference": "b4272c2e78d30f9085b079aedb692b2da879b313" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/34ccf6f6b49c915421c7886c88c0cb77f3ebbfd2", - "reference": "34ccf6f6b49c915421c7886c88c0cb77f3ebbfd2", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/b4272c2e78d30f9085b079aedb692b2da879b313", + "reference": "b4272c2e78d30f9085b079aedb692b2da879b313", "shasum": "" }, "require": { @@ -4535,14 +5362,14 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.18.0", - "kylekatarnls/multi-tester": "^2.2.0", - "ondrejmirtes/better-reflection": "^6.11.0.0", - "phpmd/phpmd": "^2.13.0", - "phpstan/extension-installer": "^1.3.0", - "phpstan/phpstan": "^1.10.20", - "phpunit/phpunit": "^10.2.2", - "squizlabs/php_codesniffer": "^3.7.2" + "friendsofphp/php-cs-fixer": "^3.52.1", + "kylekatarnls/multi-tester": "^2.5.3", + "ondrejmirtes/better-reflection": "^6.25.0.4", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.10.65", + "phpunit/phpunit": "^10.5.15", + "squizlabs/php_codesniffer": "^3.9.0" }, "bin": [ "bin/carbon" @@ -4610,7 +5437,7 @@ "type": "tidelift" } ], - "time": "2024-03-13T12:42:37+00:00" + "time": "2024-03-27T21:37:24+00:00" }, { "name": "nette/schema", @@ -6285,72 +7112,323 @@ ], "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", "keywords": [ - "guid", - "identifier", - "uuid" + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.5" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2023-11-08T05:53:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.1.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-16T16:21:57+00:00" + }, + { + "name": "ryangjchandler/blade-capture-directive", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/ryangjchandler/blade-capture-directive.git", + "reference": "cb6f58663d97f17bece176295240b740835e14f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ryangjchandler/blade-capture-directive/zipball/cb6f58663d97f17bece176295240b740835e14f1", + "reference": "cb6f58663d97f17bece176295240b740835e14f1", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.9.2" + }, + "require-dev": { + "nunomaduro/collision": "^7.0|^8.0", + "nunomaduro/larastan": "^2.0", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.0", + "spatie/laravel-ray": "^1.26" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "RyanChandler\\BladeCaptureDirective\\BladeCaptureDirectiveServiceProvider" + ], + "aliases": { + "BladeCaptureDirective": "RyanChandler\\BladeCaptureDirective\\Facades\\BladeCaptureDirective" + } + } + }, + "autoload": { + "psr-4": { + "RyanChandler\\BladeCaptureDirective\\": "src", + "RyanChandler\\BladeCaptureDirective\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ryan Chandler", + "email": "support@ryangjchandler.co.uk", + "role": "Developer" + } + ], + "description": "Create inline partials in your Blade templates with ease.", + "homepage": "https://github.com/ryangjchandler/blade-capture-directive", + "keywords": [ + "blade-capture-directive", + "laravel", + "ryangjchandler" + ], + "support": { + "issues": "https://github.com/ryangjchandler/blade-capture-directive/issues", + "source": "https://github.com/ryangjchandler/blade-capture-directive/tree/v1.0.0" + }, + "funding": [ + { + "url": "https://github.com/ryangjchandler", + "type": "github" + } + ], + "time": "2024-02-26T18:08:49+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.10.2", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9bb7db07b5d66d90f6ebf542f09fc67d800e5259", + "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.10.2" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2024-02-07T12:57:50+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" ], "support": { - "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.5" + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", - "type": "tidelift" - } - ], - "time": "2023-11-08T05:53:05+00:00" + "time": "2022-08-31T10:31:18+00:00" }, { - "name": "ryangjchandler/blade-capture-directive", - "version": "v1.0.0", + "name": "seld/signal-handler", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/ryangjchandler/blade-capture-directive.git", - "reference": "cb6f58663d97f17bece176295240b740835e14f1" + "url": "https://github.com/Seldaek/signal-handler.git", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ryangjchandler/blade-capture-directive/zipball/cb6f58663d97f17bece176295240b740835e14f1", - "reference": "cb6f58663d97f17bece176295240b740835e14f1", + "url": "https://api.github.com/repos/Seldaek/signal-handler/zipball/04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", "shasum": "" }, "require": { - "illuminate/contracts": "^10.0|^11.0", - "php": "^8.1", - "spatie/laravel-package-tools": "^1.9.2" + "php": ">=7.2.0" }, "require-dev": { - "nunomaduro/collision": "^7.0|^8.0", - "nunomaduro/larastan": "^2.0", - "orchestra/testbench": "^8.0|^9.0", - "pestphp/pest": "^2.0", - "pestphp/pest-plugin-laravel": "^2.0", - "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1", "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.0", - "spatie/laravel-ray": "^1.26" + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^7.5.20 || ^8.5.23", + "psr/log": "^1 || ^2 || ^3" }, "type": "library", "extra": { - "laravel": { - "providers": [ - "RyanChandler\\BladeCaptureDirective\\BladeCaptureDirectiveServiceProvider" - ], - "aliases": { - "BladeCaptureDirective": "RyanChandler\\BladeCaptureDirective\\Facades\\BladeCaptureDirective" - } + "branch-alias": { + "dev-main": "2.x-dev" } }, "autoload": { "psr-4": { - "RyanChandler\\BladeCaptureDirective\\": "src", - "RyanChandler\\BladeCaptureDirective\\Database\\Factories\\": "database/factories" + "Seld\\Signal\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -6359,29 +7437,24 @@ ], "authors": [ { - "name": "Ryan Chandler", - "email": "support@ryangjchandler.co.uk", - "role": "Developer" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" } ], - "description": "Create inline partials in your Blade templates with ease.", - "homepage": "https://github.com/ryangjchandler/blade-capture-directive", + "description": "Simple unix signal handler that silently fails where signals are not supported for easy cross-platform development", "keywords": [ - "blade-capture-directive", - "laravel", - "ryangjchandler" + "posix", + "sigint", + "signal", + "sigterm", + "unix" ], "support": { - "issues": "https://github.com/ryangjchandler/blade-capture-directive/issues", - "source": "https://github.com/ryangjchandler/blade-capture-directive/tree/v1.0.0" + "issues": "https://github.com/Seldaek/signal-handler/issues", + "source": "https://github.com/Seldaek/signal-handler/tree/2.0.2" }, - "funding": [ - { - "url": "https://github.com/ryangjchandler", - "type": "github" - } - ], - "time": "2024-02-26T18:08:49+00:00" + "time": "2023-09-03T09:24:00+00:00" }, { "name": "spatie/color", @@ -7215,6 +8288,69 @@ ], "time": "2023-05-23T14:45:45+00:00" }, + { + "name": "symfony/filesystem", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2890e3a825bc0c0558526c04499c13f83e1b6b12", + "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T15:02:46+00:00" + }, { "name": "symfony/finder", "version": "v7.0.0", @@ -8175,6 +9311,82 @@ ], "time": "2024-01-29T20:11:03+00:00" }, + { + "name": "symfony/polyfill-php73", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, { "name": "symfony/polyfill-php80", "version": "v1.29.0", @@ -8255,6 +9467,82 @@ ], "time": "2024-01-29T20:11:03+00:00" }, + { + "name": "symfony/polyfill-php81", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d", + "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, { "name": "symfony/polyfill-php83", "version": "v1.29.0", @@ -11130,16 +12418,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.0.8", + "version": "11.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "48ea58408879a9aad630022186398364051482fc" + "reference": "591bbfe416400385527d5086b346b92c06de404b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/48ea58408879a9aad630022186398364051482fc", - "reference": "48ea58408879a9aad630022186398364051482fc", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/591bbfe416400385527d5086b346b92c06de404b", + "reference": "591bbfe416400385527d5086b346b92c06de404b", "shasum": "" }, "require": { @@ -11210,7 +12498,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.0.8" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.0.9" }, "funding": [ { @@ -11226,7 +12514,7 @@ "type": "tidelift" } ], - "time": "2024-03-22T04:21:01+00:00" + "time": "2024-03-28T10:09:42+00:00" }, { "name": "sebastian/cli-parser", diff --git a/database/factories/ClientFactory.php b/database/factories/ClientFactory.php index 814cfdc8..605c2e31 100644 --- a/database/factories/ClientFactory.php +++ b/database/factories/ClientFactory.php @@ -34,4 +34,13 @@ public function forOrganization(Organization $organization): self ]; }); } + + public function randomCreatedAt(): self + { + return $this->state(function (array $attributes): array { + return [ + 'created_at' => $this->faker->dateTimeBetween('-1 day', 'now'), + ]; + }); + } } diff --git a/database/factories/OrganizationFactory.php b/database/factories/OrganizationFactory.php index 9124b4ba..f83ef0cf 100644 --- a/database/factories/OrganizationFactory.php +++ b/database/factories/OrganizationFactory.php @@ -22,6 +22,8 @@ public function definition(): array { return [ 'name' => $this->faker->unique()->company(), + 'currency' => $this->faker->currencyCode, + 'billable_rate' => $this->faker->numberBetween(50, 1000) * 100, 'user_id' => User::factory(), 'personal_team' => true, ]; diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php index f8654731..0a05d150 100644 --- a/database/factories/ProjectFactory.php +++ b/database/factories/ProjectFactory.php @@ -7,6 +7,8 @@ use App\Models\Client; use App\Models\Organization; use App\Models\Project; +use App\Models\ProjectMember; +use App\Models\User; use App\Service\ColorService; use Illuminate\Database\Eloquent\Factories\Factory; @@ -25,8 +27,10 @@ public function definition(): array return [ 'name' => $this->faker->company(), 'color' => app(ColorService::class)->getRandomColor(), - 'organization_id' => Organization::factory(), + 'billable_rate' => $this->faker->numberBetween(50, 1000) * 100, + 'is_public' => false, 'client_id' => null, + 'organization_id' => Organization::factory(), ]; } @@ -39,6 +43,34 @@ public function forOrganization(Organization $organization): self }); } + public function isPublic(): self + { + return $this->state(function (array $attributes): array { + return [ + 'is_public' => true, + ]; + }); + } + + public function isPrivate(): self + { + return $this->state(function (array $attributes): array { + return [ + 'is_public' => false, + ]; + }); + } + + public function addMember(User $user, array $attributes = []): self + { + return $this->afterCreating(function (Project $project) use ($user, $attributes): void { + ProjectMember::factory() + ->forProject($project) + ->forUser($user) + ->create($attributes); + }); + } + public function withClient(): self { return $this->state(function (array $attributes): array { diff --git a/database/factories/ProjectMemberFactory.php b/database/factories/ProjectMemberFactory.php new file mode 100644 index 00000000..a586fee7 --- /dev/null +++ b/database/factories/ProjectMemberFactory.php @@ -0,0 +1,48 @@ + + */ +class ProjectMemberFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'billable_rate' => $this->faker->numberBetween(50, 1000) * 100, + 'project_id' => Project::factory(), + 'user_id' => User::factory(), + ]; + } + + public function forUser(User $user): self + { + return $this->state(function (array $attributes) use ($user): array { + return [ + 'user_id' => $user->getKey(), + ]; + }); + } + + public function forProject(Project $project): self + { + return $this->state(function (array $attributes) use ($project): array { + return [ + 'project_id' => $project->getKey(), + ]; + }); + } +} diff --git a/database/factories/TagFactory.php b/database/factories/TagFactory.php index 4d00a041..58c02336 100644 --- a/database/factories/TagFactory.php +++ b/database/factories/TagFactory.php @@ -34,4 +34,13 @@ public function forOrganization(Organization $organization): self ]; }); } + + public function randomCreatedAt(): self + { + return $this->state(function (array $attributes): array { + return [ + 'created_at' => $this->faker->dateTimeBetween('-1 day', 'now'), + ]; + }); + } } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index dd361c2e..1c879317 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -69,6 +69,13 @@ public function unverified(): static }); } + public function attachToOrganization(Organization $organization, array $pivot = []): static + { + return $this->afterCreating(function (User $user) use ($organization, $pivot) { + $user->organizations()->attach($organization, $pivot); + }); + } + /** * Indicate that the user should have a personal team. */ diff --git a/database/migrations/2020_05_21_100000_create_organizations_table.php b/database/migrations/2020_05_21_100000_create_organizations_table.php index 7fbcb9c4..50008804 100644 --- a/database/migrations/2020_05_21_100000_create_organizations_table.php +++ b/database/migrations/2020_05_21_100000_create_organizations_table.php @@ -18,6 +18,8 @@ public function up(): void $table->foreignUuid('user_id')->index(); $table->string('name'); $table->boolean('personal_team'); + $table->integer('billable_rate')->unsigned()->nullable(); + $table->string('currency', 3); $table->timestamps(); }); } diff --git a/database/migrations/2020_05_21_200000_create_organization_user_table.php b/database/migrations/2020_05_21_200000_create_organization_user_table.php index 71e50b2d..30f47bc2 100644 --- a/database/migrations/2020_05_21_200000_create_organization_user_table.php +++ b/database/migrations/2020_05_21_200000_create_organization_user_table.php @@ -18,6 +18,7 @@ public function up(): void $table->foreignUuid('organization_id'); $table->foreignUuid('user_id'); $table->string('role')->nullable(); + $table->integer('billable_rate')->unsigned()->nullable(); $table->timestamps(); $table->unique(['organization_id', 'user_id']); diff --git a/database/migrations/2024_01_20_110439_create_projects_table.php b/database/migrations/2024_01_20_110439_create_projects_table.php index 8f5b32b4..f2ab8b1e 100644 --- a/database/migrations/2024_01_20_110439_create_projects_table.php +++ b/database/migrations/2024_01_20_110439_create_projects_table.php @@ -17,6 +17,8 @@ public function up(): void $table->uuid('id')->primary(); $table->string('name', 255); $table->string('color', 16); + $table->integer('billable_rate')->unsigned()->nullable(); + $table->boolean('is_public')->default(false); $table->uuid('client_id')->nullable(); $table->foreign('client_id') ->references('id') diff --git a/database/migrations/2024_01_20_110837_create_time_entries_table.php b/database/migrations/2024_01_20_110837_create_time_entries_table.php index 5fa5e9fb..e471038e 100644 --- a/database/migrations/2024_01_20_110837_create_time_entries_table.php +++ b/database/migrations/2024_01_20_110837_create_time_entries_table.php @@ -18,6 +18,7 @@ public function up(): void $table->string('description', 500); $table->dateTime('start'); $table->dateTime('end')->nullable(); + $table->integer('billable_rate')->unsigned()->nullable(); $table->boolean('billable')->default(false); $table->uuid('user_id'); $table->foreign('user_id') diff --git a/database/migrations/2024_03_26_171253_create_project_members_table.php b/database/migrations/2024_03_26_171253_create_project_members_table.php new file mode 100644 index 00000000..fe87a356 --- /dev/null +++ b/database/migrations/2024_03_26_171253_create_project_members_table.php @@ -0,0 +1,42 @@ +uuid('id')->primary(); + $table->integer('billable_rate')->unsigned()->nullable(); + $table->uuid('project_id'); + $table->foreign('project_id') + ->references('id') + ->on('projects') + ->onDelete('restrict') + ->onUpdate('cascade'); + $table->uuid('user_id'); + $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('restrict') + ->onUpdate('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('project_members'); + } +}; diff --git a/lang/en/exceptions.php b/lang/en/exceptions.php index 706574fb..d889cfd9 100644 --- a/lang/en/exceptions.php +++ b/lang/en/exceptions.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Exceptions\Api\TimeEntryCanNotBeRestartedApiException; use App\Exceptions\Api\TimeEntryStillRunningApiException; use App\Exceptions\Api\UserNotPlaceholderApiException; @@ -9,5 +10,6 @@ 'api' => [ TimeEntryStillRunningApiException::KEY => 'Time entry is still running', UserNotPlaceholderApiException::KEY => 'The given user is not a placeholder', + TimeEntryCanNotBeRestartedApiException::KEY => 'Time entry is already stopped and can not be restarted', ], ]; diff --git a/lang/en/validation.php b/lang/en/validation.php index a058ef4d..40766b93 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -199,6 +199,7 @@ */ 'color' => 'The :attribute field must be a valid color.', + 'currency' => 'The :attribute field must be a valid currency code (ISO 4217).', 'organization' => 'The :attribute does not exist.', 'task_belongs_to_project' => 'The :attribute is not part of the given project.', ]; diff --git a/resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue b/resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue index f021589b..09049e9d 100644 --- a/resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue +++ b/resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue @@ -16,6 +16,7 @@ const props = defineProps<{ const form = useForm({ name: props.team.name, + currency: props.team.currency, }); const updateTeamName = () => { @@ -69,6 +70,26 @@ const updateTeamName = () => { + + +
+ + + +