From accb8ebcc522e6b9974ba4cac7a05de3f97ba8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20Ioni=C8=9B=C4=83?= Date: Thu, 21 Sep 2023 15:14:03 +0100 Subject: [PATCH] review users (#290) --- app/Concerns/BelongsToOrganization.php | 40 +++ .../Arrayable.php} | 18 +- app/Concerns/Enums/Comparable.php | 20 ++ app/Concerns/Enums/HasLabel.php | 22 ++ app/Concerns/HasRole.php | 83 ++++++ app/Concerns/LogsActivityForApproval.php | 2 +- app/Concerns/MustSetInitialPassword.php | 22 +- app/Enums/ActivityDomain.php | 5 +- app/Enums/OrganizationStatus.php | 11 +- app/Enums/ProjectStatus.php | 11 +- app/Enums/UserRole.php | 19 +- app/Enums/VolunteerStatus.php | 12 +- .../Actions/Tables/ExportAction.php | 1 - .../RelationManagers/UsersRelationManager.php | 41 ++- .../MessagesRelationManager.php | 2 +- app/Filament/Resources/UserResource.php | 89 +++--- .../UserResource/Pages/CreateUser.php | 43 --- .../Resources/UserResource/Pages/EditUser.php | 43 --- app/Forms/Components/UserLink.php | 2 +- .../Auth/RegisteredUserController.php | 7 +- .../Auth/VerifyEmailController.php | 3 +- .../Dashboard/ProjectController.php | 12 +- .../Dashboard/RegionalProjectController.php | 13 +- .../Controllers/Dashboard/UserController.php | 69 +++++ .../Dashboard/VolunteerController.php | 6 +- app/Http/Controllers/DashboardController.php | 2 +- app/Http/Middleware/HandleInertiaRequests.php | 5 +- .../Requests/RegionalProject/StoreRequest.php | 2 +- app/Http/Requests/RegistrationRequest.php | 2 +- .../Collections/ResourceCollection.php | 22 +- .../Resources/Collections/UserCollection.php | 38 +++ app/Http/Resources/Resource.php | 49 +++ app/Http/Resources/TicketMessageResource.php | 2 +- app/Http/Resources/UserResource.php | 23 ++ .../SendOrganizationForApprovalListener.php | 4 +- .../Ticket/SendTicketCreatedNotification.php | 4 +- .../SendTicketReplyReceivedNotification.php | 4 +- .../SendTicketStatusChangedNotification.php | 4 +- app/Listeners/User/DeleteOrganization.php | 2 +- app/Models/Organization.php | 8 - app/Models/User.php | 27 +- app/Policies/OrganizationPolicy.php | 27 +- app/Policies/ProjectPolicy.php | 15 +- app/Policies/TicketPolicy.php | 6 +- app/Policies/UserPolicy.php | 45 +++ .../UserDoesntBelongToAnOrganization.php | 29 ++ app/Traits/HasRole.php | 61 ---- app/helpers.php | 20 +- database/factories/OrganizationFactory.php | 14 +- database/factories/TicketFactory.php | 13 +- database/factories/UserFactory.php | 25 +- .../2014_10_12_000000_create_users_table.php | 6 +- ...dd_organization_column_to_users_table.php} | 10 +- database/seeders/DatabaseSeeder.php | 8 +- lang/ro.json | 44 ++- lang/ro/user.php | 25 +- lang/ro/volunteer.php | 8 +- public/images/svg/default_avatar.svg | 11 - resources/js/Components/Footer.vue | 10 +- resources/js/Components/LanguageSwitcher.vue | 64 ++++ resources/js/Components/Modal.vue | 98 ------ resources/js/Components/Navbar.vue | 282 +++++++----------- resources/js/Components/Notification.vue | 24 +- resources/js/Components/ResponsiveNavLink.vue | 26 -- resources/js/Components/Title.vue | 4 +- .../js/Components/buttons/PrimaryButton.vue | 44 ++- .../js/Components/buttons/SecondaryButton.vue | 8 +- resources/js/Components/cards/ProjectCard.vue | 10 +- .../Components/cards/ProjectSummaryCard.vue | 29 +- .../js/Components/charts/ProjectsChart.vue | 18 +- .../js/Components/charts/TimeEvolution.vue | 54 ++-- .../js/Components/dropdowns/FlyoutMenu.vue | 15 +- resources/js/Components/filters/Sort.vue | 5 +- resources/js/Components/form/FileGroup.vue | 5 +- .../js/Components/form/SelectNoBorder.vue | 58 ---- resources/js/Components/links/NavLink.vue | 20 +- .../Components/modals/ChampionshipModal.vue | 11 +- .../Components/modals/ConfirmationModal.vue | 188 ++++++++++++ .../js/Components/modals/DonateModal.vue | 9 +- resources/js/Components/modals/Modal.vue | 100 ++++--- .../modals/ToggleTicketStatusModal.vue | 124 -------- .../js/Components/modals/VolunteerModal.vue | 9 +- resources/js/Layouts/DashboardLayout.vue | 22 +- .../js/Pages/AdminOng/Projects/AddProject.vue | 4 +- .../AdminOng/Projects/AddRegionalProject.vue | 4 +- resources/js/Pages/AdminOng/Tickets/Index.vue | 94 +++--- resources/js/Pages/AdminOng/Tickets/Show.vue | 87 +++--- resources/js/Pages/AdminOng/Users/Index.vue | 86 ++++++ .../js/Pages/AdminOng/Volunteers/Index.vue | 48 ++- resources/js/Pages/Auth/ConfirmPassword.vue | 10 +- resources/js/Pages/Auth/ForgotPassword.vue | 10 +- resources/js/Pages/Auth/Login.vue | 10 +- resources/js/Pages/Auth/Register.vue | 10 +- .../js/Pages/Auth/Registration/Step1.vue | 15 +- .../js/Pages/Auth/Registration/Step2.vue | 30 +- .../js/Pages/Auth/Registration/Step3.vue | 14 +- .../js/Pages/Auth/Registration/Step4.vue | 14 +- .../js/Pages/Auth/Registration/Step5.vue | 16 +- .../js/Pages/Auth/Registration/Success.vue | 17 +- resources/js/Pages/Auth/ResetPassword.vue | 10 +- resources/js/Pages/Auth/Welcome.vue | 10 +- .../Profile/Partials/UpdatePasswordForm.vue | 18 +- .../Partials/UpdateProfileInformationForm.vue | 26 +- .../Public/Championship/Championship.vue | 11 +- .../js/Pages/Public/Regional/Edition.vue | 10 +- .../js/Pages/Public/Regional/Regional.vue | 10 +- resources/js/Pages/Public/Website/Contact.vue | 10 +- routes/dashboard.php | 17 +- tailwind.config.js | 31 +- 109 files changed, 1568 insertions(+), 1452 deletions(-) create mode 100644 app/Concerns/BelongsToOrganization.php rename app/Concerns/{ArrayableEnum.php => Enums/Arrayable.php} (62%) create mode 100644 app/Concerns/Enums/Comparable.php create mode 100644 app/Concerns/Enums/HasLabel.php create mode 100644 app/Concerns/HasRole.php create mode 100644 app/Http/Controllers/Dashboard/UserController.php create mode 100644 app/Http/Resources/Collections/UserCollection.php create mode 100644 app/Http/Resources/Resource.php create mode 100644 app/Http/Resources/UserResource.php create mode 100644 app/Policies/UserPolicy.php create mode 100644 app/Rules/UserDoesntBelongToAnOrganization.php delete mode 100644 app/Traits/HasRole.php rename database/migrations/{2023_05_05_142245_update_users.php => 2023_05_05_142245_add_organization_column_to_users_table.php} (62%) delete mode 100644 public/images/svg/default_avatar.svg create mode 100644 resources/js/Components/LanguageSwitcher.vue delete mode 100644 resources/js/Components/Modal.vue delete mode 100644 resources/js/Components/ResponsiveNavLink.vue delete mode 100644 resources/js/Components/form/SelectNoBorder.vue create mode 100644 resources/js/Components/modals/ConfirmationModal.vue delete mode 100644 resources/js/Components/modals/ToggleTicketStatusModal.vue create mode 100644 resources/js/Pages/AdminOng/Users/Index.vue diff --git a/app/Concerns/BelongsToOrganization.php b/app/Concerns/BelongsToOrganization.php new file mode 100644 index 00000000..143ea3d0 --- /dev/null +++ b/app/Concerns/BelongsToOrganization.php @@ -0,0 +1,40 @@ +fillable[] = 'organization_id'; + } + + public function organization(): BelongsTo + { + return $this->belongsTo(Organization::class); + } + + public function belongsToOrganization(?Organization $organization = null): bool + { + if ($organization === null) { + return $this->organization_id !== null; + } + + return $this->organization_id === $organization->getKey(); + } + + public function scopeWhereBelongsToOrganization(Builder $query, ?Organization $organization = null): Builder + { + if ($organization === null) { + return $query->whereNotNull('organization_id'); + } + + return $query->whereBelongsTo($organization); + } +} diff --git a/app/Concerns/ArrayableEnum.php b/app/Concerns/Enums/Arrayable.php similarity index 62% rename from app/Concerns/ArrayableEnum.php rename to app/Concerns/Enums/Arrayable.php index c58af5d5..4c952ef2 100644 --- a/app/Concerns/ArrayableEnum.php +++ b/app/Concerns/Enums/Arrayable.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\Concerns; +namespace App\Concerns\Enums; -trait ArrayableEnum +trait Arrayable { public static function names(): array { @@ -28,18 +28,4 @@ public static function options(): array ]) ->all(); } - - public function label(): string - { - $label = collect([$this->translationKeyPrefix(), $this->value]) - ->filter() - ->implode('.'); - - return __($label); - } - - protected function translationKeyPrefix(): ?string - { - return null; - } } diff --git a/app/Concerns/Enums/Comparable.php b/app/Concerns/Enums/Comparable.php new file mode 100644 index 00000000..efefc281 --- /dev/null +++ b/app/Concerns/Enums/Comparable.php @@ -0,0 +1,20 @@ +value === $enum->value; + } + + return $this->value === $enum; + } +} diff --git a/app/Concerns/Enums/HasLabel.php b/app/Concerns/Enums/HasLabel.php new file mode 100644 index 00000000..b96a3ef4 --- /dev/null +++ b/app/Concerns/Enums/HasLabel.php @@ -0,0 +1,22 @@ +labelKeyPrefix(), $this->value]) + ->filter() + ->implode('.'); + + return __($label); + } +} diff --git a/app/Concerns/HasRole.php b/app/Concerns/HasRole.php new file mode 100644 index 00000000..5aa8b7c6 --- /dev/null +++ b/app/Concerns/HasRole.php @@ -0,0 +1,83 @@ +casts['role'] = UserRole::class; + } + + private function hasRole(UserRole $role): bool + { + return $this->role === $role; + } + + public function isSuperUser(): bool + { + return $this->isSuperAdmin() || $this->isSuperManager(); + } + + public function isSuperAdmin(): bool + { + return $this->hasRole(UserRole::SUPERADMIN); + } + + public function isSuperManager(): bool + { + return $this->hasRole(UserRole::SUPERMANAGER); + } + + public function isOrganizationAdmin(?Organization $organization = null): bool + { + return $this->hasRole(UserRole::ADMIN) && $this->belongsToOrganization($organization); + } + + public function isOrganizationManager(?Organization $organization = null): bool + { + return $this->hasRole(UserRole::USER) && $this->belongsToOrganization($organization); + } + + // ========================================== // + public function scopeOnlySuperUsers(Builder $query): Builder + { + return $query->whereIn('role', [UserRole::SUPERADMIN, UserRole::SUPERMANAGER]); + } + + public function scopeOnlySuperAdmins(Builder $query): Builder + { + return $query->where('role', UserRole::SUPERADMIN); + } + + public function scopeOnlySuperManagers(Builder $query): Builder + { + return $query->where('role', UserRole::SUPERMANAGER); + } + + public function scopeWithoutSuperUsers(Builder $query): Builder + { + return $query->whereNotIn('role', [UserRole::SUPERADMIN, UserRole::SUPERMANAGER]); + } + + // ========================================== // + + public function scopeOnlyOrganizationAdmins(Builder $query, ?Organization $organization = null): Builder + { + return $query->where('role', UserRole::ADMIN) + ->whereBelongsToOrganization($organization); + } + + // ========================================== // + + public function isDonor(): bool + { + return $this->role === UserRole::donor; + } +} diff --git a/app/Concerns/LogsActivityForApproval.php b/app/Concerns/LogsActivityForApproval.php index 87c49eb5..3ddd1894 100644 --- a/app/Concerns/LogsActivityForApproval.php +++ b/app/Concerns/LogsActivityForApproval.php @@ -59,7 +59,7 @@ public function tapActivity(Activity $activity, string $eventName) return; } - if (auth()->user()->isBbAdmin() || auth()->user()->isBbManager()) { + if (auth()->user()->isSuperUser()) { activity()->disableLogging(); if (! $activity->description) { diff --git a/app/Concerns/MustSetInitialPassword.php b/app/Concerns/MustSetInitialPassword.php index 30fe6707..85fd17c2 100644 --- a/app/Concerns/MustSetInitialPassword.php +++ b/app/Concerns/MustSetInitialPassword.php @@ -4,9 +4,8 @@ namespace App\Concerns; -use App\Enums\UserRole; -use App\Notifications\Admin\WelcomeNotification as AdminWelcomeNotification; -use App\Notifications\Ngo\WelcomeNotification; +use App\Notifications\Admin; +use App\Notifications\Ngo; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; @@ -21,10 +20,8 @@ protected static function bootMustSetInitialPassword(): void }); static::created(function (self $user) { - if (! app()->runningInConsole()) { - if (! empty($user->created_by)) { - $user->sendWelcomeNotification(); - } + if (filled($user->created_by)) { + $user->sendWelcomeNotification(); } }); } @@ -45,11 +42,10 @@ public function setPassword(string $password): bool public function sendWelcomeNotification(): void { - if ($this->role === UserRole::ngo_admin) { - $this->notify(new WelcomeNotification()); - - return; - } - $this->notify(new AdminWelcomeNotification()); + $this->notify( + $this->isSuperUser() + ? new Admin\WelcomeNotification + : new Ngo\WelcomeNotification + ); } } diff --git a/app/Enums/ActivityDomain.php b/app/Enums/ActivityDomain.php index 864061c5..89d6e63f 100644 --- a/app/Enums/ActivityDomain.php +++ b/app/Enums/ActivityDomain.php @@ -4,11 +4,12 @@ namespace App\Enums; -use App\Concerns\ArrayableEnum; +use App\Concerns\Enums\Arrayable; enum ActivityDomain: string { - use ArrayableEnum; + use Arrayable; + case environmental_protection = 'Protecția mediului'; case education = 'Educație'; case healthcare = 'Sănătate'; diff --git a/app/Enums/OrganizationStatus.php b/app/Enums/OrganizationStatus.php index 1e787ec5..135bc66a 100644 --- a/app/Enums/OrganizationStatus.php +++ b/app/Enums/OrganizationStatus.php @@ -4,18 +4,23 @@ namespace App\Enums; -use App\Concerns\ArrayableEnum; +use App\Concerns\Enums\Arrayable; +use App\Concerns\Enums\Comparable; +use App\Concerns\Enums\HasLabel; enum OrganizationStatus: string { - use ArrayableEnum; + use Arrayable; + use Comparable; + use HasLabel; + case draft = 'draft'; case pending = 'pending'; case approved = 'active'; case rejected = 'disabled'; case pending_changes = 'pending_changes'; - protected function translationKeyPrefix(): ?string + protected function labelKeyPrefix(): ?string { return 'organization.status_arr'; } diff --git a/app/Enums/ProjectStatus.php b/app/Enums/ProjectStatus.php index a15282f5..47eaee8f 100644 --- a/app/Enums/ProjectStatus.php +++ b/app/Enums/ProjectStatus.php @@ -4,11 +4,16 @@ namespace App\Enums; -use App\Concerns\ArrayableEnum; +use App\Concerns\Enums\Arrayable; +use App\Concerns\Enums\Comparable; +use App\Concerns\Enums\HasLabel; enum ProjectStatus: string { - use ArrayableEnum; + use Arrayable; + use Comparable; + use HasLabel; + case draft = 'draft'; case pending = 'pending'; case change_request = 'change_request'; @@ -17,7 +22,7 @@ enum ProjectStatus: string case active = 'active'; case disabled = 'disabled'; - protected function translationKeyPrefix(): ?string + protected function labelKeyPrefix(): ?string { return 'project.status_arr'; } diff --git a/app/Enums/UserRole.php b/app/Enums/UserRole.php index 5084a6fb..a22c69cd 100644 --- a/app/Enums/UserRole.php +++ b/app/Enums/UserRole.php @@ -4,17 +4,24 @@ namespace App\Enums; -use App\Concerns\ArrayableEnum; +use App\Concerns\Enums\Arrayable; +use App\Concerns\Enums\Comparable; +use App\Concerns\Enums\HasLabel; enum UserRole: string { - use ArrayableEnum; + use Arrayable; + use Comparable; + use HasLabel; + + case SUPERADMIN = 'superadmin'; + case SUPERMANAGER = 'supermanager'; + case ADMIN = 'admin'; + case USER = 'user'; + case donor = 'donor'; - case ngo_admin = 'ngo-admin'; - case bb_manager = 'bb-manager'; - case bb_admin = 'bb-admin'; - public function translationKeyPrefix(): string + public function labelKeyPrefix(): string { return 'user.roles'; } diff --git a/app/Enums/VolunteerStatus.php b/app/Enums/VolunteerStatus.php index 9383cf0b..cb170b09 100644 --- a/app/Enums/VolunteerStatus.php +++ b/app/Enums/VolunteerStatus.php @@ -4,15 +4,21 @@ namespace App\Enums; -use App\Concerns\ArrayableEnum; +use App\Concerns\Enums\Arrayable; +use App\Concerns\Enums\Comparable; +use App\Concerns\Enums\HasLabel; enum VolunteerStatus: string { - use ArrayableEnum; + use Arrayable; + use Comparable; + use HasLabel; + case PENDING = 'pending'; case APPROVED = 'approved'; case REJECTED = 'rejected'; - public function translationKeyPrefix(): string + + public function labelKeyPrefix(): string { return 'volunteer.statuses'; } diff --git a/app/Filament/Resources/OrganizationResource/Actions/Tables/ExportAction.php b/app/Filament/Resources/OrganizationResource/Actions/Tables/ExportAction.php index b71d3d31..2a31c9a1 100644 --- a/app/Filament/Resources/OrganizationResource/Actions/Tables/ExportAction.php +++ b/app/Filament/Resources/OrganizationResource/Actions/Tables/ExportAction.php @@ -45,7 +45,6 @@ protected function setUp(): void ->with([ 'activityDomains', 'counties', - 'users' => fn ($q) => $q->onlyNGOAdmins(), 'projects' => fn ($q) => $q->select('id', 'organization_id', 'status') ->withCount('donations'), ]) diff --git a/app/Filament/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php b/app/Filament/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php index 00fb8542..7c76630a 100644 --- a/app/Filament/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php +++ b/app/Filament/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php @@ -1,14 +1,16 @@ schema([ - Forms\Components\TextInput::make('name') + TextInput::make('name') + ->label(__('user.name')) ->required() ->maxLength(255), + + TextInput::make('email') + ->label(__('user.email')) + ->email() + ->unique('users', 'email') + ->required(), ]); } @@ -35,20 +44,34 @@ public static function table(Table $table): Table { return $table ->columns([ - Tables\Columns\TextColumn::make('name'), + TextColumn::make('name') + ->label(__('user.name')) + ->sortable(), + + TextColumn::make('email') + ->label(__('user.email')) + ->sortable(), + + TextColumn::make('role') + ->label(__('user.role')) + ->getStateUsing(fn (User $record) => $record->role?->label()) + ->sortable(), ]) ->filters([ // ]) ->headerActions([ - Tables\Actions\CreateAction::make(), + Tables\Actions\AttachAction::make() + ->label(__('user.action.attach')) + ->modalHeading(__('user.action.attach')) + ->color('primary'), ]) ->actions([ - Tables\Actions\EditAction::make(), - Tables\Actions\DeleteAction::make(), + Tables\Actions\DetachAction::make() + ->label(__('user.action.detach')), ]) ->bulkActions([ - Tables\Actions\DeleteBulkAction::make(), + // ]); } } diff --git a/app/Filament/Resources/TicketResource/RelationManagers/MessagesRelationManager.php b/app/Filament/Resources/TicketResource/RelationManagers/MessagesRelationManager.php index 23c78013..529d7ed6 100644 --- a/app/Filament/Resources/TicketResource/RelationManagers/MessagesRelationManager.php +++ b/app/Filament/Resources/TicketResource/RelationManagers/MessagesRelationManager.php @@ -41,7 +41,7 @@ public static function table(Table $table): Table ->translateLabel() ->weight('bold') ->color(function (Component $livewire, TicketMessage $record) { - if ($record->user->isBbAdmin()) { + if ($record->user->isSuperUser()) { return 'warning'; } diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index 8a3d1b72..07e1b64a 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -10,7 +10,6 @@ use App\Filament\Resources\UserResource\RelationManagers\DonationsRelationManager; use App\Filament\Resources\UserResource\RelationManagers\VolunteersRelationManager; use App\Models\User; -use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Resources\Form; @@ -30,6 +29,8 @@ class UserResource extends Resource protected static ?string $navigationIcon = 'heroicon-o-users'; + protected static ?string $recordTitleAttribute = 'name'; + public static function getPluralLabel(): ?string { return __('user.label.plural'); @@ -47,55 +48,39 @@ protected static function getNavigationGroup(): ?string public static function form(Form $form): Form { - return $form->schema([ - Fieldset::make(__('user.labels.general_data')) - ->columns(1)->schema([ - TextInput::make('name') - ->label(__('user.name')) - ->inlineLabel() - ->required() - ->maxLength(255), - TextInput::make('email') - ->label(__('user.email')) - ->email() - ->unique('users', 'email') - ->inlineLabel() - ->required(), - Select::make('role') - ->label(__('user.role')) - ->options( - collect( - UserRole::options() - )->only( - [ - UserRole::bb_admin->value, - UserRole::bb_manager->value, - UserRole::ngo_admin->value, - ] - )->toArray() - )->reactive() - ->inlineLabel() - ->visibleOn(['edit', 'create']) - ->required(), - Select::make('role') - ->label(__('user.role')) - ->options(UserRole::options()) - ->reactive() - ->inlineLabel() - ->visibleOn(['view']) - ->required(), - Select::make('organization') - ->label(__('user.organization')) - ->relationship('organization', 'name') - ->hidden(function (callable $get) { - return $get('role') !== UserRole::ngo_admin->value; - }) - ->searchable() - ->preload() - ->required(), - ]), - - ]); + return $form + ->columns(1) + ->schema([ + TextInput::make('name') + ->label(__('user.name')) + ->inlineLabel() + ->required() + ->maxLength(255), + + TextInput::make('email') + ->label(__('user.email')) + ->email() + ->unique('users', 'email', ignoreRecord: true) + ->inlineLabel() + ->required(), + + Select::make('role') + ->label(__('user.role')) + ->options(UserRole::options()) + ->enum(UserRole::class) + ->reactive() + ->inlineLabel() + ->required(), + + Select::make('organization') + ->label(__('user.organization')) + ->relationship('organization', 'name') + ->hidden(fn (callable $get) => UserRole::ADMIN->is($get('role'))) + ->searchable() + ->inlineLabel() + ->preload() + ->required(), + ]); } public static function table(Table $table): Table @@ -148,16 +133,16 @@ public static function table(Table $table): Table Tables\Actions\DeleteBulkAction::make(), ]); } + public static function getRelations(): array { return [ DonationsRelationManager::class, VolunteersRelationManager::class, - BadgesRelationManager::class + BadgesRelationManager::class, ]; } - public static function getPages(): array { return [ diff --git a/app/Filament/Resources/UserResource/Pages/CreateUser.php b/app/Filament/Resources/UserResource/Pages/CreateUser.php index e61952f2..ce4c16e9 100644 --- a/app/Filament/Resources/UserResource/Pages/CreateUser.php +++ b/app/Filament/Resources/UserResource/Pages/CreateUser.php @@ -4,11 +4,7 @@ namespace App\Filament\Resources\UserResource\Pages; -use App\Enums\UserRole; use App\Filament\Resources\UserResource; -use Filament\Forms\Components\Select; -use Filament\Forms\Components\TextInput; -use Filament\Resources\Form; use Filament\Resources\Pages\CreateRecord; class CreateUser extends CreateRecord @@ -17,45 +13,6 @@ class CreateUser extends CreateRecord protected static bool $canCreateAnother = false; - public function form(Form $form): Form - { - return $form - ->schema([ - TextInput::make('name') - ->label(__('user.name')) - ->required(), - TextInput::make('email') - ->label(__('user.email')) - ->email() - ->unique('users', 'email') - ->required(), - Select::make('role') - ->label(__('user.role')) - ->options( - collect( - UserRole::options() - )->only( - [ - UserRole::bb_admin->value, - UserRole::bb_manager->value, - UserRole::ngo_admin->value, - ] - )->toArray() - )->reactive() - ->required(), - Select::make('organization') - ->label(__('user.organization')) - ->relationship('organization', 'name') - ->hidden(function (callable $get) { - return $get('role') !== UserRole::ngo_admin->value; - }) - ->searchable() - ->preload() - ->required(), - - ]); - } - protected function mutateFormDataBeforeCreate(array $data): array { $data['created_by'] = auth()->id(); diff --git a/app/Filament/Resources/UserResource/Pages/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php index 59f144db..2e14c622 100644 --- a/app/Filament/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Resources/UserResource/Pages/EditUser.php @@ -4,12 +4,8 @@ namespace App\Filament\Resources\UserResource\Pages; -use App\Enums\UserRole; use App\Filament\Resources\UserResource; -use Filament\Forms\Components\Select; -use Filament\Forms\Components\TextInput; use Filament\Pages\Actions; -use Filament\Resources\Form; use Filament\Resources\Pages\EditRecord; class EditUser extends EditRecord @@ -22,43 +18,4 @@ protected function getActions(): array Actions\DeleteAction::make(), ]; } - - public function form(Form $form): Form - { - return $form - ->schema([ - TextInput::make('name') - ->label(__('user.name')) - ->required(), - TextInput::make('email') - ->label(__('user.email')) - ->email() - ->unique('users', 'email') - ->required(), - Select::make('role') - ->label(__('user.role')) - ->options( - collect( - UserRole::options() - )->only( - [ - UserRole::bb_admin->value, - UserRole::bb_manager->value, - UserRole::ngo_admin->value, - ] - )->toArray() - )->reactive() - ->required(), - Select::make('organization') - ->label(__('user.organization')) - ->relationship('organization', 'name') - ->hidden(function (callable $get) { - return $get('role') !== UserRole::ngo_admin->value; - }) - ->searchable() - ->preload() - ->required(), - - ]); - } } diff --git a/app/Forms/Components/UserLink.php b/app/Forms/Components/UserLink.php index 1dbfd1d5..9835d8b6 100644 --- a/app/Forms/Components/UserLink.php +++ b/app/Forms/Components/UserLink.php @@ -16,7 +16,7 @@ class UserLink extends Field public function getUsers(): Collection { return $this->getRecord() - ->getAdministrators() + ->users ->map(fn (User $user) => [ 'name' => $user->name, 'url' => UserResource::getUrl('view', $user), diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 46caacf7..43e690de 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -43,12 +43,13 @@ public function store(RegistrationRequest $request): RedirectResponse 'name' => $attributes['user']['name'], 'email' => $attributes['user']['email'], 'password' => Hash::make($attributes['user']['password']), - 'role' => UserRole::tryFrom($attributes['type']), + 'role' => $attributes['type'] === 'organization' ? UserRole::ADMIN : UserRole::USER, + ]); event(new Registered($user)); - if ($user->isNgoAdmin()) { + if ($user->isOrganizationAdmin()) { $attributes['ngo']['status'] = OrganizationStatus::draft; $organization = $user->organization()->create($attributes['ngo']); @@ -70,7 +71,7 @@ public function update(Request $request, $userId): RedirectResponse { try { $user = User::find($userId); - $user->source_of_information = $request->input('source_of_information'); + $user->referrer = $request->input('referrer'); $user->save(); return redirect()->back() diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php index d93a4a09..61234fd5 100644 --- a/app/Http/Controllers/Auth/VerifyEmailController.php +++ b/app/Http/Controllers/Auth/VerifyEmailController.php @@ -24,7 +24,8 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse if ($request->user()->markEmailAsVerified()) { event(new Verified($request->user())); - if ($request->user()->isNgoAdmin()) { + + if ($request->user()->isOrganizationAdmin()) { event(new SendOrganizationForApproval($request->user()->load('organization')->organization)); } } diff --git a/app/Http/Controllers/Dashboard/ProjectController.php b/app/Http/Controllers/Dashboard/ProjectController.php index 5f622c27..7c0e2243 100644 --- a/app/Http/Controllers/Dashboard/ProjectController.php +++ b/app/Http/Controllers/Dashboard/ProjectController.php @@ -39,17 +39,9 @@ public function index(Request $request) public function create() { - $counties = cache()->remember('counties', 60 * 60 * 24, function () { - return \App\Models\County::get(['name', 'id']); - }); - - $projectCategories = cache()->remember('projectCategories', 60 * 60 * 24, function () { - return ProjectCategory::get(['name', 'id']); - }); - return Inertia::render('AdminOng/Projects/AddProject', [ - 'counties' => $counties, - 'projectCategories' => $projectCategories, + 'counties' => $this->getCounties(), + 'projectCategories' => $this->getProjectCategories(), ]); } diff --git a/app/Http/Controllers/Dashboard/RegionalProjectController.php b/app/Http/Controllers/Dashboard/RegionalProjectController.php index 5051ddf3..60d60b77 100644 --- a/app/Http/Controllers/Dashboard/RegionalProjectController.php +++ b/app/Http/Controllers/Dashboard/RegionalProjectController.php @@ -6,7 +6,6 @@ use App\Http\Controllers\Controller; use App\Http\Requests\RegionalProject\StoreRequest; -use App\Models\ActivityDomain; use App\Models\County; use App\Models\ProjectCategory; use App\Models\RegionalProject; @@ -33,17 +32,9 @@ public function index() */ public function create() { - $counties = cache()->remember('counties', 60 * 60 * 24, function () { - return \App\Models\County::get(['name', 'id']); - }); - - $projectCategories = cache()->remember('activityDomains', 60 * 60 * 24, function () { - return ActivityDomain::get(['name', 'id']); - }); - return Inertia::render('AdminOng/Projects/AddRegionalProject', [ - 'counties' => $counties, - 'projectCategories' => $projectCategories, + 'counties' => $this->getCounties(), + 'projectCategories' => $this->getProjectCategories(), ]); } diff --git a/app/Http/Controllers/Dashboard/UserController.php b/app/Http/Controllers/Dashboard/UserController.php new file mode 100644 index 00000000..2be3a4e3 --- /dev/null +++ b/app/Http/Controllers/Dashboard/UserController.php @@ -0,0 +1,69 @@ + UserCollection::make( + QueryBuilder::for(User::class) + ->with('organization:id') + ->where('organization_id', auth()->user()->organization_id) + ->allowedSorts('name', 'email', 'created_at') + ->defaultSorts('created_at') + ->paginate(), + )->withPermissions(), + ]); + } + + public function store(Request $request): RedirectResponse + { + $this->authorize('create', User::class); + + $attributes = $request->validate([ + 'name' => ['required', 'string', 'max:200'], + 'email' => ['required', 'string', 'email', new UserDoesntBelongToAnOrganization], + ]); + + $user = User::firstOrNew( + ['email' => $attributes['email']], + [ + 'name' => $attributes['name'], + 'created_by' => auth()->user()->id, + ] + ); + + $user->organization() + ->associate(auth()->user()->organization) + ->save(); + + return redirect()->back() + ->with('success', __('user.messages.created')); + } + + public function destroy(Request $request, User $user): RedirectResponse + { + $this->authorize('delete', $user); + + $user->organization() + ->dissociate() + ->save(); + + return redirect()->back() + ->with('success', __('user.messages.deleted')); + } +} diff --git a/app/Http/Controllers/Dashboard/VolunteerController.php b/app/Http/Controllers/Dashboard/VolunteerController.php index 0404cdc8..0a409bdd 100644 --- a/app/Http/Controllers/Dashboard/VolunteerController.php +++ b/app/Http/Controllers/Dashboard/VolunteerController.php @@ -42,7 +42,7 @@ public function approve(Request $request, VolunteerRequest $volunteerRequest) $volunteerRequest->markAsApproved(); return redirect()->back() - ->with('success', 'Voluntarul a fost aprobat cu succes'); + ->with('success', __('volunteer.messages.approved')); } public function reject(Request $request, VolunteerRequest $volunteerRequest) @@ -50,7 +50,7 @@ public function reject(Request $request, VolunteerRequest $volunteerRequest) $volunteerRequest->markAsRejected(); return redirect()->back() - ->with('success', 'Voluntarul a fost respins cu succes'); + ->with('success', __('volunteer.messages.rejected')); } public function delete(Request $request, VolunteerRequest $volunteerRequest) @@ -58,6 +58,6 @@ public function delete(Request $request, VolunteerRequest $volunteerRequest) $volunteerRequest->delete(); return redirect()->back() - ->with('success', 'Voluntarul a fost sters cu succes'); + ->with('success', __('volunteer.messages.deleted')); } } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index a6d1de36..555e6c3e 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -12,7 +12,7 @@ class DashboardController extends Controller { public function __invoke(): Response|RedirectResponse { - if (auth()->user()->isNgoAdmin()) { + if (auth()->user()->belongsToOrganization()) { return redirect()->route('dashboard.organization.edit'); } if (auth()->user()->isDonor()) { diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index b1490e48..eac22582 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -28,7 +28,10 @@ public function share(Request $request): array 'auth' => [ 'user' => $request->user(), ], - 'locales' => locales(), + 'locales' => fn () => [ + 'available' => locales(), + 'current' => app()->getLocale(), + ], ]); } diff --git a/app/Http/Requests/RegionalProject/StoreRequest.php b/app/Http/Requests/RegionalProject/StoreRequest.php index 4825af96..d740318e 100644 --- a/app/Http/Requests/RegionalProject/StoreRequest.php +++ b/app/Http/Requests/RegionalProject/StoreRequest.php @@ -14,7 +14,7 @@ class StoreRequest extends FormRequest */ public function authorize(): bool { - return auth()->user()->isNgoAdmin(); + return auth()->user()->isOrganizationAdmin(); } /** diff --git a/app/Http/Requests/RegistrationRequest.php b/app/Http/Requests/RegistrationRequest.php index b21a45ed..b33226f0 100644 --- a/app/Http/Requests/RegistrationRequest.php +++ b/app/Http/Requests/RegistrationRequest.php @@ -24,7 +24,7 @@ public function rules(): array 'user.password' => ['string', 'required', 'confirmed'], ]; - if ($this->type === 'ngo-admin') { + if ($this->type === 'organization') { $rules = array_merge($rules, [ 'ngo' => ['array', 'required'], 'ngo.name' => ['string', 'required'], diff --git a/app/Http/Resources/Collections/ResourceCollection.php b/app/Http/Resources/Collections/ResourceCollection.php index a02f73d3..5f1aec05 100644 --- a/app/Http/Resources/Collections/ResourceCollection.php +++ b/app/Http/Resources/Collections/ResourceCollection.php @@ -10,16 +10,18 @@ abstract class ResourceCollection extends BaseCollection { - protected array $columns = []; + public string $model; + + protected array $appends = []; public function toArray(Request $request): array { - return [ + return array_merge([ 'columns' => $this->getColumns(), 'filter' => $request->query('filter'), 'sort' => $this->getSort($request), 'data' => $this->collection, - ]; + ], $this->appends); } abstract protected function getColumns(): array; @@ -46,4 +48,18 @@ protected function getSort(Request $request): array 'direction' => $direction, ]; } + + public function withPermissions(): static + { + $this->collects::withPermissions(); + + if (! \is_null($this->model)) { + $this->appends['can'] = collect(['viewAny', 'create']) + ->mapWithKeys(fn (string $ability) => [ + $ability => auth()->user()->can($ability, $this->model), + ]); + } + + return $this; + } } diff --git a/app/Http/Resources/Collections/UserCollection.php b/app/Http/Resources/Collections/UserCollection.php new file mode 100644 index 00000000..b20e9724 --- /dev/null +++ b/app/Http/Resources/Collections/UserCollection.php @@ -0,0 +1,38 @@ +label(__('user.column.name')) + ->sortable(), + + TableColumn::make('email') + ->label(__('user.column.email')), + + TableColumn::make('role') + ->label(__('user.column.role')), + ]; + } + + protected function permissions(): array + { + return [ + 'create' => auth()->user()->can('create', User::class), + ]; + } +} diff --git a/app/Http/Resources/Resource.php b/app/Http/Resources/Resource.php new file mode 100644 index 00000000..eaf70f0c --- /dev/null +++ b/app/Http/Resources/Resource.php @@ -0,0 +1,49 @@ +mapWithKeys(fn (string $ability) => [ + $ability => auth()->user()->can($ability, $this->resource), + ]) + ->merge($this->additionalPermissions()); + } + + return $data; + } + + protected function additionalPermissions(): array + { + return []; + } +} diff --git a/app/Http/Resources/TicketMessageResource.php b/app/Http/Resources/TicketMessageResource.php index 4d03e64f..be87f441 100644 --- a/app/Http/Resources/TicketMessageResource.php +++ b/app/Http/Resources/TicketMessageResource.php @@ -21,7 +21,7 @@ public function toArray(Request $request): array 'user' => [ 'id' => $this->user->id, 'name' => $this->user->name, - 'is_bb_admin' => $this->user->isBbAdmin(), + 'is_superuser' => $this->user->isSuperUser(), ], ]; } diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php new file mode 100644 index 00000000..e306ee75 --- /dev/null +++ b/app/Http/Resources/UserResource.php @@ -0,0 +1,23 @@ + $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'is_admin' => $this->isOrganizationAdmin(), + 'role' => $this->isOrganizationAdmin() + ? __('user.roles.admin') + : __('user.roles.manager'), + ]; + } +} diff --git a/app/Listeners/Organization/SendOrganizationForApprovalListener.php b/app/Listeners/Organization/SendOrganizationForApprovalListener.php index 10282089..2c5a7fc9 100644 --- a/app/Listeners/Organization/SendOrganizationForApprovalListener.php +++ b/app/Listeners/Organization/SendOrganizationForApprovalListener.php @@ -18,14 +18,14 @@ public function handle(SendOrganizationForApproval $event): void Notification::send( User::query() - ->onlyBBAdmins() + ->onlySuperUsers() ->get(), new OrganizationCreatedAdmin($event->organization) ); Notification::send( User::query() - ->onlyNGOAdmins($event->organization) + ->whereBelongsToOrganization($event->organization) ->get(), new OrganizationCreated($event->organization) ); diff --git a/app/Listeners/Ticket/SendTicketCreatedNotification.php b/app/Listeners/Ticket/SendTicketCreatedNotification.php index f3ea4ae0..1498a0ef 100644 --- a/app/Listeners/Ticket/SendTicketCreatedNotification.php +++ b/app/Listeners/Ticket/SendTicketCreatedNotification.php @@ -19,14 +19,14 @@ public function handle(TicketCreated $event): void { Notification::send( User::query() - ->onlyBBAdmins() + ->onlySuperUsers() ->get(), new Admin\TicketCreatedNotification($event->ticket) ); Notification::send( User::query() - ->onlyNGOAdmins($event->ticket->organization) + ->whereBelongsToOrganization($event->ticket->organization) ->get(), new Ngo\TicketCreatedNotification($event->ticket) ); diff --git a/app/Listeners/Ticket/SendTicketReplyReceivedNotification.php b/app/Listeners/Ticket/SendTicketReplyReceivedNotification.php index c2b684b9..99c850b3 100644 --- a/app/Listeners/Ticket/SendTicketReplyReceivedNotification.php +++ b/app/Listeners/Ticket/SendTicketReplyReceivedNotification.php @@ -19,14 +19,14 @@ public function handle(TicketReplyReceived $event): void { Notification::send( User::query() - ->onlyBBAdmins() + ->onlySuperUsers() ->get(), new Admin\TicketReceivedReplyNotification($event->message) ); Notification::send( User::query() - ->onlyNGOAdmins($event->message->ticket->organization) + ->whereBelongsToOrganization($event->message->ticket->organization) ->get(), new Ngo\TicketReceivedReplyNotification($event->message) ); diff --git a/app/Listeners/Ticket/SendTicketStatusChangedNotification.php b/app/Listeners/Ticket/SendTicketStatusChangedNotification.php index 9074254a..5c9edf01 100644 --- a/app/Listeners/Ticket/SendTicketStatusChangedNotification.php +++ b/app/Listeners/Ticket/SendTicketStatusChangedNotification.php @@ -25,14 +25,14 @@ public function handle(TicketUpdated $event): void Notification::send( User::query() - ->onlyBBAdmins() + ->onlySuperUsers() ->get(), new Admin\TicketStatusChangedNotification($event->ticket, $ticketIsOpen) ); Notification::send( User::query() - ->onlyNGOAdmins($event->ticket->organization) + ->whereBelongsToOrganization($event->ticket->organization) ->get(), new Ngo\TicketStatusChangedNotification($event->ticket, $ticketIsOpen) ); diff --git a/app/Listeners/User/DeleteOrganization.php b/app/Listeners/User/DeleteOrganization.php index 8fdcd629..757c7797 100644 --- a/app/Listeners/User/DeleteOrganization.php +++ b/app/Listeners/User/DeleteOrganization.php @@ -15,7 +15,7 @@ class DeleteOrganization public function handle(UserDeleting $event): void { if ( - ! $event->user->isNgoAdmin() || + ! $event->user->isOrganizationAdmin() || $event->user->organization->users()->count() > 1 ) { return; diff --git a/app/Models/Organization.php b/app/Models/Organization.php index ac86977c..b93d76da 100644 --- a/app/Models/Organization.php +++ b/app/Models/Organization.php @@ -17,7 +17,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphMany; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Vite; use Spatie\Activitylog\LogOptions; use Spatie\Image\Manipulations; @@ -189,13 +188,6 @@ public function scopeWhereDoesntHaveDonations(Builder $query): Builder return $query->whereDoesntHave('projects.donations'); } - public function getAdministrators(): Collection - { - return $this->users() - ->onlyNGOAdmins() - ->get(); - } - public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() diff --git a/app/Models/User.php b/app/Models/User.php index 93fba962..7253e93d 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,27 +4,25 @@ namespace App\Models; +use App\Concerns\BelongsToOrganization; +use App\Concerns\HasRole; use App\Concerns\MustSetInitialPassword; -use App\Enums\UserRole; use App\Events\User\UserDeleting; -use App\Traits\HasRole; use Filament\Models\Contracts\FilamentUser; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Prunable; -use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable implements FilamentUser, MustVerifyEmail { - use HasApiTokens; + use BelongsToOrganization; use HasFactory; use Notifiable; use HasRole; @@ -42,8 +40,7 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail 'password', 'role', 'phone', - 'organization_id', - 'source_of_information', + 'referrer', 'created_by', ]; @@ -77,24 +74,14 @@ public function badges(): BelongsToMany ->orderByPivot('allocated_at', 'desc'); } - public function organization(): BelongsTo - { - return $this->belongsTo(Organization::class); - } - public function donations(): HasMany { - return $this->hasMany(Donation::class); - } - - public function currentOrganization(): Organization - { - return $this->organization()->first(); + return $this->hasMany(Donation::class); } public function canAccessFilament(): bool { - return $this->isBBAdmin() || $this->isBBManager(); + return $this->isSuperUser(); } public function getFilamentName(): string @@ -117,7 +104,7 @@ public function prunable(): Builder return static::query() ->with('organization:id,name') ->where('created_at', '<=', now()->subHours(48)) - ->whereNotIn('role', [UserRole::bb_admin, UserRole::bb_manager]) + ->withoutSuperUsers() ->whereNull('email_verified_at') ->whereNull('password_set_at'); } diff --git a/app/Policies/OrganizationPolicy.php b/app/Policies/OrganizationPolicy.php index b6357ed2..e558bb5e 100644 --- a/app/Policies/OrganizationPolicy.php +++ b/app/Policies/OrganizationPolicy.php @@ -4,7 +4,6 @@ namespace App\Policies; -use App\Enums\UserRole; use App\Models\Organization; use App\Models\User; @@ -46,15 +45,7 @@ public function update(User $user, Organization $organization): bool * An organization can be updated only by BB Admins, BB Managers * and NGO Admins that belong to the organization. */ - if ( - (UserRole::bb_admin === $user->role) || - (UserRole::bb_manager === $user->role) || - ((UserRole::ngo_admin === $user->role) && ($user->organization_id === $organization->id)) - ) { - return true; - } else { - return false; - } + return $user->isSuperUser() || $user->isOrganizationAdmin($organization); } /** @@ -66,15 +57,7 @@ public function delete(User $user, Organization $organization): bool * An organization can be deleted only by BB Admins, BB Managers * and NGO Admins that belong to the organization. */ - if ( - (UserRole::bb_admin === $user->role) || - (UserRole::bb_manager === $user->role) || - ((UserRole::ngo_admin === $user->role) && ($user->organization_id === $organization->id)) - ) { - return true; - } else { - return false; - } + return $user->isSuperUser() || $user->isOrganizationAdmin($organization); } /** @@ -85,11 +68,7 @@ public function restore(User $user, Organization $organization): bool /* * An organization can be restored only by BB Admins and BB Managers. */ - if ((UserRole::bb_admin === $user->role) || (UserRole::bb_manager === $user->role)) { - return true; - } else { - return false; - } + return $user->isSuperUser(); } /** diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php index f6e5359e..02de06c7 100644 --- a/app/Policies/ProjectPolicy.php +++ b/app/Policies/ProjectPolicy.php @@ -4,8 +4,6 @@ namespace App\Policies; -use App\Enums\UserRole; -use App\Models\Organization; use App\Models\Project; use App\Models\User; @@ -18,20 +16,11 @@ public function viewAny(User $user): bool public function view(User $user, Project $project): bool { - if ($user->organization_id !== $project->organization_id) { - return false; - } - - /* Anyone can see the details of an organization. */ - return true; + return $user->belongsToOrganization($project->organization); } public function editAsNgo(User $user, Project $project): bool { - if ($user->organization_id !== $project->organization_id && $user->role !== UserRole::ngo_admin) { - return false; - } - - return true; + return $user->belongsToOrganization($project->organization); } } diff --git a/app/Policies/TicketPolicy.php b/app/Policies/TicketPolicy.php index 53c59ac3..bbb8a627 100644 --- a/app/Policies/TicketPolicy.php +++ b/app/Policies/TicketPolicy.php @@ -16,17 +16,17 @@ public function viewAny(User $user): bool public function view(User $user, Ticket $ticket): bool { - return $user->isBbAdmin() || $user->organization->is($ticket->organization); + return $user->isSuperUser() || $user->organization->is($ticket->organization); } public function create(User $user): bool { - return $user->isNgoAdmin(); + return $user->belongsToOrganization(); } public function update(User $user, Ticket $ticket): bool { - return $user->isBbAdmin() || ($user->isNgoAdmin() && $user->organization->is($ticket->organization)); + return $user->isSuperUser() || $user->belongsToOrganization($ticket->organization); } public function reply(User $user, Ticket $ticket): bool diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php new file mode 100644 index 00000000..99b656f3 --- /dev/null +++ b/app/Policies/UserPolicy.php @@ -0,0 +1,45 @@ +isSuperUser() || $user->belongstoOrganization($model->organization); + } + + public function create(User $user): bool + { + return $user->isSuperAdmin() || $user->isOrganizationAdmin(); + } + + public function update(User $user, User $model): bool + { + return $user->isSuperAdmin() || $user->isOrganizationAdmin($model->organization); + } + + public function delete(User $user, User $model): bool + { + return $user->isSuperAdmin() || $user->isOrganizationAdmin($model->organization); + } + + public function restore(User $user, User $model): bool + { + return $user->isSuperAdmin() || $user->isOrganizationAdmin($model->organization); + } + + public function forceDelete(User $user, User $model): bool + { + return $user->isSuperAdmin() || $user->isOrganizationAdmin($model->organization); + } +} diff --git a/app/Rules/UserDoesntBelongToAnOrganization.php b/app/Rules/UserDoesntBelongToAnOrganization.php new file mode 100644 index 00000000..3e22e176 --- /dev/null +++ b/app/Rules/UserDoesntBelongToAnOrganization.php @@ -0,0 +1,29 @@ +where($attribute, $value) + ->whereNotNull('organization_id') + ->exists(); + + if ($exists) { + $fail(__('validation.user_belongs_to_organization')); + } + } +} diff --git a/app/Traits/HasRole.php b/app/Traits/HasRole.php deleted file mode 100644 index 2abac8be..00000000 --- a/app/Traits/HasRole.php +++ /dev/null @@ -1,61 +0,0 @@ -casts['role'] = UserRole::class; - } - - public function hasRole(string $role): bool - { - return $this->role === UserRole::tryFrom($role); - } - - public function isDonor(): bool - { - return $this->role === UserRole::donor; - } - - public function isNgoAdmin(): bool - { - return $this->role === UserRole::ngo_admin; - } - - public function isBbManager(): bool - { - return $this->role === UserRole::bb_manager; - } - - public function isBbAdmin(): bool - { - return $this->role === UserRole::bb_admin; - } - - public function scopeRole(Builder $query, array|string|Collection|UserRole $roles): Builder - { - return $query->whereIn('role', collect($roles)); - } - - public function scopeOnlyBBAdmins(Builder $query): Builder - { - return $query->role(UserRole::bb_admin); - } - - public function scopeOnlyNGOAdmins(Builder $query, ?Organization $organization = null): Builder - { - return $query->role(UserRole::ngo_admin) - ->when($organization !== null, function (Builder $query) use ($organization) { - return $query->where('organization_id', $organization->id); - }); - } -} diff --git a/app/helpers.php b/app/helpers.php index a7616fd9..74668a09 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -4,16 +4,20 @@ use Illuminate\Support\Collection; -function locales(): Collection -{ - return collect(config('filament-spatie-laravel-translatable-plugin.default_locales')); +if (! function_exists('locales')) { + function locales(): Collection + { + return collect(config('filament-spatie-laravel-translatable-plugin.default_locales')); + } } -function money_format($number, $decimals = 0): string -{ - $formatter = new NumberFormatter(app()->getLocale(), NumberFormatter::CURRENCY); +if (! function_exists('money_format')) { + function money_format($number, $decimals = 0): string + { + $formatter = new NumberFormatter(app()->getLocale(), NumberFormatter::CURRENCY); - $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $decimals); + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $decimals); - return $formatter->formatCurrency((float) $number, 'RON'); + return $formatter->formatCurrency((float) $number, 'RON'); + } } diff --git a/database/factories/OrganizationFactory.php b/database/factories/OrganizationFactory.php index a1ab9a8a..37106cfc 100644 --- a/database/factories/OrganizationFactory.php +++ b/database/factories/OrganizationFactory.php @@ -15,6 +15,7 @@ use App\Models\User; use App\Models\Volunteer; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Database\Eloquent\Factories\Sequence; /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Organization> @@ -87,12 +88,23 @@ public function configure(): static $admin = User::factory() ->for($organization) - ->ngoAdmin() + ->organizationAdmin() ->state([ 'email' => "admin-{$organization->id}@example.com", ]) ->create(); + $managers = User::factory() + ->count(3) + ->for($organization) + ->ngoManager() + ->state(new Sequence( + ['email' => "manager-{$organization->id}@example.com"], + ['email' => "manager-{$organization->id}@example.org"], + ['email' => "manager-{$organization->id}@example.net"], + )) + ->create(); + if ($organization->isPending()) { return; } diff --git a/database/factories/TicketFactory.php b/database/factories/TicketFactory.php index 2b16142c..9d58c238 100644 --- a/database/factories/TicketFactory.php +++ b/database/factories/TicketFactory.php @@ -4,7 +4,6 @@ namespace Database\Factories; -use App\Enums\UserRole; use App\Models\Organization; use App\Models\Ticket; use App\Models\TicketMessage; @@ -35,23 +34,23 @@ public function definition(): array public function configure(): static { return $this->afterCreating(function (Ticket $ticket) { - $ngoAdmin = $ticket->organization - ->getAdministrators() + $organizationAdmin = $ticket->organization + ->users ->first(); - $bbAdmin = User::query() - ->role(UserRole::bb_admin) + $superAdmin = User::query() + ->onlySuperAdmins() ->first(); TicketMessage::factory() ->for($ticket) - ->recycle($ngoAdmin) + ->recycle($organizationAdmin) ->count(fake()->randomDigitNotNull()) ->create(); TicketMessage::factory() ->for($ticket) - ->recycle($bbAdmin) + ->recycle($superAdmin) ->count(fake()->randomDigitNotNull()) ->create(); }); diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 31b0c713..80a53d0f 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -4,6 +4,7 @@ namespace Database\Factories; +use App\Enums\UserRole; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; @@ -44,37 +45,47 @@ public function unverified(): static public function donor(): static { return $this->state(fn (array $attributes) => [ - 'role' => 'donor', + 'role' => UserRole::donor, ]); } /** * Make a NGO Admin user. */ - public function ngoAdmin(): static + public function organizationAdmin(): static { return $this->state(fn (array $attributes) => [ - 'role' => 'ngo-admin', + 'role' => UserRole::ADMIN, + ]); + } + + /** + * Make a NGO Manager user. + */ + public function ngoManager(): static + { + return $this->state(fn (array $attributes) => [ + // 'role' => UserRole::ngo_manager, ]); } /** * Make a BB Manager user. */ - public function bbManager(): static + public function superManager(): static { return $this->state(fn (array $attributes) => [ - 'role' => 'bb-manager', + 'role' => UserRole::SUPERMANAGER, ]); } /** * Make a BB Admin user. */ - public function bbAdmin(): static + public function superAdmin(): static { return $this->state(fn (array $attributes) => [ - 'role' => 'bb-admin', + 'role' => UserRole::SUPERADMIN, ]); } } diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index 2bec77ad..28dd810c 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -17,10 +17,14 @@ public function up(): void $table->id(); $table->string('name'); $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); + $table->string('phone')->nullable(); $table->string('password'); + $table->string('role')->nullable(); $table->rememberToken(); $table->timestamps(); + $table->timestamp('email_verified_at')->nullable(); + $table->timestamp('password_set_at')->nullable(); + $table->string('referrer')->nullable(); }); } diff --git a/database/migrations/2023_05_05_142245_update_users.php b/database/migrations/2023_05_05_142245_add_organization_column_to_users_table.php similarity index 62% rename from database/migrations/2023_05_05_142245_update_users.php rename to database/migrations/2023_05_05_142245_add_organization_column_to_users_table.php index 93656ec7..4b7de09e 100644 --- a/database/migrations/2023_05_05_142245_update_users.php +++ b/database/migrations/2023_05_05_142245_add_organization_column_to_users_table.php @@ -15,11 +15,10 @@ public function up(): void { Schema::table('users', function (Blueprint $table) { - $table->enum('role', ['donor', 'ngo-admin', 'bb-manager', 'bb-admin']); - $table->string('phone')->nullable(); - $table->string('source_of_information')->nullable(); - $table->timestamp('password_set_at')->nullable(); - $table->foreignIdFor(Organization::class)->nullable()->constrained()->onDelete('cascade'); + $table->foreignIdFor(Organization::class) + ->nullable() + ->constrained() + ->cascadeOnDelete(); }); } @@ -31,7 +30,6 @@ public function down(): void Schema::table('users', function (Blueprint $table) { $table->dropForeign(['organization_id']); $table->dropColumn('organization_id'); - $table->dropColumn('role'); }); } }; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 29674611..a507a846 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -30,13 +30,13 @@ public function run(): void Mail::fake(); - User::factory(['email' => 'bb-admin@example.com']) - ->bbAdmin() + User::factory(['email' => 'superadmin@example.com']) + ->superAdmin() ->create(); - User::factory(['email' => 'bb-manager@example.com']) + User::factory(['email' => 'supermanager@example.com']) ->count(1) - ->bbManager() + ->superManager() ->create(); User::factory() diff --git a/lang/ro.json b/lang/ro.json index af499e6a..3a800202 100644 --- a/lang/ro.json +++ b/lang/ro.json @@ -172,8 +172,8 @@ "organization_logo_label": "Logo-ul organizației", "change_image_label": "Schimbă imaginea", "delete_image_label": "Sterge imagine", - "active": "Activ", - "inactive": "Inactiv", + "active": "Deschis", + "inactive": "Închis", "become_volunter": "Fii voluntar", "target_amount": "Suma target", "share_project": "Distribuie acest proiect pe", @@ -224,12 +224,10 @@ "back": "Inapoi", "open_tickets": "Tichete deschise", "closed_tickets": "Tichete închise", - "close": "Inchide", + "close": "Închide", "ticket": "Ticket", "subject": "Subiect", "date": "Data", - "close_ticket": "Închide ticket", - "reopen_ticket": "Redeschide ticket", "answer": "Raspunde", "message": "Mesaj", "add_ticket": "Deschide un tichet nou", @@ -391,9 +389,6 @@ "ticket_subject": "Subiect", "ticket_created_at": "Data", "ticket_closed_at": "Închis la data", - "ticket_reply_header": "Trimite un răspuns", - "confirm_close_ticket": "Ești sigur că vrei să închizi acest tichet?", - "confirm_reopen_ticket": "Ești sigur că vrei să redeschizi acest tichet?", "add_regional_project": "Adaugă un proiect regional", "field_has_pending_changes": "Acest câmp are modificări neaprobare. Dacă il editezi, modificările vor fi pierdute.", "verify_email_title": "Îți mulțumim că te-ai înregistrat pe platforma Bursa Binelui !", @@ -405,5 +400,36 @@ "sort.end_date": "Data sfârșit proiect", "sort.target": "Target", "sort.donations_total": "Suma donată", - "sort.donations_count": "Număr donații" + "sort.donations_count": "Număr donații", + + "users.add_manager": "Adaugă manager", + "users.remove.title": "Șterge utilizator", + "users.remove.content": "Ești sigur că vrei să ștergi utilizatorul \":name\"?", + + "volunteers.accept.trigger": "Acceptă", + "volunteers.accept.title": "Acceptă voluntar", + "volunteers.accept.content": "Ești sigur că vrei să accepți voluntarul \":name\"?", + + "volunteers.reject.trigger": "Respinge", + "volunteers.reject.title": "Respinge voluntar", + "volunteers.reject.content": "Ești sigur că vrei să respingi voluntarul \":name\"?", + + "volunteers.delete.trigger": "Șterge", + "volunteers.delete.title": "Șterge voluntar", + "volunteers.delete.content": "Ești sigur că vrei să ștergi voluntarul \":name\"?", + + "tickets.reply.trigger": "Răspunde", + "tickets.reply.title": "Trimite un răspuns", + + "tickets.close.title": "Închide ticket", + "tickets.close.content": "Ești sigur că vrei să închizi acest tichet?", + + "tickets.reopen.title": "Redeschide ticket", + "tickets.reopen.content": "Ești sigur că vrei să redeschizi acest tichet?", + + "navigation.users": "Utilizatori", + "navigation.volunteers": "Voluntari", + + "locales.ro": "Română", + "locales.en": "English" } diff --git a/lang/ro/user.php b/lang/ro/user.php index d9863e9e..a6a7275e 100644 --- a/lang/ro/user.php +++ b/lang/ro/user.php @@ -4,14 +4,18 @@ return [ 'roles' => [ + 'superadmin' => 'Administrator Bursa Binelui', + 'supermanager' => 'Manager Bursa Binelui', + 'admin' => 'Administrator ONG', + 'manager' => 'Manager ONG', + 'donor' => 'Donator/Voluntar', - 'ngo-admin' => 'Administrator ONG', - 'bb-manager' => 'Manager Bursa Binelui', - 'bb-admin' => 'Administrator Bursa Binelui', ], + 'label' => [ 'singular' => 'Utilizator', - 'plural' => 'Utilizatori', ], + 'plural' => 'Utilizatori', + ], 'labels' => [ 'general_data' => 'Date generale', 'volunteer_for_organization' => 'Voluntar in organizatie', @@ -32,6 +36,8 @@ 'role' => 'Rol', 'organization' => 'Organizație', 'messages' => [ + 'created' => 'Utilizatorul a fost adăugat.', + 'deleted' => 'Utilizatorul a fost șters.', 'set_initial_password_success' => 'Parola a fost setată cu succes!', ], 'filters' => [ @@ -40,4 +46,15 @@ 'has_donations' => 'Are donații', 'is_volunteer' => 'Este voluntar', ], + + 'column' => [ + 'name' => 'Nume', + 'email' => 'Email', + 'role' => 'Rol', + ], + + 'action' => [ + 'attach' => 'Alocă utilizator', + 'detach' => 'Elimină', + ], ]; diff --git a/lang/ro/volunteer.php b/lang/ro/volunteer.php index 007d23ee..f58e3786 100644 --- a/lang/ro/volunteer.php +++ b/lang/ro/volunteer.php @@ -18,10 +18,16 @@ ], 'statuses' => [ - 'pending' => 'In asteptare', + 'pending' => 'În așteptare', 'approved' => 'Aprobat', 'rejected' => 'Respins', ], 'model' => 'Proiect', + + 'messages' => [ + 'approved' => 'Voluntarul a fost aprobat.', + 'rejected' => 'Voluntarul a fost respins.', + 'deleted' => 'Voluntarul a fost șters.', + ], ]; diff --git a/public/images/svg/default_avatar.svg b/public/images/svg/default_avatar.svg deleted file mode 100644 index 81b71c36..00000000 --- a/public/images/svg/default_avatar.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/resources/js/Components/Footer.vue b/resources/js/Components/Footer.vue index 942a31fa..8098e6ae 100644 --- a/resources/js/Components/Footer.vue +++ b/resources/js/Components/Footer.vue @@ -116,15 +116,7 @@ />
- - {{ $t('subscribe') }} - +
diff --git a/resources/js/Components/LanguageSwitcher.vue b/resources/js/Components/LanguageSwitcher.vue new file mode 100644 index 00000000..8ed0d77a --- /dev/null +++ b/resources/js/Components/LanguageSwitcher.vue @@ -0,0 +1,64 @@ + + + diff --git a/resources/js/Components/Modal.vue b/resources/js/Components/Modal.vue deleted file mode 100644 index bb5bb382..00000000 --- a/resources/js/Components/Modal.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - diff --git a/resources/js/Components/Navbar.vue b/resources/js/Components/Navbar.vue index 3bb84eb0..ba3382bb 100644 --- a/resources/js/Components/Navbar.vue +++ b/resources/js/Components/Navbar.vue @@ -1,137 +1,121 @@ diff --git a/resources/js/Components/links/NavLink.vue b/resources/js/Components/links/NavLink.vue index d904ca0d..b2b0915a 100644 --- a/resources/js/Components/links/NavLink.vue +++ b/resources/js/Components/links/NavLink.vue @@ -1,32 +1,24 @@ diff --git a/resources/js/Components/modals/ChampionshipModal.vue b/resources/js/Components/modals/ChampionshipModal.vue index 2764d8d8..b82ea66c 100644 --- a/resources/js/Components/modals/ChampionshipModal.vue +++ b/resources/js/Components/modals/ChampionshipModal.vue @@ -1,15 +1,6 @@ diff --git a/resources/js/Pages/Auth/Registration/Success.vue b/resources/js/Pages/Auth/Registration/Success.vue index fa39c683..e88a1e53 100644 --- a/resources/js/Pages/Auth/Registration/Success.vue +++ b/resources/js/Pages/Auth/Registration/Success.vue @@ -12,7 +12,7 @@ :id="option.value" name="info" type="radio" - v-model="social.source_of_information" + v-model="social.referrer" :value="option.value" class="w-4 h-4 border-gray-300 text-primary-500 focus:ring-primary-500" /> @@ -24,7 +24,7 @@ - - {{ $t('send') }} - + @@ -100,5 +91,5 @@ } }; - const update = () => (props.social.source_of_information = other.value); + const update = () => (props.social.referrer = other.value); diff --git a/resources/js/Pages/Auth/ResetPassword.vue b/resources/js/Pages/Auth/ResetPassword.vue index 9fa29085..313eb8b6 100644 --- a/resources/js/Pages/Auth/ResetPassword.vue +++ b/resources/js/Pages/Auth/ResetPassword.vue @@ -23,15 +23,7 @@
- - {{ $t('save') }} - +
diff --git a/resources/js/Pages/Auth/Welcome.vue b/resources/js/Pages/Auth/Welcome.vue index 7994d568..a0701dbc 100644 --- a/resources/js/Pages/Auth/Welcome.vue +++ b/resources/js/Pages/Auth/Welcome.vue @@ -23,15 +23,7 @@
- - {{ $t('save') }} - +
diff --git a/resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue b/resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue index 52ba3c29..493965dd 100644 --- a/resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue +++ b/resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue @@ -5,7 +5,6 @@
-
- - + {{ $t('cancel') }} - - {{ $t('save') }} - +

{{ $t('saved') }}

diff --git a/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue b/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue index 88836de0..f32e710c 100644 --- a/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue +++ b/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue @@ -1,13 +1,11 @@