diff --git a/.env.example b/.env.example index c4d4a2cf..12871552 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ APP_NAME=Laravel APP_ENV=local -APP_KEY= +APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk= APP_DEBUG=true APP_URL=http://localhost diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml new file mode 100644 index 00000000..65ec88eb --- /dev/null +++ b/.github/workflows/pint.yml @@ -0,0 +1,11 @@ +name: PHP Linting +on: push +jobs: + pint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: "laravel-pint" + uses: aglipanci/laravel-pint-action@2.0.0 + with: + configPath: "pint.json" diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 86452d17..292463ef 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -4,7 +4,7 @@ namespace App\Actions\Fortify; -use App\Models\Team; +use App\Models\Organization; use App\Models\User; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; @@ -46,9 +46,9 @@ public function create(array $input): User */ protected function createTeam(User $user): void { - $user->ownedTeams()->save(Team::forceCreate([ + $user->ownedTeams()->save(Organization::forceCreate([ 'user_id' => $user->id, - 'name' => explode(' ', $user->name, 2)[0]."'s Team", + 'name' => explode(' ', $user->name, 2)[0]."'s Organization", 'personal_team' => true, ])); } diff --git a/app/Actions/Jetstream/AddTeamMember.php b/app/Actions/Jetstream/AddOrganizationMember.php similarity index 69% rename from app/Actions/Jetstream/AddTeamMember.php rename to app/Actions/Jetstream/AddOrganizationMember.php index 56bb5ca8..acee7c3f 100644 --- a/app/Actions/Jetstream/AddTeamMember.php +++ b/app/Actions/Jetstream/AddOrganizationMember.php @@ -4,7 +4,7 @@ namespace App\Actions\Jetstream; -use App\Models\Team; +use App\Models\Organization; use App\Models\User; use Closure; use Illuminate\Support\Facades\Gate; @@ -15,32 +15,32 @@ use Laravel\Jetstream\Jetstream; use Laravel\Jetstream\Rules\Role; -class AddTeamMember implements AddsTeamMembers +class AddOrganizationMember implements AddsTeamMembers { /** * Add a new team member to the given team. */ - public function add(User $user, Team $team, string $email, ?string $role = null): void + public function add(User $user, Organization $organization, string $email, ?string $role = null): void { - Gate::forUser($user)->authorize('addTeamMember', $team); + Gate::forUser($user)->authorize('addTeamMember', $organization); - $this->validate($team, $email, $role); + $this->validate($organization, $email, $role); $newTeamMember = Jetstream::findUserByEmailOrFail($email); - AddingTeamMember::dispatch($team, $newTeamMember); + AddingTeamMember::dispatch($organization, $newTeamMember); - $team->users()->attach( + $organization->users()->attach( $newTeamMember, ['role' => $role] ); - TeamMemberAdded::dispatch($team, $newTeamMember); + TeamMemberAdded::dispatch($organization, $newTeamMember); } /** * Validate the add member operation. */ - protected function validate(Team $team, string $email, ?string $role): void + protected function validate(Organization $organization, string $email, ?string $role): void { Validator::make([ 'email' => $email, @@ -48,7 +48,7 @@ protected function validate(Team $team, string $email, ?string $role): void ], $this->rules(), [ 'email.exists' => __('We were unable to find a registered user with this email address.'), ])->after( - $this->ensureUserIsNotAlreadyOnTeam($team, $email) + $this->ensureUserIsNotAlreadyOnTeam($organization, $email) )->validateWithBag('addTeamMember'); } @@ -70,7 +70,7 @@ protected function rules(): array /** * Ensure that the user is not already on the team. */ - protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure + protected function ensureUserIsNotAlreadyOnTeam(Organization $team, string $email): Closure { return function ($validator) use ($team, $email) { $validator->errors()->addIf( diff --git a/app/Actions/Jetstream/CreateTeam.php b/app/Actions/Jetstream/CreateOrganization.php similarity index 59% rename from app/Actions/Jetstream/CreateTeam.php rename to app/Actions/Jetstream/CreateOrganization.php index 57d9fb52..99ea48ff 100644 --- a/app/Actions/Jetstream/CreateTeam.php +++ b/app/Actions/Jetstream/CreateOrganization.php @@ -4,22 +4,27 @@ namespace App\Actions\Jetstream; -use App\Models\Team; +use App\Models\Organization; use App\Models\User; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; use Laravel\Jetstream\Contracts\CreatesTeams; use Laravel\Jetstream\Events\AddingTeam; use Laravel\Jetstream\Jetstream; -class CreateTeam implements CreatesTeams +class CreateOrganization implements CreatesTeams { /** * Validate and create a new team for the given user. * * @param array $input + * + * @throws AuthorizationException + * @throws ValidationException */ - public function create(User $user, array $input): Team + public function create(User $user, array $input): Organization { Gate::forUser($user)->authorize('create', Jetstream::newTeamModel()); @@ -29,11 +34,14 @@ public function create(User $user, array $input): Team AddingTeam::dispatch($user); - $user->switchTeam($team = $user->ownedTeams()->create([ + /** @var Organization $organization */ + $organization = $user->ownedTeams()->create([ 'name' => $input['name'], 'personal_team' => false, - ])); + ]); + + $user->switchTeam($organization); - return $team; + return $organization; } } diff --git a/app/Actions/Jetstream/DeleteTeam.php b/app/Actions/Jetstream/DeleteOrganization.php similarity index 60% rename from app/Actions/Jetstream/DeleteTeam.php rename to app/Actions/Jetstream/DeleteOrganization.php index dd206590..8682e49d 100644 --- a/app/Actions/Jetstream/DeleteTeam.php +++ b/app/Actions/Jetstream/DeleteOrganization.php @@ -4,15 +4,15 @@ namespace App\Actions\Jetstream; -use App\Models\Team; +use App\Models\Organization; use Laravel\Jetstream\Contracts\DeletesTeams; -class DeleteTeam implements DeletesTeams +class DeleteOrganization implements DeletesTeams { /** * Delete the given team. */ - public function delete(Team $team): void + public function delete(Organization $team): void { $team->purge(); } diff --git a/app/Actions/Jetstream/DeleteUser.php b/app/Actions/Jetstream/DeleteUser.php index ce38676e..0fcda600 100644 --- a/app/Actions/Jetstream/DeleteUser.php +++ b/app/Actions/Jetstream/DeleteUser.php @@ -4,7 +4,7 @@ namespace App\Actions\Jetstream; -use App\Models\Team; +use App\Models\Organization; use App\Models\User; use Illuminate\Support\Facades\DB; use Laravel\Jetstream\Contracts\DeletesTeams; @@ -47,7 +47,7 @@ protected function deleteTeams(User $user): void { $user->teams()->detach(); - $user->ownedTeams->each(function (Team $team) { + $user->ownedTeams->each(function (Organization $team) { $this->deletesTeams->delete($team); }); } diff --git a/app/Actions/Jetstream/InviteOrganizationMember.php b/app/Actions/Jetstream/InviteOrganizationMember.php new file mode 100644 index 00000000..e6aef0f4 --- /dev/null +++ b/app/Actions/Jetstream/InviteOrganizationMember.php @@ -0,0 +1,94 @@ +authorize('addTeamMember', $organization); + + $this->validate($organization, $email, $role); + + InvitingTeamMember::dispatch($organization, $email, $role); + + $invitation = $organization->teamInvitations()->create([ + 'email' => $email, + 'role' => $role, + ]); + + Mail::to($email)->send(new TeamInvitation($invitation)); + } + + /** + * Validate the invite member operation. + */ + protected function validate(Organization $organization, string $email, ?string $role): void + { + Validator::make([ + 'email' => $email, + 'role' => $role, + ], $this->rules($organization), [ + 'email.unique' => __('This user has already been invited to the team.'), + ])->after( + $this->ensureUserIsNotAlreadyOnTeam($organization, $email) + )->validateWithBag('addTeamMember'); + } + + /** + * Get the validation rules for inviting a team member. + * + * @return array + */ + protected function rules(Organization $organization): array + { + return array_filter([ + 'email' => [ + 'required', + 'email', + new UniqueEloquent(OrganizationInvitation::class, 'email', function (Builder $builder) use ($organization) { + /** @var Builder $builder */ + return $builder->whereBelongsTo($organization, 'organization'); + }), + ], + 'role' => Jetstream::hasRoles() + ? ['required', 'string', new Role] + : null, + ]); + } + + /** + * Ensure that the user is not already on the team. + */ + protected function ensureUserIsNotAlreadyOnTeam(Organization $organization, string $email): Closure + { + return function ($validator) use ($organization, $email) { + $validator->errors()->addIf( + $organization->hasUserWithEmail($email), + 'email', + __('This user already belongs to the team.') + ); + }; + } +} diff --git a/app/Actions/Jetstream/InviteTeamMember.php b/app/Actions/Jetstream/InviteTeamMember.php deleted file mode 100644 index 3c0dedcd..00000000 --- a/app/Actions/Jetstream/InviteTeamMember.php +++ /dev/null @@ -1,90 +0,0 @@ -authorize('addTeamMember', $team); - - $this->validate($team, $email, $role); - - InvitingTeamMember::dispatch($team, $email, $role); - - $invitation = $team->teamInvitations()->create([ - 'email' => $email, - 'role' => $role, - ]); - - Mail::to($email)->send(new TeamInvitation($invitation)); - } - - /** - * Validate the invite member operation. - */ - protected function validate(Team $team, string $email, ?string $role): void - { - Validator::make([ - 'email' => $email, - 'role' => $role, - ], $this->rules($team), [ - 'email.unique' => __('This user has already been invited to the team.'), - ])->after( - $this->ensureUserIsNotAlreadyOnTeam($team, $email) - )->validateWithBag('addTeamMember'); - } - - /** - * Get the validation rules for inviting a team member. - * - * @return array - */ - protected function rules(Team $team): array - { - return array_filter([ - 'email' => [ - 'required', 'email', - Rule::unique('team_invitations')->where(function (Builder $query) use ($team) { - $query->where('team_id', $team->id); - }), - ], - 'role' => Jetstream::hasRoles() - ? ['required', 'string', new Role] - : null, - ]); - } - - /** - * Ensure that the user is not already on the team. - */ - protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure - { - return function ($validator) use ($team, $email) { - $validator->errors()->addIf( - $team->hasUserWithEmail($email), - 'email', - __('This user already belongs to the team.') - ); - }; - } -} diff --git a/app/Actions/Jetstream/RemoveTeamMember.php b/app/Actions/Jetstream/RemoveOrganizationMember.php similarity index 57% rename from app/Actions/Jetstream/RemoveTeamMember.php rename to app/Actions/Jetstream/RemoveOrganizationMember.php index d9f06b9d..d03a094a 100644 --- a/app/Actions/Jetstream/RemoveTeamMember.php +++ b/app/Actions/Jetstream/RemoveOrganizationMember.php @@ -4,7 +4,7 @@ namespace App\Actions\Jetstream; -use App\Models\Team; +use App\Models\Organization; use App\Models\User; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Support\Facades\Gate; @@ -12,28 +12,28 @@ use Laravel\Jetstream\Contracts\RemovesTeamMembers; use Laravel\Jetstream\Events\TeamMemberRemoved; -class RemoveTeamMember implements RemovesTeamMembers +class RemoveOrganizationMember implements RemovesTeamMembers { /** * Remove the team member from the given team. */ - public function remove(User $user, Team $team, User $teamMember): void + public function remove(User $user, Organization $organization, User $teamMember): void { - $this->authorize($user, $team, $teamMember); + $this->authorize($user, $organization, $teamMember); - $this->ensureUserDoesNotOwnTeam($teamMember, $team); + $this->ensureUserDoesNotOwnTeam($teamMember, $organization); - $team->removeUser($teamMember); + $organization->removeUser($teamMember); - TeamMemberRemoved::dispatch($team, $teamMember); + TeamMemberRemoved::dispatch($organization, $teamMember); } /** * Authorize that the user can remove the team member. */ - protected function authorize(User $user, Team $team, User $teamMember): void + protected function authorize(User $user, Organization $organization, User $teamMember): void { - if (! Gate::forUser($user)->check('removeTeamMember', $team) && + if (! Gate::forUser($user)->check('removeTeamMember', $organization) && $user->id !== $teamMember->id) { throw new AuthorizationException; } @@ -42,9 +42,9 @@ protected function authorize(User $user, Team $team, User $teamMember): void /** * Ensure that the currently authenticated user does not own the team. */ - protected function ensureUserDoesNotOwnTeam(User $teamMember, Team $team): void + protected function ensureUserDoesNotOwnTeam(User $teamMember, Organization $organization): void { - if ($teamMember->id === $team->owner->id) { + if ($teamMember->id === $organization->owner->id) { throw ValidationException::withMessages([ 'team' => [__('You may not leave a team that you created.')], ])->errorBag('removeTeamMember'); diff --git a/app/Actions/Jetstream/UpdateTeamName.php b/app/Actions/Jetstream/UpdateOrganization.php similarity index 55% rename from app/Actions/Jetstream/UpdateTeamName.php rename to app/Actions/Jetstream/UpdateOrganization.php index 3de075b9..ff0e10da 100644 --- a/app/Actions/Jetstream/UpdateTeamName.php +++ b/app/Actions/Jetstream/UpdateOrganization.php @@ -4,28 +4,33 @@ namespace App\Actions\Jetstream; -use App\Models\Team; +use App\Models\Organization; use App\Models\User; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; use Laravel\Jetstream\Contracts\UpdatesTeamNames; -class UpdateTeamName implements UpdatesTeamNames +class UpdateOrganization implements UpdatesTeamNames { /** * Validate and update the given team's name. * * @param array $input + * + * @throws AuthorizationException + * @throws ValidationException */ - public function update(User $user, Team $team, array $input): void + public function update(User $user, Organization $organization, array $input): void { - Gate::forUser($user)->authorize('update', $team); + Gate::forUser($user)->authorize('update', $organization); Validator::make($input, [ 'name' => ['required', 'string', 'max:255'], ])->validateWithBag('updateTeamName'); - $team->forceFill([ + $organization->forceFill([ 'name' => $input['name'], ])->save(); } diff --git a/app/Models/Client.php b/app/Models/Client.php new file mode 100644 index 00000000..e2046218 --- /dev/null +++ b/app/Models/Client.php @@ -0,0 +1,44 @@ + + */ + protected $casts = [ + 'name' => 'string', + ]; + + /** + * @return BelongsTo + */ + public function organization(): BelongsTo + { + return $this->belongsTo(Organization::class, 'organization_id'); + } +} diff --git a/app/Models/Membership.php b/app/Models/Membership.php index f2ccc525..fa7c627d 100644 --- a/app/Models/Membership.php +++ b/app/Models/Membership.php @@ -7,14 +7,24 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids; use Laravel\Jetstream\Membership as JetstreamMembership; +/** + * @property string $id + * @property string $role + * @property string $organization_id + * @property string $user_id + * @property string $created_at + * @property string $updated_at + * @property-read Organization $organization + * @property-read User $user + */ class Membership extends JetstreamMembership { use HasUuids; /** - * Indicates if the IDs are auto-incrementing. + * The table associated with the pivot model. * - * @var bool + * @var string */ - public $incrementing = true; + protected $table = 'organization_user'; } diff --git a/app/Models/Team.php b/app/Models/Organization.php similarity index 85% rename from app/Models/Team.php rename to app/Models/Organization.php index f546bab8..24146da3 100644 --- a/app/Models/Team.php +++ b/app/Models/Organization.php @@ -4,7 +4,7 @@ namespace App\Models; -use Database\Factories\TeamFactory; +use Database\Factories\OrganizationFactory; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -17,10 +17,10 @@ * @property string $id * @property User $owner * - * @method HasMany teamInvitations() - * @method static TeamFactory factory() + * @method HasMany teamInvitations() + * @method static OrganizationFactory factory() */ -class Team extends JetstreamTeam +class Organization extends JetstreamTeam { use HasFactory; use HasUuids; diff --git a/app/Models/OrganizationInvitation.php b/app/Models/OrganizationInvitation.php new file mode 100644 index 00000000..b47aea10 --- /dev/null +++ b/app/Models/OrganizationInvitation.php @@ -0,0 +1,52 @@ + + */ + protected $fillable = [ + 'email', + 'role', + ]; + + /** + * Get the organization that the invitation belongs to. + */ + public function organization(): BelongsTo + { + return $this->belongsTo(Jetstream::teamModel(), 'organization_id'); + } + + /** + * Get the organization that the invitation belongs to. + */ + public function team(): BelongsTo + { + return $this->belongsTo(Jetstream::teamModel(), 'organization_id'); + } +} diff --git a/app/Models/Project.php b/app/Models/Project.php new file mode 100644 index 00000000..4b6cee3e --- /dev/null +++ b/app/Models/Project.php @@ -0,0 +1,63 @@ + $tasks + * + * @method static ProjectFactory factory() + */ +class Project extends Model +{ + use HasFactory; + use HasUuids; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'name' => 'string', + ]; + + /** + * @return BelongsTo + */ + public function organization(): BelongsTo + { + return $this->belongsTo(Organization::class, 'organization_id'); + } + + /** + * @return BelongsTo + */ + public function client(): BelongsTo + { + return $this->belongsTo(Client::class, 'client_id'); + } + + /** + * @return HasMany + */ + public function tasks(): HasMany + { + return $this->hasMany(Task::class); + } +} diff --git a/app/Models/Tag.php b/app/Models/Tag.php new file mode 100644 index 00000000..9934f22d --- /dev/null +++ b/app/Models/Tag.php @@ -0,0 +1,42 @@ + + */ + protected $casts = [ + 'name' => 'string', + ]; + + /** + * @return BelongsTo + */ + public function organization(): BelongsTo + { + return $this->belongsTo(Organization::class, 'organization_id'); + } +} diff --git a/app/Models/Task.php b/app/Models/Task.php new file mode 100644 index 00000000..3909ed09 --- /dev/null +++ b/app/Models/Task.php @@ -0,0 +1,52 @@ + + */ + protected $casts = [ + 'name' => 'string', + ]; + + /** + * @return BelongsTo + */ + public function project(): BelongsTo + { + return $this->belongsTo(Project::class); + } + + /** + * @return BelongsTo + */ + public function organization(): BelongsTo + { + return $this->belongsTo(Organization::class, 'organization_id'); + } +} diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php deleted file mode 100644 index b21f1882..00000000 --- a/app/Models/TeamInvitation.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ - protected $fillable = [ - 'email', - 'role', - ]; - - /** - * Get the team that the invitation belongs to. - */ - public function team(): BelongsTo - { - return $this->belongsTo(Jetstream::teamModel()); - } -} diff --git a/app/Models/TimeEntry.php b/app/Models/TimeEntry.php new file mode 100644 index 00000000..ae9344b4 --- /dev/null +++ b/app/Models/TimeEntry.php @@ -0,0 +1,77 @@ + + */ + protected $casts = [ + 'description' => 'string', + 'start' => 'datetime', + 'end' => 'datetime', + 'billable' => 'bool', + 'tags' => 'array', + ]; + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } + + /** + * @return BelongsTo + */ + public function organization(): BelongsTo + { + return $this->belongsTo(Organization::class, 'organization_id'); + } + + /** + * @return BelongsTo + */ + public function project(): BelongsTo + { + return $this->belongsTo(Project::class, 'project_id'); + } + + /** + * @return BelongsTo + */ + public function task(): BelongsTo + { + return $this->belongsTo(Task::class, 'task_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 551bf126..b510c01c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -8,6 +8,7 @@ use Filament\Panel; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -20,7 +21,7 @@ * @property string $id * @property string $name * - * @method HasMany ownedTeams() + * @method HasMany ownedTeams() * @method static UserFactory factory() */ class User extends Authenticatable @@ -79,4 +80,17 @@ public function canAccessPanel(Panel $panel): bool // TODO: Implement canAccessPanel() method. return false; } + + /** + * @return BelongsToMany + */ + public function organizations(): BelongsToMany + { + return $this->belongsToMany(Organization::class, Membership::class) + ->withPivot([ + 'role', + ]) + ->withTimestamps() + ->as('membership'); + } } diff --git a/app/Policies/TeamPolicy.php b/app/Policies/OrganizationPolicy.php similarity index 55% rename from app/Policies/TeamPolicy.php rename to app/Policies/OrganizationPolicy.php index 824404f9..32fbb092 100644 --- a/app/Policies/TeamPolicy.php +++ b/app/Policies/OrganizationPolicy.php @@ -4,11 +4,11 @@ namespace App\Policies; -use App\Models\Team; +use App\Models\Organization; use App\Models\User; use Illuminate\Auth\Access\HandlesAuthorization; -class TeamPolicy +class OrganizationPolicy { use HandlesAuthorization; @@ -23,9 +23,9 @@ public function viewAny(User $user): bool /** * Determine whether the user can view the model. */ - public function view(User $user, Team $team): bool + public function view(User $user, Organization $organization): bool { - return $user->belongsToTeam($team); + return $user->belongsToTeam($organization); } /** @@ -39,40 +39,40 @@ public function create(User $user): bool /** * Determine whether the user can update the model. */ - public function update(User $user, Team $team): bool + public function update(User $user, Organization $organization): bool { - return $user->ownsTeam($team); + return $user->ownsTeam($organization); } /** * Determine whether the user can add team members. */ - public function addTeamMember(User $user, Team $team): bool + public function addTeamMember(User $user, Organization $organization): bool { - return $user->ownsTeam($team); + return $user->ownsTeam($organization); } /** * Determine whether the user can update team member permissions. */ - public function updateTeamMember(User $user, Team $team): bool + public function updateTeamMember(User $user, Organization $organization): bool { - return $user->ownsTeam($team); + return $user->ownsTeam($organization); } /** * Determine whether the user can remove team members. */ - public function removeTeamMember(User $user, Team $team): bool + public function removeTeamMember(User $user, Organization $organization): bool { - return $user->ownsTeam($team); + return $user->ownsTeam($organization); } /** * Determine whether the user can delete the model. */ - public function delete(User $user, Team $team): bool + public function delete(User $user, Organization $organization): bool { - return $user->ownsTeam($team); + return $user->ownsTeam($organization); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 2faa5ae5..f1cc0727 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,8 +5,8 @@ namespace App\Providers; use App\Models\Membership; -use App\Models\Team; -use App\Models\TeamInvitation; +use App\Models\Organization; +use App\Models\OrganizationInvitation; use App\Models\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; @@ -31,8 +31,8 @@ public function boot(): void Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction()); Relation::enforceMorphMap([ 'membership' => Membership::class, - 'team' => Team::class, - 'team_invitation' => TeamInvitation::class, + 'team' => Organization::class, + 'team_invitation' => OrganizationInvitation::class, 'user' => User::class, ]); } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 63ec34ea..e7a414c9 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -4,7 +4,8 @@ namespace App\Providers; -// use Illuminate\Support\Facades\Gate; +use App\Models\Organization; +use App\Policies\OrganizationPolicy; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Laravel\Jetstream\Jetstream; use Laravel\Passport\Passport; @@ -17,7 +18,7 @@ class AuthServiceProvider extends ServiceProvider * @var array */ protected $policies = [ - // + Organization::class => OrganizationPolicy::class, ]; /** diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index 99de7aa0..0f4742dc 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -4,13 +4,15 @@ namespace App\Providers; -use App\Actions\Jetstream\AddTeamMember; -use App\Actions\Jetstream\CreateTeam; -use App\Actions\Jetstream\DeleteTeam; +use App\Actions\Jetstream\AddOrganizationMember; +use App\Actions\Jetstream\CreateOrganization; +use App\Actions\Jetstream\DeleteOrganization; use App\Actions\Jetstream\DeleteUser; -use App\Actions\Jetstream\InviteTeamMember; -use App\Actions\Jetstream\RemoveTeamMember; -use App\Actions\Jetstream\UpdateTeamName; +use App\Actions\Jetstream\InviteOrganizationMember; +use App\Actions\Jetstream\RemoveOrganizationMember; +use App\Actions\Jetstream\UpdateOrganization; +use App\Models\Organization; +use App\Models\OrganizationInvitation; use Illuminate\Support\ServiceProvider; use Laravel\Jetstream\Jetstream; @@ -31,13 +33,15 @@ public function boot(): void { $this->configurePermissions(); - Jetstream::createTeamsUsing(CreateTeam::class); - Jetstream::updateTeamNamesUsing(UpdateTeamName::class); - Jetstream::addTeamMembersUsing(AddTeamMember::class); - Jetstream::inviteTeamMembersUsing(InviteTeamMember::class); - Jetstream::removeTeamMembersUsing(RemoveTeamMember::class); - Jetstream::deleteTeamsUsing(DeleteTeam::class); + Jetstream::createTeamsUsing(CreateOrganization::class); + Jetstream::updateTeamNamesUsing(UpdateOrganization::class); + Jetstream::addTeamMembersUsing(AddOrganizationMember::class); + Jetstream::inviteTeamMembersUsing(InviteOrganizationMember::class); + Jetstream::removeTeamMembersUsing(RemoveOrganizationMember::class); + Jetstream::deleteTeamsUsing(DeleteOrganization::class); Jetstream::deleteUsersUsing(DeleteUser::class); + Jetstream::useTeamModel(Organization::class); + Jetstream::useTeamInvitationModel(OrganizationInvitation::class); } /** diff --git a/composer.json b/composer.json index 8bc3d197..7c5b2894 100644 --- a/composer.json +++ b/composer.json @@ -9,13 +9,16 @@ "filament/filament": "^3.2", "guzzlehttp/guzzle": "^7.2", "inertiajs/inertia-laravel": "^0.6.8", + "korridor/laravel-model-validation-rules": "^3.0", "laravel/framework": "^10.10", "laravel/jetstream": "^4.2", "laravel/passport": "*", "laravel/tinker": "^2.8", - "tightenco/ziggy": "^1.0" + "tightenco/ziggy": "^1.0", + "tpetry/laravel-postgresql-enhanced": "^0.33.0" }, "require-dev": { + "brianium/paratest": "^7.3", "fakerphp/faker": "^1.9.1", "fumeapp/modeltyper": "^2.2", "larastan/larastan": "^2.0", @@ -58,6 +61,18 @@ ], "generate-typescript": [ "@php artisan model:typer > ./resources/js/types/models.ts" + ], + "ptest": [ + "@php artisan test --parallel --colors=always --stop-on-failure" + ], + "test": [ + "@php artisan test --colors=always --stop-on-failure" + ], + "test:coverage": [ + "@php artisan test --coverage --colors=always --stop-on-failure" + ], + "fix": [ + "@php pint" ] }, "extra": { diff --git a/composer.lock b/composer.lock index 25d206a9..56969805 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": "e68c6995fd0859775953a62c436b3461", + "content-hash": "ad3e9deab517156569b424191cdb7871", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -453,26 +453,26 @@ }, { "name": "danharrin/livewire-rate-limiting", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/danharrin/livewire-rate-limiting.git", - "reference": "bc2cc0a0b5b517fdc5bba8671013dd71081f70a8" + "reference": "bf16003f0d977b5a41071526d697eec94ac41735" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/danharrin/livewire-rate-limiting/zipball/bc2cc0a0b5b517fdc5bba8671013dd71081f70a8", - "reference": "bc2cc0a0b5b517fdc5bba8671013dd71081f70a8", + "url": "https://api.github.com/repos/danharrin/livewire-rate-limiting/zipball/bf16003f0d977b5a41071526d697eec94ac41735", + "reference": "bf16003f0d977b5a41071526d697eec94ac41735", "shasum": "" }, "require": { - "illuminate/support": "^9.0|^10.0", + "illuminate/support": "^9.0|^10.0|^11.0", "php": "^8.0" }, "require-dev": { "livewire/livewire": "^3.0", "livewire/volt": "^1.3", - "orchestra/testbench": "^7.0|^8.0", + "orchestra/testbench": "^7.0|^8.0|^9.0", "phpunit/phpunit": "^9.0|^10.0" }, "type": "library", @@ -503,7 +503,7 @@ "type": "github" } ], - "time": "2023-10-27T15:01:19+00:00" + "time": "2024-01-21T14:53:34+00:00" }, { "name": "dasprid/enum", @@ -792,16 +792,16 @@ }, { "name": "doctrine/dbal", - "version": "3.7.2", + "version": "3.7.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "0ac3c270590e54910715e9a1a044cc368df282b2" + "reference": "ce594cbc39a4866c544f1a970d285ff0548221ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/0ac3c270590e54910715e9a1a044cc368df282b2", - "reference": "0ac3c270590e54910715e9a1a044cc368df282b2", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/ce594cbc39a4866c544f1a970d285ff0548221ad", + "reference": "ce594cbc39a4866c544f1a970d285ff0548221ad", "shasum": "" }, "require": { @@ -817,14 +817,14 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.10.42", + "phpstan/phpstan": "1.10.56", "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.13", + "phpunit/phpunit": "9.6.15", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.7.2", - "symfony/cache": "^5.4|^6.0", - "symfony/console": "^4.4|^5.4|^6.0", + "squizlabs/php_codesniffer": "3.8.1", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" }, "suggest": { @@ -885,7 +885,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.7.2" + "source": "https://github.com/doctrine/dbal/tree/3.7.3" }, "funding": [ { @@ -901,7 +901,7 @@ "type": "tidelift" } ], - "time": "2023-11-19T08:06:58+00:00" + "time": "2024-01-21T07:53:09+00:00" }, { "name": "doctrine/deprecations", @@ -1339,16 +1339,16 @@ }, { "name": "filament/actions", - "version": "v3.2.7", + "version": "v3.2.9", "source": { "type": "git", "url": "https://github.com/filamentphp/actions.git", - "reference": "34d2093e4100182e0b9435ef47635a68ec93c660" + "reference": "465ef83a1c43b3b7fe122dde50eda2ee0f9138ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/actions/zipball/34d2093e4100182e0b9435ef47635a68ec93c660", - "reference": "34d2093e4100182e0b9435ef47635a68ec93c660", + "url": "https://api.github.com/repos/filamentphp/actions/zipball/465ef83a1c43b3b7fe122dde50eda2ee0f9138ea", + "reference": "465ef83a1c43b3b7fe122dde50eda2ee0f9138ea", "shasum": "" }, "require": { @@ -1388,20 +1388,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T00:40:00+00:00" + "time": "2024-01-21T14:44:52+00:00" }, { "name": "filament/filament", - "version": "v3.2.7", + "version": "v3.2.9", "source": { "type": "git", "url": "https://github.com/filamentphp/panels.git", - "reference": "a25a54c82e6e1028b330e3ea6f6e41afe5b3a3d1" + "reference": "a830f2d38073d3a4cdbe3798c957b69be50d39c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/panels/zipball/a25a54c82e6e1028b330e3ea6f6e41afe5b3a3d1", - "reference": "a25a54c82e6e1028b330e3ea6f6e41afe5b3a3d1", + "url": "https://api.github.com/repos/filamentphp/panels/zipball/a830f2d38073d3a4cdbe3798c957b69be50d39c3", + "reference": "a830f2d38073d3a4cdbe3798c957b69be50d39c3", "shasum": "" }, "require": { @@ -1453,20 +1453,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T00:40:03+00:00" + "time": "2024-01-21T14:44:57+00:00" }, { "name": "filament/forms", - "version": "v3.2.7", + "version": "v3.2.9", "source": { "type": "git", "url": "https://github.com/filamentphp/forms.git", - "reference": "90ccb87d307958254573a3a2e4a4c0b91dc0b64c" + "reference": "fc37c620f66a2e13e160b516cc4d0e5ad8ae9425" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/forms/zipball/90ccb87d307958254573a3a2e4a4c0b91dc0b64c", - "reference": "90ccb87d307958254573a3a2e4a4c0b91dc0b64c", + "url": "https://api.github.com/repos/filamentphp/forms/zipball/fc37c620f66a2e13e160b516cc4d0e5ad8ae9425", + "reference": "fc37c620f66a2e13e160b516cc4d0e5ad8ae9425", "shasum": "" }, "require": { @@ -1509,20 +1509,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T00:40:03+00:00" + "time": "2024-01-21T14:44:57+00:00" }, { "name": "filament/infolists", - "version": "v3.2.7", + "version": "v3.2.9", "source": { "type": "git", "url": "https://github.com/filamentphp/infolists.git", - "reference": "0eb661c396fac32e8ff681069b6c7ff9e7e2681c" + "reference": "b071063c45f0cd314c863947d1d841da09d40750" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/infolists/zipball/0eb661c396fac32e8ff681069b6c7ff9e7e2681c", - "reference": "0eb661c396fac32e8ff681069b6c7ff9e7e2681c", + "url": "https://api.github.com/repos/filamentphp/infolists/zipball/b071063c45f0cd314c863947d1d841da09d40750", + "reference": "b071063c45f0cd314c863947d1d841da09d40750", "shasum": "" }, "require": { @@ -1560,11 +1560,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T00:40:02+00:00" + "time": "2024-01-21T14:44:53+00:00" }, { "name": "filament/notifications", - "version": "v3.2.7", + "version": "v3.2.9", "source": { "type": "git", "url": "https://github.com/filamentphp/notifications.git", @@ -1616,16 +1616,16 @@ }, { "name": "filament/support", - "version": "v3.2.7", + "version": "v3.2.9", "source": { "type": "git", "url": "https://github.com/filamentphp/support.git", - "reference": "37663c414646ea7435121a3fef3d5d2d4f45a81f" + "reference": "3b4d5d197c04e5a0b2a250d97c4761a07da9c85e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/support/zipball/37663c414646ea7435121a3fef3d5d2d4f45a81f", - "reference": "37663c414646ea7435121a3fef3d5d2d4f45a81f", + "url": "https://api.github.com/repos/filamentphp/support/zipball/3b4d5d197c04e5a0b2a250d97c4761a07da9c85e", + "reference": "3b4d5d197c04e5a0b2a250d97c4761a07da9c85e", "shasum": "" }, "require": { @@ -1669,20 +1669,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T00:39:58+00:00" + "time": "2024-01-21T14:44:58+00:00" }, { "name": "filament/tables", - "version": "v3.2.7", + "version": "v3.2.9", "source": { "type": "git", "url": "https://github.com/filamentphp/tables.git", - "reference": "f2fdc714813c2be3e9145c1c2758e63528443166" + "reference": "87c55f6a1107d6d70b61a83f2cd7495343fdb19c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/tables/zipball/f2fdc714813c2be3e9145c1c2758e63528443166", - "reference": "f2fdc714813c2be3e9145c1c2758e63528443166", + "url": "https://api.github.com/repos/filamentphp/tables/zipball/87c55f6a1107d6d70b61a83f2cd7495343fdb19c", + "reference": "87c55f6a1107d6d70b61a83f2cd7495343fdb19c", "shasum": "" }, "require": { @@ -1722,11 +1722,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T00:40:18+00:00" + "time": "2024-01-21T14:45:11+00:00" }, { "name": "filament/widgets", - "version": "v3.2.7", + "version": "v3.2.9", "source": { "type": "git", "url": "https://github.com/filamentphp/widgets.git", @@ -2509,6 +2509,70 @@ }, "time": "2023-12-07T10:44:41+00:00" }, + { + "name": "korridor/laravel-model-validation-rules", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/korridor/laravel-model-validation-rules.git", + "reference": "23537e5bd296a042bbe0c3a1c6d556c89dfbad42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/korridor/laravel-model-validation-rules/zipball/23537e5bd296a042bbe0c3a1c6d556c89dfbad42", + "reference": "23537e5bd296a042bbe0c3a1c6d556c89dfbad42", + "shasum": "" + }, + "require": { + "illuminate/database": "^10", + "illuminate/support": "^10", + "php": ">=8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.6", + "orchestra/testbench": "^8.0", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Korridor\\LaravelModelValidationRules\\ModelValidationServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Korridor\\LaravelModelValidationRules\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "korridor", + "email": "26689068+korridor@users.noreply.github.com" + } + ], + "description": "A laravel validation rule that uses eloquent to validate if a model exists", + "homepage": "https://github.com/korridor/laravel-model-validation-rules", + "keywords": [ + "eloquent", + "exist", + "laravel", + "model", + "rule", + "validation" + ], + "support": { + "issues": "https://github.com/korridor/laravel-model-validation-rules/issues", + "source": "https://github.com/korridor/laravel-model-validation-rules/tree/3.0.0" + }, + "time": "2023-02-16T11:15:08+00:00" + }, { "name": "laravel/fortify", "version": "v1.20.0", @@ -4458,31 +4522,31 @@ }, { "name": "nette/schema", - "version": "v1.2.5", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a" + "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a", - "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a", + "url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", + "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", "shasum": "" }, "require": { - "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", - "php": "7.1 - 8.3" + "nette/utils": "^4.0", + "php": "8.1 - 8.3" }, "require-dev": { - "nette/tester": "^2.3 || ^2.4", + "nette/tester": "^2.4", "phpstan/phpstan-nette": "^1.0", - "tracy/tracy": "^2.7" + "tracy/tracy": "^2.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -4514,9 +4578,9 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.2.5" + "source": "https://github.com/nette/schema/tree/v1.3.0" }, - "time": "2023-10-05T20:37:59+00:00" + "time": "2023-12-11T11:54:22+00:00" }, { "name": "nette/utils", @@ -8829,6 +8893,71 @@ }, "time": "2023-12-08T13:03:43+00:00" }, + { + "name": "tpetry/laravel-postgresql-enhanced", + "version": "0.33.0", + "source": { + "type": "git", + "url": "https://github.com/tpetry/laravel-postgresql-enhanced.git", + "reference": "a75749e45983ae548d4d85a9ef7c3240710a3384" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tpetry/laravel-postgresql-enhanced/zipball/a75749e45983ae548d4d85a9ef7c3240710a3384", + "reference": "a75749e45983ae548d4d85a9ef7c3240710a3384", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^2.6|^3.5", + "illuminate/container": "^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/database": "^6.0|^7.0|^8.79|^9.0|^10.0", + "illuminate/events": "^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0", + "php": "^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19.3|^3.5.0", + "nunomaduro/larastan": "^1.0|^2.1", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^8.5.23|^9.5.13" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Tpetry\\PostgresqlEnhanced\\PostgresqlEnhancedServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Tpetry\\PostgresqlEnhanced\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "tpetry", + "email": "tobias@tpetry.me" + } + ], + "description": "Support for many missing PostgreSQL specific features", + "homepage": "https://github.com/tpetry/laravel-postgresql-enhanced", + "keywords": [ + "laravel", + "postgresql" + ], + "support": { + "issues": "https://github.com/tpetry/laravel-postgresql-enhanced/issues", + "source": "https://github.com/tpetry/laravel-postgresql-enhanced/tree/0.33.0" + }, + "time": "2023-10-23T15:36:21+00:00" + }, { "name": "vlucas/phpdotenv", "version": "v5.6.0", @@ -9047,6 +9176,101 @@ } ], "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "551f46f52a93177d873f3be08a1649ae886b4a30" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/551f46f52a93177d873f3be08a1649ae886b4a30", + "reference": "551f46f52a93177d873f3be08a1649ae886b4a30", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^0.5.1 || ^1.0.0", + "jean85/pretty-package-versions": "^2.0.5", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "phpunit/php-code-coverage": "^10.1.7", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-timer": "^6.0", + "phpunit/phpunit": "^10.4.2", + "sebastian/environment": "^6.0.1", + "symfony/console": "^6.3.4 || ^7.0.0", + "symfony/process": "^6.3.4 || ^7.0.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0.0", + "ext-pcov": "*", + "ext-posix": "*", + "infection/infection": "^0.27.6", + "phpstan/phpstan": "^1.10.40", + "phpstan/phpstan-deprecation-rules": "^1.1.4", + "phpstan/phpstan-phpunit": "^1.3.15", + "phpstan/phpstan-strict-rules": "^1.5.2", + "squizlabs/php_codesniffer": "^3.7.2", + "symfony/filesystem": "^6.3.1 || ^7.0.0" + }, + "bin": [ + "bin/paratest", + "bin/paratest.bat", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2023-10-31T09:24:17+00:00" + }, { "name": "fakerphp/faker", "version": "v1.23.1", @@ -9110,6 +9334,67 @@ }, "time": "2024-01-02T13:46:09+00:00" }, + { + "name": "fidry/cpu-core-counter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "85193c0b0cb5c47894b5eaec906e946f054e7077" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/85193c0b0cb5c47894b5eaec906e946f054e7077", + "reference": "85193c0b0cb5c47894b5eaec906e946f054e7077", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2023-09-17T21:38:23+00:00" + }, { "name": "filp/whoops", "version": "2.15.4", @@ -9292,6 +9577,65 @@ }, "time": "2020-07-09T08:09:16+00:00" }, + { + "name": "jean85/pretty-package-versions", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.17", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^0.12.66", + "phpunit/phpunit": "^7.5|^8.5|^9.4", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + }, + "time": "2021-10-08T21:21:46+00:00" + }, { "name": "larastan/larastan", "version": "v2.8.1", diff --git a/database/factories/ClientFactory.php b/database/factories/ClientFactory.php new file mode 100644 index 00000000..814cfdc8 --- /dev/null +++ b/database/factories/ClientFactory.php @@ -0,0 +1,37 @@ + + */ +class ClientFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->company(), + 'organization_id' => Organization::factory(), + ]; + } + + public function forOrganization(Organization $organization): self + { + return $this->state(function (array $attributes) use ($organization) { + return [ + 'organization_id' => $organization->getKey(), + ]; + }); + } +} diff --git a/database/factories/TeamFactory.php b/database/factories/OrganizationFactory.php similarity index 80% rename from database/factories/TeamFactory.php rename to database/factories/OrganizationFactory.php index c2cc4e0e..1f305b6f 100644 --- a/database/factories/TeamFactory.php +++ b/database/factories/OrganizationFactory.php @@ -4,13 +4,14 @@ namespace Database\Factories; +use App\Models\Organization; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Team> + * @extends Factory */ -class TeamFactory extends Factory +class OrganizationFactory extends Factory { /** * Define the model's default state. diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php new file mode 100644 index 00000000..709a2320 --- /dev/null +++ b/database/factories/ProjectFactory.php @@ -0,0 +1,49 @@ + + */ +class ProjectFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->company(), + 'color' => $this->faker->hexColor(), + 'organization_id' => Organization::factory(), + 'client_id' => null, + ]; + } + + public function forOrganization(Organization $organization): self + { + return $this->state(function (array $attributes) use ($organization) { + return [ + 'organization_id' => $organization->getKey(), + ]; + }); + } + + public function forClient(?Client $client): self + { + return $this->state(function (array $attributes) use ($client) { + return [ + 'client_id' => $client?->getKey(), + ]; + }); + } +} diff --git a/database/factories/TagFactory.php b/database/factories/TagFactory.php new file mode 100644 index 00000000..4d00a041 --- /dev/null +++ b/database/factories/TagFactory.php @@ -0,0 +1,37 @@ + + */ +class TagFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->name, + 'organization_id' => Organization::factory(), + ]; + } + + public function forOrganization(Organization $organization): self + { + return $this->state(function (array $attributes) use ($organization) { + return [ + 'organization_id' => $organization->getKey(), + ]; + }); + } +} diff --git a/database/factories/TaskFactory.php b/database/factories/TaskFactory.php new file mode 100644 index 00000000..0921e9c4 --- /dev/null +++ b/database/factories/TaskFactory.php @@ -0,0 +1,48 @@ + + */ +class TaskFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->word(), + 'project_id' => Project::factory(), + 'organization_id' => Organization::factory(), + ]; + } + + public function forProject(Project $project): self + { + return $this->state(function (array $attributes) use ($project) { + return [ + 'project_id' => $project->getKey(), + ]; + }); + } + + public function forOrganization(Organization $organization): self + { + return $this->state(function (array $attributes) use ($organization) { + return [ + 'organization_id' => $organization->getKey(), + ]; + }); + } +} diff --git a/database/factories/TimeEntryFactory.php b/database/factories/TimeEntryFactory.php new file mode 100644 index 00000000..1fdad027 --- /dev/null +++ b/database/factories/TimeEntryFactory.php @@ -0,0 +1,74 @@ + + */ +class TimeEntryFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $start = $this->faker->dateTimeBetween('-1 year', '-1 hour'); + + return [ + 'description' => $this->faker->sentence(), + 'start' => $start, + 'end' => $this->faker->dateTimeBetween($start, 'now'), + 'billable' => $this->faker->boolean(), + 'tags' => [], + 'user_id' => User::factory(), + 'organization_id' => Organization::factory(), + ]; + } + + public function forUser(User $user): self + { + return $this->state(function (array $attributes) use ($user) { + return [ + 'user_id' => $user->getKey(), + ]; + }); + } + + public function forOrganization(Organization $organization): self + { + return $this->state(function (array $attributes) use ($organization) { + return [ + 'organization_id' => $organization->getKey(), + ]; + }); + } + + public function forProject(?Project $project): self + { + return $this->state(function (array $attributes) use ($project) { + return [ + 'project_id' => $project?->getKey(), + ]; + }); + } + + public function forTask(?Task $task): self + { + return $this->state(function (array $attributes) use ($task) { + return [ + 'task_id' => $task?->getKey(), + ]; + }); + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 94854008..60f31ffb 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -4,14 +4,13 @@ namespace Database\Factories; -use App\Models\Team; +use App\Models\Organization; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; -use Laravel\Jetstream\Features; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> + * @extends Factory */ class UserFactory extends Factory { @@ -50,16 +49,12 @@ public function unverified(): static /** * Indicate that the user should have a personal team. */ - public function withPersonalTeam(?callable $callback = null): static + public function withPersonalOrganization(?callable $callback = null): static { - if (! Features::hasTeamFeatures()) { - return $this->state([]); - } - return $this->has( - Team::factory() + Organization::factory() ->state(fn (array $attributes, User $user) => [ - 'name' => $user->name.'\'s Team', + 'name' => $user->name.'\'s Organization', 'user_id' => $user->id, 'personal_team' => true, ]) diff --git a/database/migrations/2020_05_21_100000_create_teams_table.php b/database/migrations/2020_05_21_100000_create_organizations_table.php similarity index 90% rename from database/migrations/2020_05_21_100000_create_teams_table.php rename to database/migrations/2020_05_21_100000_create_organizations_table.php index 44a6ce42..f2a41ebb 100644 --- a/database/migrations/2020_05_21_100000_create_teams_table.php +++ b/database/migrations/2020_05_21_100000_create_organizations_table.php @@ -13,7 +13,7 @@ */ public function up(): void { - Schema::create('teams', function (Blueprint $table) { + Schema::create('organizations', function (Blueprint $table) { $table->uuid('id')->primary(); $table->foreignUuid('user_id')->index(); $table->string('name'); diff --git a/database/migrations/2020_05_21_200000_create_team_user_table.php b/database/migrations/2020_05_21_200000_create_organization_user_table.php similarity index 76% rename from database/migrations/2020_05_21_200000_create_team_user_table.php rename to database/migrations/2020_05_21_200000_create_organization_user_table.php index 7e223c5d..1097ffaa 100644 --- a/database/migrations/2020_05_21_200000_create_team_user_table.php +++ b/database/migrations/2020_05_21_200000_create_organization_user_table.php @@ -13,14 +13,14 @@ */ public function up(): void { - Schema::create('team_user', function (Blueprint $table) { + Schema::create('organization_user', function (Blueprint $table) { $table->uuid('id')->primary(); - $table->foreignUuid('team_id'); + $table->foreignUuid('organization_id'); $table->foreignUuid('user_id'); $table->string('role')->nullable(); $table->timestamps(); - $table->unique(['team_id', 'user_id']); + $table->unique(['organization_id', 'user_id']); }); } diff --git a/database/migrations/2020_05_21_300000_create_team_invitations_table.php b/database/migrations/2020_05_21_300000_create_organization_invitations_table.php similarity index 78% rename from database/migrations/2020_05_21_300000_create_team_invitations_table.php rename to database/migrations/2020_05_21_300000_create_organization_invitations_table.php index 12d4ceba..5c8c49c3 100644 --- a/database/migrations/2020_05_21_300000_create_team_invitations_table.php +++ b/database/migrations/2020_05_21_300000_create_organization_invitations_table.php @@ -13,16 +13,16 @@ */ public function up(): void { - Schema::create('team_invitations', function (Blueprint $table) { + Schema::create('organization_invitations', function (Blueprint $table) { $table->uuid('id')->primary(); - $table->foreignUuid('team_id') + $table->foreignUuid('organization_id') ->constrained() ->cascadeOnDelete(); $table->string('email'); $table->string('role')->nullable(); $table->timestamps(); - $table->unique(['team_id', 'email']); + $table->unique(['organization_id', 'email']); }); } diff --git a/database/migrations/2024_01_20_110218_create_clients_table.php b/database/migrations/2024_01_20_110218_create_clients_table.php new file mode 100644 index 00000000..463c7e5d --- /dev/null +++ b/database/migrations/2024_01_20_110218_create_clients_table.php @@ -0,0 +1,36 @@ +uuid('id')->primary(); + $table->string('name', 255); + $table->uuid('organization_id'); + $table->foreign('organization_id') + ->references('id') + ->on('organizations') + ->cascadeOnUpdate() + ->restrictOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('clients'); + } +}; diff --git a/database/migrations/2024_01_20_110439_create_projects_table.php b/database/migrations/2024_01_20_110439_create_projects_table.php new file mode 100644 index 00000000..8f5b32b4 --- /dev/null +++ b/database/migrations/2024_01_20_110439_create_projects_table.php @@ -0,0 +1,43 @@ +uuid('id')->primary(); + $table->string('name', 255); + $table->string('color', 16); + $table->uuid('client_id')->nullable(); + $table->foreign('client_id') + ->references('id') + ->on('clients') + ->cascadeOnUpdate() + ->restrictOnDelete(); + $table->uuid('organization_id'); + $table->foreign('organization_id') + ->references('id') + ->on('organizations') + ->cascadeOnUpdate() + ->restrictOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('projects'); + } +}; diff --git a/database/migrations/2024_01_20_110444_create_tasks_table.php b/database/migrations/2024_01_20_110444_create_tasks_table.php new file mode 100644 index 00000000..876508f2 --- /dev/null +++ b/database/migrations/2024_01_20_110444_create_tasks_table.php @@ -0,0 +1,42 @@ +uuid('id')->primary(); + $table->string('name', 255); + $table->uuid('project_id'); + $table->foreign('project_id') + ->references('id') + ->on('projects') + ->cascadeOnUpdate() + ->restrictOnDelete(); + $table->uuid('organization_id'); + $table->foreign('organization_id') + ->references('id') + ->on('organizations') + ->cascadeOnUpdate() + ->restrictOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tasks'); + } +}; diff --git a/database/migrations/2024_01_20_110452_create_tags_table.php b/database/migrations/2024_01_20_110452_create_tags_table.php new file mode 100644 index 00000000..a1edb629 --- /dev/null +++ b/database/migrations/2024_01_20_110452_create_tags_table.php @@ -0,0 +1,36 @@ +uuid('id')->primary(); + $table->string('name', 255); + $table->uuid('organization_id'); + $table->foreign('organization_id') + ->references('id') + ->on('organizations') + ->cascadeOnUpdate() + ->restrictOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tags'); + } +}; 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 new file mode 100644 index 00000000..91c5dcda --- /dev/null +++ b/database/migrations/2024_01_20_110837_create_time_entries_table.php @@ -0,0 +1,58 @@ +uuid('id')->primary(); + $table->string('description', 255); + $table->dateTime('start'); + $table->dateTime('end')->nullable(); + $table->boolean('billable')->default(false); + $table->uuid('user_id'); + $table->foreign('user_id') + ->references('id') + ->on('users') + ->cascadeOnUpdate() + ->restrictOnDelete(); + $table->uuid('organization_id'); + $table->foreign('organization_id') + ->references('id') + ->on('organizations') + ->cascadeOnUpdate() + ->restrictOnDelete(); + $table->uuid('project_id')->nullable(); + $table->foreign('project_id') + ->references('id') + ->on('projects') + ->cascadeOnUpdate() + ->restrictOnDelete(); + $table->uuid('task_id')->nullable(); + $table->foreign('task_id') + ->references('id') + ->on('tasks') + ->cascadeOnUpdate() + ->restrictOnDelete(); + $table->jsonb('tags')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('time_entries'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 07679bee..eabb5750 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -5,8 +5,14 @@ namespace Database\Seeders; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; +use App\Models\Client; +use App\Models\Organization; +use App\Models\Project; +use App\Models\Task; +use App\Models\TimeEntry; use App\Models\User; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\DB; class DatabaseSeeder extends Seeder { @@ -15,9 +21,44 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - User::factory()->withPersonalTeam()->create([ + $this->deleteAll(); + $organization = Organization::factory()->create([ + 'name' => 'ACME Corp', + ]); + $user1 = User::factory()->withPersonalOrganization()->create([ 'name' => 'Test User', 'email' => 'test@example.com', ]); + $userAcmeAdmin = User::factory()->create([ + 'name' => 'ACME Admin', + 'email' => 'admin@acme.test', + ]); + $user1->organizations()->attach($organization, [ + 'role' => 'editor', + ]); + $userAcmeAdmin->organizations()->attach($organization, [ + 'role' => 'admin', + ]); + $client = Client::factory()->create([ + 'name' => 'Big Company', + ]); + $bigCompanyProject = Project::factory()->forClient($client)->create([ + 'name' => 'Big Company Project', + ]); + Task::factory()->forProject($bigCompanyProject)->create(); + + $internalProject = Project::factory()->create([ + 'name' => 'Internal Project', + ]); + } + + private function deleteAll(): void + { + DB::table((new TimeEntry())->getTable())->delete(); + DB::table((new Task())->getTable())->delete(); + DB::table((new Project())->getTable())->delete(); + DB::table((new Client())->getTable())->delete(); + DB::table((new User())->getTable())->delete(); + DB::table((new Organization())->getTable())->delete(); } } diff --git a/docker-compose.yml b/docker-compose.yml index b65aaa47..a0a43805 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,6 @@ services: extra_hosts: - 'host.docker.internal:host-gateway' ports: - - '${APP_PORT:-80}:80' - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' environment: WWWUSER: '${WWWUSER}' diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue index b3e3a5ec..76155d51 100644 --- a/resources/js/Layouts/AppLayout.vue +++ b/resources/js/Layouts/AppLayout.vue @@ -111,13 +111,13 @@ const logout = () => {