diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 877c958a..e1e6b4f8 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -9,22 +9,10 @@ 'align_multiline_comment' => true, 'array_indentation' => true, 'array_syntax' => ['syntax' => 'short'], - - 'binary_operator_spaces' => [ - 'default' => 'single_space', - 'operators' => ['=>' => null], - ], 'blank_line_after_namespace' => true, 'blank_line_after_opening_tag' => true, - 'blank_line_before_statement' => [ - 'statements' => ['return'], - ], 'braces' => true, 'cast_spaces' => true, - 'class_attributes_separation' => [ - 'elements' => ['method'], - ], - 'class_definition' => true, 'concat_space' => [ 'spacing' => 'one', ], @@ -96,8 +84,7 @@ 'no_singleline_whitespace_before_semicolons' => true, 'no_spaces_after_function_name' => true, 'no_spaces_inside_parenthesis' => true, - 'no_trailing_comma_in_list_call' => true, - 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_comma_in_singleline' => true, 'no_trailing_whitespace' => true, 'no_trailing_whitespace_in_comment' => true, 'no_unreachable_default_argument_value' => true, @@ -155,7 +142,7 @@ // php-cs-fixer 3: Changed options 'binary_operator_spaces' => [ 'default' => 'single_space', - 'operators' => ['=>' => null], + 'operators' => [], ], 'blank_line_before_statement' => [ 'statements' => ['return'], @@ -194,6 +181,7 @@ __DIR__ . '/app', __DIR__ . '/config', __DIR__ . '/database', + __DIR__ . '/lang', __DIR__ . '/resources', __DIR__ . '/routes', __DIR__ . '/tests', diff --git a/app/Concerns/LogsActivity.php b/app/Concerns/LogsActivity.php new file mode 100644 index 00000000..d7cd95ba --- /dev/null +++ b/app/Concerns/LogsActivity.php @@ -0,0 +1,95 @@ +each(function ($eventName) { + if ($eventName === 'updated') { + static::updating(function (Model $model) { + $oldValues = (new static())->setRawAttributes($model->getRawOriginal()); + $model->oldAttributes = static::logChanges($oldValues); + }); + } + + static::$eventName(function (Model $model) use ($eventName) { + $model->activitylogOptions = $model->getActivitylogOptions(); + + if (! $model->shouldLogEvent($eventName)) { + return; + } + + $changes = $model->attributeValuesToBeLogged($eventName); + + $description = $model->getDescriptionForEvent($eventName); + + $logName = $model->getLogNameToUse(); + + // Submitting empty description will cause place holder replacer to fail. + if ($description == '') { + return; + } + + if ($model->isLogEmpty($changes) && ! $model->activitylogOptions->submitEmptyLogs) { + return; + } + + // User can define a custom pipelines to mutate, add or remove from changes + // each pipe receives the event carrier bag with changes and the model in + // question every pipe should manipulate new and old attributes. + $event = app(Pipeline::class) + ->send(new EventLogBag($eventName, $model, $changes, $model->activitylogOptions)) + ->through(static::$changesPipes) + ->thenReturn(); + + if ($eventName === 'updated' || $eventName === 'updating') { + foreach ($event->changes['attributes'] as $key => $value) { + static::actuallyLog($logName, $eventName, $model, [ + $key => [ + 'old' => $event->changes['old'][$key], + 'new' => $value, + ], + ], $description); + } + } else { + static::actuallyLog($logName, $eventName, $model, $event->changes, $description); + } + + // Reset log options so the model can be serialized. + $model->activitylogOptions = null; + }); + }); + } + + protected static function actuallyLog(?string $logName, string $eventName, Model $model, array $properties, string $description): void + { + // Actual logging + $logger = app(ActivityLogger::class) + ->useLog($logName) + ->event($eventName) + ->performedOn($model) + ->withProperties($properties); + + if (method_exists($model, 'tapActivity')) { + $logger->tap([$model, 'tapActivity'], $eventName); + } + + $logger->log($description); + } +} diff --git a/app/Concerns/LogsActivityForApproval.php b/app/Concerns/LogsActivityForApproval.php new file mode 100644 index 00000000..87c49eb5 --- /dev/null +++ b/app/Concerns/LogsActivityForApproval.php @@ -0,0 +1,89 @@ +getDirty()) + ->map(fn ($value, $key) => [ + 'old' => $this->getOriginal($key), + 'new' => $value, + ]); + + if ($changes->isEmpty()) { + return; + } + + Activity::pendingChangesFor($this, $changes->keys()->all())->delete(); + + $eventName = 'updated'; + + $description = $this->getDescriptionForEvent($eventName) ?: $eventName; + + // Actual logging + app(ActivityLogger::class) + ->useLog('pending') + ->event($eventName) + ->performedOn($this) + ->withProperties($changes) + ->log($description); + } + + public function tapActivity(Activity $activity, string $eventName) + { + // Flip properties + $properties = []; + + foreach ($activity->properties->get('attributes', []) as $key => $value) { + $properties[$key] = [ + 'old' => data_get($activity->properties, "old.{$key}"), + 'new' => $value, + ]; + } + + if (! empty($properties)) { + $activity->properties = $properties; + } + + if (auth()->guest()) { + return; + } + + if (auth()->user()->isBbAdmin() || auth()->user()->isBbManager()) { + activity()->disableLogging(); + + if (! $activity->description) { + $activity->description = $this->getDescriptionForEvent($eventName); + } + + $activity->properties->each( + fn ($values, $key) => $activity + ->replicate() + ->fill([ + 'properties' => [$key => $values], + 'approved_at' => now(), + ]) + ->save() + ); + } else { + if ( + $activity->properties->count() === 1 && + property_exists($this, 'requiresApproval') && + ! \in_array($activity->properties->keys()->first(), $this->requiresApproval) + ) { + $activity->log_name = 'auto_approved'; + $activity->approved_at = now(); + } + } + } +} diff --git a/app/Concerns/MustSetInitialPassword.php b/app/Concerns/MustSetInitialPassword.php index 63384194..1a994160 100644 --- a/app/Concerns/MustSetInitialPassword.php +++ b/app/Concerns/MustSetInitialPassword.php @@ -22,8 +22,7 @@ protected static function bootMustSetInitialPassword(): void static::created(function (self $user) { if (! app()->runningInConsole()) { - if (!empty($user->created_by)) - { + if (! empty($user->created_by)) { $user->sendWelcomeNotification(); } } @@ -44,9 +43,9 @@ public function markPasswordAsSet(): bool public function sendWelcomeNotification(): void { - if ($this->role===UserRole::ngo_admin) - { + if ($this->role === UserRole::ngo_admin) { $this->notify(new WelcomeNotification()); + return; } $this->notify(new AdminWelcomeNotification()); diff --git a/app/Enums/OrganizationStatus.php b/app/Enums/OrganizationStatus.php index 8cd7ff3f..0076b1d4 100644 --- a/app/Enums/OrganizationStatus.php +++ b/app/Enums/OrganizationStatus.php @@ -13,6 +13,8 @@ enum OrganizationStatus: string case approved = 'active'; case rejected = 'disabled'; + public const pending_changes = 'pending_changes'; + protected function translationKeyPrefix(): ?string { return 'organization.status_arr'; diff --git a/app/Enums/UserRole.php b/app/Enums/UserRole.php index 878d3712..5084a6fb 100644 --- a/app/Enums/UserRole.php +++ b/app/Enums/UserRole.php @@ -18,5 +18,4 @@ public function translationKeyPrefix(): string { return 'user.roles'; } - } diff --git a/app/Filament/Forms/Components/Download.php b/app/Filament/Forms/Components/Download.php new file mode 100644 index 00000000..50ee0aed --- /dev/null +++ b/app/Filament/Forms/Components/Download.php @@ -0,0 +1,58 @@ +collectionName($collectionName); + $this->statePath($collectionName); + } + + public static function make(string $collectionName): static + { + $static = app(static::class, ['collectionName' => $collectionName]); + $static->configure(); + + return $static; + } + + public function collectionName(string | Closure $collectionName): static + { + $this->collectionName = $collectionName; + + return $this; + } + + public function getCollectionName(): string + { + return $this->evaluate($this->collectionName); + } + + public function getDownloadItems(): Collection + { + return $this->getRecord() + ->getMedia($this->getCollectionName()) + ->map(fn (Media $media) => [ + 'url' => $media->getFullUrl(), + 'name' => $media->name . '.' . $media->extension, + ]); + } +} diff --git a/app/Filament/Forms/Components/Value.php b/app/Filament/Forms/Components/Value.php index b40d8f34..5fa6b9fc 100644 --- a/app/Filament/Forms/Components/Value.php +++ b/app/Filament/Forms/Components/Value.php @@ -21,7 +21,7 @@ class Value extends Component use Concerns\HasHint; use Concerns\HasName; - protected string $view = 'components.forms.value'; + protected string $view = 'forms.components.value'; protected bool $empty = false; diff --git a/app/Filament/Resources/OrganizationResource.php b/app/Filament/Resources/OrganizationResource.php index 8d8c3c5c..320fa9a8 100644 --- a/app/Filament/Resources/OrganizationResource.php +++ b/app/Filament/Resources/OrganizationResource.php @@ -4,231 +4,183 @@ namespace App\Filament\Resources; -use App\Enums\OrganizationStatus; +use App\Filament\Forms\Components\Download; use App\Filament\Resources\OrganizationResource\Pages; -use App\Filament\Resources\OrganizationResource\Widgets\ApprovedOrganizationWidget; -use App\Filament\Resources\OrganizationResource\Widgets\NewOrganizationWidget; -use App\Filament\Resources\OrganizationResource\Widgets\RejectedOrganizationWidget; -use App\Forms\Components\Link; use App\Forms\Components\UserLink; use App\Models\Organization; -use App\Tables\Columns\ResourceNameColumn; -use Filament\Forms; +use App\Rules\ValidCIF; +use Filament\Forms\Components\Fieldset; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\SpatieMediaLibraryFileUpload; +use Filament\Forms\Components\Textarea; +use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Resources\Form; use Filament\Resources\Resource; -use Filament\Resources\Table; -use Filament\Tables; -use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Filters\Layout; -use Filament\Tables\Filters\SelectFilter; class OrganizationResource extends Resource { protected static ?string $model = Organization::class; - protected static ?string $navigationGroup = 'Administrează'; - protected static ?int $navigationSort = 1; - protected static ?string $navigationLabel = 'Organizații'; - - protected static ?string $label = 'Organizație'; - - protected static ?string $pluralLabel = 'Organizații'; - - protected static ?string $navigationIcon = 'heroicon-o-collection'; + protected static ?string $navigationIcon = 'heroicon-o-office-building'; - public static function form(Form $form): Form + protected static function getNavigationGroup(): ?string { - return $form - ->schema([ - UserLink::make('administator')->label(__('organization.labels.administrator'))->inlineLabel()->columnSpanFull(), - Forms\Components\Fieldset::make(__('organization.labels.general_data'))->schema([ - Forms\Components\TextInput::make('name') - ->label(__('organization.labels.name')) - ->inlineLabel() - ->columnSpanFull() - ->required() - ->maxLength(255), - Forms\Components\TextInput::make('cif') - ->label(__('organization.labels.cif')) - ->inlineLabel() - ->columnSpanFull() - ->required() - ->maxLength(255), - Forms\Components\SpatieMediaLibraryFileUpload::make('organizationFilesLogo') - ->collection('organizationFilesLogo') - ->label(__('organization.labels.logo')) - ->inlineLabel() - ->columnSpanFull() - ->required() - ->maxFiles(1) - ->acceptedFileTypes(['image/*']), - Forms\Components\Textarea::make('description') - ->label(__('organization.labels.description')) - ->inlineLabel() - ->columnSpanFull() - ->required() - ->maxLength(65535), - Forms\Components\Select::make('activity_domains') - ->label(__('organization.labels.activity_domains')) - ->columnSpanFull() - ->inlineLabel() - ->multiple() - ->relationship('activityDomains', 'name') - ->required(), - Forms\Components\Grid::make(3) - ->schema( - [ - Link::make('organizationFilesStatuteLabel')->inlineLabel()->label(__('organization.labels.statute')), - Forms\Components\SpatieMediaLibraryFileUpload::make('organizationFilesStatute') - ->disableLabel() - ->disablePreview() - ->collection('organizationFilesStatute') - ->required(), - ] - )->columns(2), - ]), - Forms\Components\Fieldset::make(__('organization.labels.volunteering_data'))->schema([ - Forms\Components\Toggle::make('accepts_volunteers') - ->label(__('organization.labels.accepts_volunteers')) - ->inlineLabel() - ->columnSpanFull() - ->required(), - Forms\Components\Textarea::make('why_volunteer') - ->label(__('organization.labels.why_volunteer')) - ->inlineLabel() - ->columnSpanFull() - ->maxLength(65535), - - ]), - Forms\Components\Fieldset::make(__('organization.labels.contact_data'))->schema([ - Forms\Components\TextInput::make('website') - ->label(__('organization.labels.website')) - ->inlineLabel() - ->columnSpanFull() - ->maxLength(255), - Forms\Components\TextInput::make('contact_email') - ->email() - ->label(__('organization.labels.contact_email')) - ->inlineLabel() - ->columnSpanFull() - ->required() - ->maxLength(255), - Forms\Components\TextInput::make('contact_phone') - ->tel() - ->label(__('organization.labels.contact_phone')) - ->inlineLabel() - ->columnSpanFull() - ->required() - ->maxLength(255), - Forms\Components\TextInput::make('contact_person') - ->label(__('organization.labels.contact_person')) - ->inlineLabel() - ->columnSpanFull() - ->required() - ->maxLength(255), - Forms\Components\TextInput::make('street_address') - ->label(__('organization.labels.street_address')) - ->inlineLabel() - ->columnSpanFull() - ->required() - ->maxLength(255), - ]), - Forms\Components\Fieldset::make(__('organization.labels.eu_platesc_data'))->schema([ - Forms\Components\TextInput::make('eu_platesc_merchant_id') - ->label(__('organization.labels.eu_platesc_merchant_id')) - ->inlineLabel() - ->columnSpanFull() - ->maxLength(255), - Forms\Components\TextInput::make('eu_platesc_private_key') - ->label(__('organization.labels.eu_platesc_private_key')) - ->inlineLabel() - ->columnSpanFull() - ->maxLength(255), - ]), - - ]); + return __('navigation.group.manage'); } - public static function table(Table $table): Table + public static function getModelLabel(): string { - return $table - ->columns([ - Tables\Columns\IconColumn::make('status')->options([ - 'heroicon-o-x-circle', - 'heroicon-o-pencil' => OrganizationStatus::rejected->value, - 'heroicon-o-clock' => OrganizationStatus::pending->value, - 'heroicon-o-check-circle' => OrganizationStatus::approved->value, - ]), - Tables\Columns\TextColumn::make('name'), - Tables\Columns\TextColumn::make('created_at') - ->dateTime(), - - ]) - ->filters([ - SelectFilter::make('status') - ->multiple() - ->options(OrganizationStatus::options()) - ->label(__('Status organizație')), - ]) - ->filtersLayout(Layout::AboveContent) - ->actions([ - Tables\Actions\EditAction::make(), - Tables\Actions\Action::make(__('organization.actions.approve')) - ->action(function () { - }) - ->icon('heroicon-o-check-circle') - ->requiresConfirmation(), - Tables\Actions\Action::make(__('organization.actions.reject')) - ->action(function () { - }) - ->icon('heroicon-o-x-circle') - ->color('danger') - ->requiresConfirmation(), - ]) - ->bulkActions([ - Tables\Actions\DeleteBulkAction::make(), - ]); + return __('organization.label.singular'); } - public static function getRelations(): array + public static function getPluralModelLabel(): string { - return [ - // - ]; + return __('organization.label.plural'); } - public static function getWidgets(): array + public static function form(Form $form): Form { - return [ - NewOrganizationWidget::class, - ApprovedOrganizationWidget::class, - RejectedOrganizationWidget::class, - ]; + return $form + ->schema([ + UserLink::make('administator') + ->label(__('organization.labels.administrator')) + ->inlineLabel() + ->columnSpanFull(), + + Fieldset::make(__('organization.labels.general_data')) + ->columns(1) + ->schema([ + TextInput::make('name') + ->label(__('organization.labels.name')) + ->inlineLabel() + ->required() + ->maxLength(255), + + TextInput::make('cif') + ->label(__('organization.labels.cif')) + ->unique(ignoreRecord: true) + ->rule(new ValidCIF) + ->inlineLabel() + ->required() + ->maxLength(255), + + SpatieMediaLibraryFileUpload::make('logo') + ->collection('logo') + ->label(__('organization.labels.logo')) + ->inlineLabel() + ->image() + ->maxFiles(1), + + Textarea::make('description') + ->label(__('organization.labels.description')) + ->inlineLabel() + ->required() + ->maxLength(65535), + + Select::make('activity_domains') + ->relationship('activityDomains', 'name') + ->label(__('organization.labels.activity_domains')) + ->inlineLabel() + ->multiple() + ->preload() + ->required(), + + Select::make('counties') + ->relationship('counties', 'name') + ->label(__('organization.labels.counties')) + ->inlineLabel() + ->multiple() + ->preload() + ->required(), + + SpatieMediaLibraryFileUpload::make('statute') + ->label(__('organization.labels.statute')) + ->inlineLabel() + ->disablePreview() + ->collection('statute') + ->hiddenOn('view'), + + Download::make('statute') + ->label(__('organization.labels.statute')) + ->inlineLabel() + ->hiddenOn('edit'), + ]), + + Fieldset::make(__('organization.labels.volunteering_data')) + ->columns(1) + ->schema([ + Toggle::make('accepts_volunteers') + ->label(__('organization.labels.accepts_volunteers')) + ->inlineLabel() + ->required(), + + Textarea::make('why_volunteer') + ->label(__('organization.labels.why_volunteer')) + ->inlineLabel() + ->maxLength(65535), + ]), + + Fieldset::make(__('organization.labels.contact_data')) + ->columns(1) + ->schema([ + TextInput::make('website') + ->label(__('organization.labels.website')) + ->inlineLabel() + ->maxLength(255), + + TextInput::make('contact_email') + ->label(__('organization.labels.contact_email')) + ->email() + ->inlineLabel() + ->required() + ->maxLength(255), + + TextInput::make('contact_phone') + ->tel() + ->label(__('organization.labels.contact_phone')) + ->inlineLabel() + ->required() + ->maxLength(255), + + TextInput::make('contact_person') + ->label(__('organization.labels.contact_person')) + ->inlineLabel() + ->required() + ->maxLength(255), + + TextInput::make('street_address') + ->label(__('organization.labels.street_address')) + ->inlineLabel() + ->required() + ->maxLength(255), + ]), + + Fieldset::make(__('organization.labels.eu_platesc_data')) + ->columns(1) + ->schema([ + TextInput::make('eu_platesc_merchant_id') + ->label(__('organization.labels.eu_platesc_merchant_id')) + ->inlineLabel() + ->maxLength(255), + + TextInput::make('eu_platesc_private_key') + ->label(__('organization.labels.eu_platesc_private_key')) + ->inlineLabel() + ->maxLength(255), + ]), + ]); } public static function getPages(): array { return [ - 'index' => Pages\OrganisationIndex::route('/'), + 'index' => Pages\ListOrganizations::route('/'), 'create' => Pages\CreateOrganization::route('/create'), 'edit' => Pages\EditOrganization::route('/{record}/edit'), 'view' => Pages\ViewOrganization::route('/{record}'), - - ]; - } - - public static function getWidgetColumns() - { - return [ - ResourceNameColumn::make('organisation_info')->label(__('organization.organization')), - - TextColumn::make('created_at') - ->label(__('field.created_at')) - ->dateTime('Y-m-d') - ->sortable(), - ]; } } diff --git a/app/Filament/Resources/OrganizationResource/Actions/ApproveAction.php b/app/Filament/Resources/OrganizationResource/Actions/ApproveAction.php deleted file mode 100644 index e4633946..00000000 --- a/app/Filament/Resources/OrganizationResource/Actions/ApproveAction.php +++ /dev/null @@ -1,25 +0,0 @@ -label(__('organization.actions.approve')); - $this->requiresConfirmation(); - $this->modalHeading(__('organization.actions.approve')); - $this->modalButton(__('organization.actions.approve')); - $this->action(function (Organization $record) { - $record->update(['status' => OrganizationStatus::approved]); - }); - } -} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Pages/ApproveOrganizationAction.php b/app/Filament/Resources/OrganizationResource/Actions/Pages/ApproveOrganizationAction.php new file mode 100644 index 00000000..c4268d30 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Pages/ApproveOrganizationAction.php @@ -0,0 +1,43 @@ +color('success'); + + $this->icon('heroicon-s-check'); + + $this->label(__('organization.actions.approve')); + + $this->requiresConfirmation(); + + $this->modalHeading(__('organization.approve_modal.heading')); + + $this->modalSubheading( + fn (Organization $record) => __('organization.approve_modal.subheading', [ + 'name' => $record->name, + ]) + ); + + $this->modalButton(__('organization.actions.approve')); + + $this->action(function (Organization $record) { + $record->markAsApproved(); + }); + } +} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Pages/DeactivateOrganizationAction.php b/app/Filament/Resources/OrganizationResource/Actions/Pages/DeactivateOrganizationAction.php new file mode 100644 index 00000000..49fe9664 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Pages/DeactivateOrganizationAction.php @@ -0,0 +1,43 @@ +color('danger'); + + $this->icon('heroicon-s-x'); + + $this->label(__('organization.actions.deactivate')); + + $this->requiresConfirmation(); + + $this->modalHeading(__('organization.deactivate_modal.heading')); + + $this->modalSubheading( + fn (Organization $record) => __('organization.deactivate_modal.subheading', [ + 'name' => $record->name, + ]) + ); + + $this->modalButton(__('organization.actions.deactivate')); + + $this->action(function (Organization $record) { + $record->markAsRejected(); + }); + } +} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Pages/ReactivateOrganizationAction.php b/app/Filament/Resources/OrganizationResource/Actions/Pages/ReactivateOrganizationAction.php new file mode 100644 index 00000000..888e0541 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Pages/ReactivateOrganizationAction.php @@ -0,0 +1,43 @@ +color('success'); + + $this->icon('heroicon-s-check'); + + $this->label(__('organization.actions.reactivate')); + + $this->requiresConfirmation(); + + $this->modalHeading(__('organization.reactivate_modal.heading')); + + $this->modalSubheading( + fn (Organization $record) => __('organization.reactivate_modal.subheading', [ + 'name' => $record->name, + ]) + ); + + $this->modalButton(__('organization.actions.reactivate')); + + $this->action(function (Organization $record) { + $record->markAsApproved(); + }); + } +} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Pages/RejectOrganizationAction.php b/app/Filament/Resources/OrganizationResource/Actions/Pages/RejectOrganizationAction.php new file mode 100644 index 00000000..d63edafd --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Pages/RejectOrganizationAction.php @@ -0,0 +1,43 @@ +color('danger'); + + $this->icon('heroicon-s-x'); + + $this->label(__('organization.actions.reject')); + + $this->requiresConfirmation(); + + $this->modalHeading(__('organization.reject_modal.heading')); + + $this->modalSubheading( + fn (Organization $record) => __('organization.reject_modal.subheading', [ + 'name' => $record->name, + ]) + ); + + $this->modalButton(__('organization.actions.reject')); + + $this->action(function (Organization $record) { + $record->markAsRejected(); + }); + } +} diff --git a/app/Filament/Resources/OrganizationResource/Actions/RejectAction.php b/app/Filament/Resources/OrganizationResource/Actions/RejectAction.php deleted file mode 100644 index 72b330e6..00000000 --- a/app/Filament/Resources/OrganizationResource/Actions/RejectAction.php +++ /dev/null @@ -1,25 +0,0 @@ -label(__('organization.actions.reject')); - $this->requiresConfirmation(); - $this->modalHeading(__('organization.actions.reject')); - $this->modalButton(__('organization.actions.reject')); - $this->action(function (Organization $record) { - $record->update(['status' => OrganizationStatus::rejected]); - }); - } -} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Tables/Activity/ApproveActivityAction.php b/app/Filament/Resources/OrganizationResource/Actions/Tables/Activity/ApproveActivityAction.php new file mode 100644 index 00000000..4aa8ab73 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Tables/Activity/ApproveActivityAction.php @@ -0,0 +1,31 @@ +label(__('activity.actions.approve')); + + $this->icon('heroicon-s-check'); + + $this->color('success'); + + $this->action(fn (Activity $record) => $record->approve()); + + $this->visible(fn (Activity $record) => $record->isPending()); + } +} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Tables/Activity/RejectActivityAction.php b/app/Filament/Resources/OrganizationResource/Actions/Tables/Activity/RejectActivityAction.php new file mode 100644 index 00000000..2b6f279c --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Tables/Activity/RejectActivityAction.php @@ -0,0 +1,31 @@ +label(__('activity.actions.reject')); + + $this->icon('heroicon-s-x'); + + $this->color('danger'); + + $this->action(fn (Activity $record) => $record->reject()); + + $this->visible(fn (Activity $record) => $record->isPending()); + } +} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Tables/Activity/ViewActivityAction.php b/app/Filament/Resources/OrganizationResource/Actions/Tables/Activity/ViewActivityAction.php new file mode 100644 index 00000000..e2d1dc58 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Tables/Activity/ViewActivityAction.php @@ -0,0 +1,50 @@ +form([ + Value::make('created_at') + ->label(__('activity.column.created_at')) + ->inlineLabel(), + + Value::make('causer.name') + ->label(__('activity.column.causer')) + ->inlineLabel(), + + Value::make('changed_field') + ->label(__('activity.column.changed_field')) + ->inlineLabel() + ->content( + fn (Activity $record) => __('organization.labels.' . $record->changed_field) + ), + + Value::make('changed_field_old_value') + ->label(__('activity.value.old')) + ->inlineLabel(), + + Value::make('changed_field_new_value') + ->label(__('activity.value.new')) + ->inlineLabel(), + ]); + + // $this->modalActions([ + // ]) + } +} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Tables/ExportAction.php b/app/Filament/Resources/OrganizationResource/Actions/Tables/ExportAction.php new file mode 100644 index 00000000..1a795d55 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Tables/ExportAction.php @@ -0,0 +1,156 @@ +status = $status?->value ?? $status; + + return $this; + } + + protected function setUp(): void + { + parent::setUp(); + + $this->color('secondary'); + + $this->exports([ + ExcelExport::make() + ->withFilename(fn () => sprintf( + '%s-%s-%s', + now()->format('Y_m_d-H_i_s'), + Str::slug(OrganizationResource::getPluralModelLabel()), + $this->status + )) + ->modifyQueryUsing(function (Builder $query) { + return $query + ->status($this->status) + ->with([ + 'activityDomains', + 'counties', + 'users' => fn ($q) => $q->onlyNGOAdmins(), + 'projects' => fn ($q) => $q->select('id', 'organization_id', 'status') + ->withCount('donations'), + ]) + ->withCount([ + 'volunteers', + ]); + }) + ->withColumns([ + Column::make('id') + ->heading('ID'), + + Column::make('name') + ->heading(__('organization.labels.name')), + + Column::make('cif') + ->heading(__('organization.labels.cif')), + + Column::make('activity_domains') + ->heading(__('organization.labels.activity_domains')) + ->formatStateUsing( + fn (Organization $record) => $record->activityDomains + ->pluck('name') + ->join(', ') + ), + + Column::make('accepts_volunteers') + ->heading(__('organization.labels.accepts_volunteers')) + ->formatStateUsing( + fn (Organization $record) => $record->accepts_volunteers + ? __('forms::components.select.boolean.true') + : __('forms::components.select.boolean.false') + ), + + Column::make('contact_person') + ->heading(__('organization.labels.contact_person')), + + Column::make('contact_phone') + ->heading(__('organization.labels.contact_phone')), + + Column::make('contact_email') + ->heading(__('organization.labels.contact_email')), + + Column::make('website') + ->heading(__('organization.labels.website')), + + Column::make('street_address') + ->heading(__('organization.labels.street_address')), + + Column::make('counties') + ->heading(__('organization.labels.counties')) + ->formatStateUsing( + fn (Organization $record) => $record->counties + ->pluck('name') + ->join(', ') + ), + + Column::make('accepts_volunteers') + ->heading(__('organization.labels.accepts_volunteers')) + ->formatStateUsing( + fn (Organization $record) => $record->accepts_volunteers + ? __('forms::components.select.boolean.true') + : __('forms::components.select.boolean.false') + ), + + Column::make('has_volunteers') + ->heading(__('organization.labels.has_volunteers')) + ->formatStateUsing( + fn (Organization $record) => $record->volunteers_count + ? __('forms::components.select.boolean.true') + : __('forms::components.select.boolean.false') + ), + + Column::make('has_projects') + ->heading(__('organization.labels.has_projects')) + ->formatStateUsing( + fn (Organization $record) => $record->projects->count() + ? __('forms::components.select.boolean.true') + : __('forms::components.select.boolean.false') + ), + + Column::make('has_active_projects') + ->heading(__('organization.labels.has_active_projects')) + ->formatStateUsing( + fn (Organization $record) => $record->projects->where('status', ProjectStatus::active)->count() + ? __('forms::components.select.boolean.true') + : __('forms::components.select.boolean.false') + ), + + Column::make('has_eu_platesc') + ->heading(__('organization.labels.has_eu_platesc')) + ->formatStateUsing( + fn (Organization $record) => $record->eu_platesc_merchant_id !== null && $record->eu_platesc_private_key !== null + ? __('forms::components.select.boolean.true') + : __('forms::components.select.boolean.false') + ), + + Column::make('has_donations') + ->heading(__('organization.labels.has_donations')) + ->formatStateUsing( + fn (Organization $record) => dd($record, $record->projects->sum('donations_count')) + ? __('forms::components.select.boolean.true') + : __('forms::components.select.boolean.false') + ), + + ]), + ]); + } +} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/ApproveOrganizationAction.php b/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/ApproveOrganizationAction.php new file mode 100644 index 00000000..1a024032 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/ApproveOrganizationAction.php @@ -0,0 +1,43 @@ +color('success'); + + $this->icon('heroicon-s-check'); + + $this->label(__('organization.actions.approve')); + + $this->requiresConfirmation(); + + $this->modalHeading(__('organization.approve_modal.heading')); + + $this->modalSubheading( + fn (Organization $record) => __('organization.approve_modal.subheading', [ + 'name' => $record->name, + ]) + ); + + $this->modalButton(__('organization.actions.approve')); + + $this->action(function (Organization $record) { + $record->markAsApproved(); + }); + } +} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/DeactivateOrganizationAction.php b/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/DeactivateOrganizationAction.php new file mode 100644 index 00000000..bc1fa180 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/DeactivateOrganizationAction.php @@ -0,0 +1,43 @@ +color('danger'); + + $this->icon('heroicon-s-x'); + + $this->label(__('organization.actions.deactivate')); + + $this->requiresConfirmation(); + + $this->modalHeading(__('organization.deactivate_modal.heading')); + + $this->modalSubheading( + fn (Organization $record) => __('organization.deactivate_modal.subheading', [ + 'name' => $record->name, + ]) + ); + + $this->modalButton(__('organization.actions.deactivate')); + + $this->action(function (Organization $record) { + $record->markAsRejected(); + }); + } +} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/ReactivateOrganizationAction.php b/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/ReactivateOrganizationAction.php new file mode 100644 index 00000000..4aa7face --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/ReactivateOrganizationAction.php @@ -0,0 +1,43 @@ +color('success'); + + $this->icon('heroicon-s-check'); + + $this->label(__('organization.actions.reactivate')); + + $this->requiresConfirmation(); + + $this->modalHeading(__('organization.reactivate_modal.heading')); + + $this->modalSubheading( + fn (Organization $record) => __('organization.reactivate_modal.subheading', [ + 'name' => $record->name, + ]) + ); + + $this->modalButton(__('organization.actions.reactivate')); + + $this->action(function (Organization $record) { + $record->markAsApproved(); + }); + } +} diff --git a/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/RejectOrganizationAction.php b/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/RejectOrganizationAction.php new file mode 100644 index 00000000..6c9f5425 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Actions/Tables/Organizations/RejectOrganizationAction.php @@ -0,0 +1,43 @@ +color('danger'); + + $this->icon('heroicon-s-x'); + + $this->label(__('organization.actions.reject')); + + $this->requiresConfirmation(); + + $this->modalHeading(__('organization.reject_modal.heading')); + + $this->modalSubheading( + fn (Organization $record) => __('organization.reject_modal.subheading', [ + 'name' => $record->name, + ]) + ); + + $this->modalButton(__('organization.actions.reject')); + + $this->action(function (Organization $record) { + $record->markAsRejected(); + }); + } +} diff --git a/app/Filament/Resources/OrganizationResource/Pages/ListOrganizations.php b/app/Filament/Resources/OrganizationResource/Pages/ListOrganizations.php index 6e965d85..4bc843ed 100644 --- a/app/Filament/Resources/OrganizationResource/Pages/ListOrganizations.php +++ b/app/Filament/Resources/OrganizationResource/Pages/ListOrganizations.php @@ -5,27 +5,32 @@ namespace App\Filament\Resources\OrganizationResource\Pages; use App\Filament\Resources\OrganizationResource; -use Filament\Pages\Actions; -use Filament\Resources\Pages\ListRecords; +use App\Filament\Resources\OrganizationResource\Widgets\ApprovedOrganizationsWidget; +use App\Filament\Resources\OrganizationResource\Widgets\PendingChangesOrganizationsWidget; +use App\Filament\Resources\OrganizationResource\Widgets\PendingOrganizationsWidget; +use App\Filament\Resources\OrganizationResource\Widgets\RejectedOrganizationsWidget; +use Filament\Resources\Pages\Page; -class ListOrganizations extends ListRecords +class ListOrganizations extends Page { protected static string $resource = OrganizationResource::class; + protected static string $view = 'filament.resources.organization-resource.pages.organisation-index'; + protected static ?string $title = ''; - protected function getActions(): array + protected function getHeaderWidgets(): array { return [ - Actions\CreateAction::make(), + PendingOrganizationsWidget::class, + PendingChangesOrganizationsWidget::class, + ApprovedOrganizationsWidget::class, + RejectedOrganizationsWidget::class, ]; } - protected function getHeaderWidgets(): array + protected function getHeaderWidgetsColumns(): int { - return [ - OrganizationResource\Widgets\NewOrganizationWidget::class, - - ]; + return 1; } } diff --git a/app/Filament/Resources/OrganizationResource/Pages/OrganisationIndex.php b/app/Filament/Resources/OrganizationResource/Pages/OrganisationIndex.php deleted file mode 100644 index 2f47c8f5..00000000 --- a/app/Filament/Resources/OrganizationResource/Pages/OrganisationIndex.php +++ /dev/null @@ -1,26 +0,0 @@ -record($this->getRecord()) + ->visible($this->getRecord()->isPending()), + + RejectOrganizationAction::make() + ->record($this->getRecord()) + ->visible($this->getRecord()->isPending()), + + ReactivateOrganizationAction::make() + ->record($this->getRecord()) + ->visible($this->getRecord()->isDisabled()), + + DeactivateOrganizationAction::make() + ->record($this->getRecord()) + ->visible($this->getRecord()->isActive()), + + ]; + } + + protected function getFooterWidgets(): array + { + return [ + OrganizationActivityWidget::class, + ]; + } } diff --git a/app/Filament/Resources/OrganizationResource/Widgets/ApprovedOrganizationWidget.php b/app/Filament/Resources/OrganizationResource/Widgets/ApprovedOrganizationWidget.php deleted file mode 100644 index 1436d6aa..00000000 --- a/app/Filament/Resources/OrganizationResource/Widgets/ApprovedOrganizationWidget.php +++ /dev/null @@ -1,92 +0,0 @@ - 2, - ]; - - protected static ?string $recordTitleAttribute = 'title'; - - public static function canView(): bool - { - return true; - } - - protected function getTableHeading(): string - { - return __('organization.heading.approved'); - } - - protected function getTableQuery(): Builder - { - return Organization::isApproved() - ->with(['media']) - ->select(['id', 'name', 'created_at', 'status']); - } - - protected function getTableQueryStringIdentifier(): ?string - { - return 'approved_organization'; - } - - protected function getDefaultTableSortColumn(): ?string - { - return 'id'; - } - - protected function getDefaultTableSortDirection(): ?string - { - return 'desc'; - } - - protected function getTableColumns(): array - { - return self::$resource::getWidgetColumns(); - } - - protected function getTableFilters(): array - { - return [ - - ]; - } - - protected function getTableRecordUrlUsing(): \Closure - { - return fn (Organization $record) => OrganizationResource::getUrl('view', $record); - } - - protected function getTableActions(): array - { - return [ - Action::make('view') - ->label(__('organization.actions.view')) - ->url($this->getTableRecordUrlUsing()) - ->icon(null), - - Action::make('edit') - ->label(__('organization.actions.edit')) - ->url(fn (Organization $record) => OrganizationResource::getUrl('edit', $record)) - ->icon(null), - - RejectAction::make('reject'), - ]; - } -} diff --git a/app/Filament/Resources/OrganizationResource/Widgets/ApprovedOrganizationsWidget.php b/app/Filament/Resources/OrganizationResource/Widgets/ApprovedOrganizationsWidget.php new file mode 100644 index 00000000..27580a98 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Widgets/ApprovedOrganizationsWidget.php @@ -0,0 +1,70 @@ +isApproved(); + } + + protected function getTableQueryStringIdentifier(): ?string + { + return 'approved'; + } + + protected function getTableColumns(): array + { + return [ + TitleWithImageColumn::make('name') + ->label(__('organization.organization')) + ->image(fn ($record) => $record->getFirstMediaUrl('logo')) + ->description( + fn ($record) => sprintf( + '%s: %s', + __('field.updated_at'), + $record->updated_at->toFormattedDateTime() + ) + ) + ->searchable() + ->sortable(), + + TextColumn::make('status_updated_at') + ->label(__('organization.labels.approved_at')) + ->dateTime() + ->sortable(), + ]; + } + + protected function getTableActions(): array + { + return [ + ViewAction::make() + ->label(__('organization.actions.view')) + ->url($this->getTableRecordUrlUsing()), + + EditAction::make() + ->label(__('organization.actions.edit')) + ->url(fn (Organization $record) => OrganizationResource::getUrl('edit', $record)), + + DeactivateOrganizationAction::make(), + ]; + } +} diff --git a/app/Filament/Resources/OrganizationResource/Widgets/BaseOrganizationsWidget.php b/app/Filament/Resources/OrganizationResource/Widgets/BaseOrganizationsWidget.php new file mode 100644 index 00000000..f1dc7515 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Widgets/BaseOrganizationsWidget.php @@ -0,0 +1,155 @@ +with(['media']) + ->select([ + 'id', + 'name', + 'created_at', + 'updated_at', + 'status_updated_at', + 'status', + ]); + } + + protected function getDefaultTableSortColumn(): ?string + { + return 'id'; + } + + protected function getDefaultTableSortDirection(): ?string + { + return 'desc'; + } + + protected function getTableHeaderActions(): array + { + return [ + ExportAction::make() + ->status(match (\get_class($this)) { + PendingOrganizationsWidget::class => OrganizationStatus::pending, + ApprovedOrganizationsWidget::class => OrganizationStatus::approved, + PendingChangesOrganizationsWidget::class => OrganizationStatus::pending_changes, + RejectedOrganizationsWidget::class => OrganizationStatus::rejected, + }), + ]; + } + + protected function getTableColumns(): array + { + return [ + TitleWithImageColumn::make('name') + ->label(__('organization.organization')) + ->image(fn ($record) => $record->cover_image) + ->description( + fn ($record) => sprintf( + '%s: %s', + __('field.updated_at'), + $record->updated_at->toFormattedDateTime() + ) + ) + ->searchable(), + + TextColumn::make('status_updated_at') + ->label(__('organization.labels.status_updated_at')) + ->dateTime() + ->sortable(), + ]; + } + + protected function getTableRecordUrlUsing(): \Closure + { + return fn (Organization $record) => OrganizationResource::getUrl('view', $record); + } + + protected function paginateTableQuery(Builder $query): Paginator + { + return $query->simplePaginate( + $this->getTableRecordsPerPage() == -1 ? $query->count() : $this->getTableRecordsPerPage(), + ['*'], + $this->getTablePaginationPageName(), + ); + } + + public static function getResource(): string + { + return static::$resource; + } + + protected function getTableFilters(): array + { + return [ + SelectFilter::make('counties') + ->relationship('counties', 'name') + ->label(__('organization.filters.counties')) + ->placeholder(__('organization.filters.counties_placeholder')) + ->multiple(), + + SelectFilter::make('activity_domains') + ->relationship('activityDomains', 'name') + ->label(__('organization.filters.activity_domains')) + ->placeholder(__('organization.filters.activity_domains_placeholder')) + ->multiple(), + + Filter::make('accepts_volunteers') + ->toggle() + ->label(__('organization.filters.accepts_volunteers')) + ->query(fn (Builder $query) => $query->whereAcceptsVolunteers()), + + Filter::make('has_volunteers') + ->toggle() + ->label(__('organization.filters.has_volunteers')) + ->query(fn (Builder $query) => $query->whereHasVolunteers()), + + Filter::make('has_projects') + ->toggle() + ->label(__('organization.filters.has_projects')) + ->query(fn (Builder $query) => $query->whereHasProjects()), + + Filter::make('has_active_projects') + ->toggle() + ->label(__('organization.filters.has_active_projects')) + ->query(fn (Builder $query) => $query->whereHasActiveProjects()), + + Filter::make('has_eu_platesc') + ->toggle() + ->label(__('organization.filters.has_eu_platesc')) + ->query(fn (Builder $query) => $query->whereHasEuPlatesc()), + + Filter::make('has_donations') + ->toggle() + ->label(__('organization.filters.has_donations')) + ->query(fn (Builder $query) => $query->whereHasDonations()), + + ]; + } +// protected function getTableFiltersLayout(): ?string +// { +// return Layout::AboveContent; +// } +} diff --git a/app/Filament/Resources/OrganizationResource/Widgets/NewOrganizationWidget.php b/app/Filament/Resources/OrganizationResource/Widgets/NewOrganizationWidget.php deleted file mode 100644 index 2f60805c..00000000 --- a/app/Filament/Resources/OrganizationResource/Widgets/NewOrganizationWidget.php +++ /dev/null @@ -1,95 +0,0 @@ - 2, - ]; - - protected static ?string $recordTitleAttribute = 'title'; - - public static function canView(): bool - { - return true; - } - - protected function getTableHeading(): string - { - return __('organization.heading.in_approval'); - } - - protected function getTableQuery(): Builder - { - return Organization::query()->isPending() - ->with(['media']) - ->select(['id', 'name', 'created_at', 'status']); - } - - protected function getTableQueryStringIdentifier(): ?string - { - return 'new_organization'; - } - - protected function getDefaultTableSortColumn(): ?string - { - return 'id'; - } - - protected function getDefaultTableSortDirection(): ?string - { - return 'desc'; - } - - protected function getTableColumns(): array - { - return self::$resource::getWidgetColumns(); - } - - protected function getTableFilters(): array - { - return [ - - ]; - } - - protected function getTableRecordUrlUsing(): \Closure - { - return fn (Organization $record) => OrganizationResource::getUrl('view', $record); - } - - protected function getTableActions(): array - { - return [ - Action::make('view') - ->label(__('organization.actions.view')) - ->url($this->getTableRecordUrlUsing()) - ->icon(null), - - Action::make('edit') - ->label(__('organization.actions.edit')) - ->url(fn (Organization $record) => OrganizationResource::getUrl('edit', $record)) - ->icon(null), - - ApproveAction::make('approve'), - - RejectAction::make('reject'), - ]; - } -} diff --git a/app/Filament/Resources/OrganizationResource/Widgets/OrganizationActivityWidget.php b/app/Filament/Resources/OrganizationResource/Widgets/OrganizationActivityWidget.php new file mode 100644 index 00000000..b2842488 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Widgets/OrganizationActivityWidget.php @@ -0,0 +1,67 @@ +forSubject($this->record) + ->forEvent('updated'); + } + + protected function getTableColumns(): array + { + return [ + TextColumn::make('created_at') + ->label(__('activity.column.created_at')), + + TextColumn::make('changed_field') + ->label(__('activity.column.changed_field')) + ->formatStateUsing( + fn ($state) => __('organization.labels.' . $state) + ), + + TextColumn::make('causer.name') + ->label(__('activity.column.causer')) + ->description( + fn (Activity $record) => $record->causer?->role->label() + ) + ->url( + fn (Activity $record) => $record->causer + ? UserResource::getUrl('view', $record->causer) + : null + ), + TextColumn::make('status') + ->label(__('activity.column.status')), + ]; + } + + protected function getTableActions(): array + { + return [ + ViewActivityAction::make(), + + ApproveActivityAction::make(), + + RejectActivityAction::make(), + ]; + } +} diff --git a/app/Filament/Resources/OrganizationResource/Widgets/PendingChangesOrganizationsWidget.php b/app/Filament/Resources/OrganizationResource/Widgets/PendingChangesOrganizationsWidget.php new file mode 100644 index 00000000..c4c1ed64 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Widgets/PendingChangesOrganizationsWidget.php @@ -0,0 +1,71 @@ +whereHas('activities', function ($q) { + $q->where('log_name', 'pending') + ->where('approved_at', null) + ->where('rejected_at', null); + }); + } + + protected function getTableQueryStringIdentifier(): ?string + { + return 'pending_changes'; + } + + protected function getTableColumns(): array + { + return [ + TitleWithImageColumn::make('name') + ->label(__('organization.organization')) + ->image(fn ($record) => $record->getFirstMediaUrl('logo')) + ->description( + fn ($record) => sprintf( + '%s: %s', + __('field.updated_at'), + $record->activities->last()->created_at->toFormattedDateTime() + ) + ) + ->searchable() + ->sortable(), + + TextColumn::make('created_at') + ->label(__('organization.labels.approved_at')) + ->description( + fn ($record) => sprintf( + '%s: %s', + __('field.updated_at'), + $record->activities->first()->created_at->toFormattedDateTime() + ) + ) + ->dateTime() + ->sortable(), + ]; + } + + protected function getTableActions(): array + { + return [ + ViewAction::make() + ->label(__('organization.actions.view')) + ->url($this->getTableRecordUrlUsing()), + ]; + } +} diff --git a/app/Filament/Resources/OrganizationResource/Widgets/PendingOrganizationsWidget.php b/app/Filament/Resources/OrganizationResource/Widgets/PendingOrganizationsWidget.php new file mode 100644 index 00000000..c68c9909 --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Widgets/PendingOrganizationsWidget.php @@ -0,0 +1,73 @@ +isPending(); + } + + protected function getTableQueryStringIdentifier(): ?string + { + return 'pending'; + } + + protected function getTableColumns(): array + { + return [ + TitleWithImageColumn::make('name') + ->label(__('organization.organization')) + ->image(fn ($record) => $record->getFirstMediaUrl('logo')) + ->description( + fn ($record) => sprintf( + '%s: %s', + __('field.updated_at'), + $record->updated_at->toFormattedDateTime() + ) + ) + ->searchable() + ->sortable(), + + TextColumn::make('created_at') + ->label(__('organization.labels.created_at')) + ->dateTime() + ->sortable(), + ]; + } + + protected function getTableActions(): array + { + return [ + ViewAction::make() + ->label(__('organization.actions.view')) + ->url($this->getTableRecordUrlUsing()), + + EditAction::make() + ->label(__('organization.actions.edit')) + ->url(fn (Organization $record) => OrganizationResource::getUrl('edit', $record)), + + ApproveOrganizationAction::make(), + + RejectOrganizationAction::make(), + ]; + } +} diff --git a/app/Filament/Resources/OrganizationResource/Widgets/RejectedOrganizationWidget.php b/app/Filament/Resources/OrganizationResource/Widgets/RejectedOrganizationWidget.php deleted file mode 100644 index bc8d916b..00000000 --- a/app/Filament/Resources/OrganizationResource/Widgets/RejectedOrganizationWidget.php +++ /dev/null @@ -1,92 +0,0 @@ - 2, - ]; - - protected static ?string $recordTitleAttribute = 'title'; - - public static function canView(): bool - { - return true; - } - - protected function getTableHeading(): string - { - return __('organization.heading.rejected'); - } - - protected function getTableQuery(): Builder - { - return Organization::isRejected() - ->with(['media']) - ->select(['id', 'name', 'created_at', 'status']); - } - - protected function getTableQueryStringIdentifier(): ?string - { - return 'rejected_organization'; - } - - protected function getDefaultTableSortColumn(): ?string - { - return 'id'; - } - - protected function getDefaultTableSortDirection(): ?string - { - return 'desc'; - } - - protected function getTableColumns(): array - { - return self::$resource::getWidgetColumns(); - } - - protected function getTableFilters(): array - { - return [ - - ]; - } - - protected function getTableRecordUrlUsing(): \Closure - { - return fn (Organization $record) => OrganizationResource::getUrl('view', $record); - } - - protected function getTableActions(): array - { - return [ - Action::make('view') - ->label(__('organization.actions.view')) - ->url($this->getTableRecordUrlUsing()) - ->icon(null), - - Action::make('edit') - ->label(__('organization.actions.edit')) - ->url(fn (Organization $record) => OrganizationResource::getUrl('edit', $record)) - ->icon(null), - - ApproveAction::make('approve'), - ]; - } -} diff --git a/app/Filament/Resources/OrganizationResource/Widgets/RejectedOrganizationsWidget.php b/app/Filament/Resources/OrganizationResource/Widgets/RejectedOrganizationsWidget.php new file mode 100644 index 00000000..3a1a240b --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Widgets/RejectedOrganizationsWidget.php @@ -0,0 +1,70 @@ +isRejected(); + } + + protected function getTableQueryStringIdentifier(): ?string + { + return 'rejected'; + } + + protected function getTableColumns(): array + { + return [ + TitleWithImageColumn::make('name') + ->label(__('organization.organization')) + ->image(fn ($record) => $record->getFirstMediaUrl('logo')) + ->description( + fn ($record) => sprintf( + '%s: %s', + __('field.updated_at'), + $record->updated_at->toFormattedDateTime() + ) + ) + ->searchable() + ->sortable(), + + TextColumn::make('status_updated_at') + ->label(__('organization.labels.rejected_at')) + ->dateTime() + ->sortable(), + ]; + } + + protected function getTableActions(): array + { + return [ + ViewAction::make() + ->label(__('organization.actions.view')) + ->url($this->getTableRecordUrlUsing()), + + EditAction::make() + ->label(__('organization.actions.edit')) + ->url(fn (Organization $record) => OrganizationResource::getUrl('edit', $record)), + + ReactivateOrganizationAction::make(), + ]; + } +} diff --git a/app/Filament/Resources/ProjectResource.php b/app/Filament/Resources/ProjectResource.php index eb5895ab..13f077fa 100644 --- a/app/Filament/Resources/ProjectResource.php +++ b/app/Filament/Resources/ProjectResource.php @@ -10,7 +10,6 @@ use App\Filament\Resources\ProjectResource\Widgets\NewProject; use App\Filament\Resources\ProjectResource\Widgets\RejectedProject; use App\Models\Project; -use App\Models\ProjectCategory; use App\Tables\Columns\ResourceNameColumn; use Filament\Forms; use Filament\Forms\Components\DatePicker; diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index af523cb7..7554c600 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -4,11 +4,8 @@ namespace App\Filament\Resources; -use App\Enums\UserRole; use App\Filament\Resources\UserResource\Pages; use App\Models\User; -use Filament\Forms\Components\Select; -use Filament\Forms\Components\TextInput; use Filament\Resources\Form; use Filament\Resources\Resource; use Filament\Resources\Table; @@ -60,6 +57,7 @@ public static function getPages(): array return [ 'index' => Pages\ListUsers::route('/'), 'create' => Pages\CreateUser::route('/create'), + 'view' => Pages\ViewUser::route('/{record}'), 'edit' => Pages\EditUser::route('/{record}/edit'), ]; } diff --git a/app/Filament/Resources/UserResource/Pages/CreateUser.php b/app/Filament/Resources/UserResource/Pages/CreateUser.php index f89c0ed7..6dc81112 100644 --- a/app/Filament/Resources/UserResource/Pages/CreateUser.php +++ b/app/Filament/Resources/UserResource/Pages/CreateUser.php @@ -14,9 +14,10 @@ class CreateUser extends CreateRecord { protected static string $resource = UserResource::class; + protected static bool $canCreateAnother = false; - public function form(Form $form): Form + public function form(Form $form): Form { return $form ->schema([ @@ -30,13 +31,16 @@ public function form(Form $form): Form ->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() + ->options( + collect( + UserRole::options() + )->only( + [ + UserRole::bb_admin->value, + UserRole::bb_manager->value, + UserRole::ngo_admin->value, + ] + )->toArray() )->reactive() ->required(), Select::make('organization') @@ -51,9 +55,11 @@ public function form(Form $form): Form ]); } + protected function mutateFormDataBeforeCreate(array $data): array { $data['created_by'] = auth()->id(); + return $data; } } diff --git a/app/Filament/Resources/UserResource/Pages/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php index 3d151732..1af89f6b 100644 --- a/app/Filament/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Resources/UserResource/Pages/EditUser.php @@ -22,7 +22,8 @@ protected function getActions(): array Actions\DeleteAction::make(), ]; } - public function form(Form $form): Form + + public function form(Form $form): Form { return $form ->schema([ @@ -36,13 +37,16 @@ public function form(Form $form): Form ->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() + ->options( + collect( + UserRole::options() + )->only( + [ + UserRole::bb_admin->value, + UserRole::bb_manager->value, + UserRole::ngo_admin->value, + ] + )->toArray() )->reactive() ->required(), Select::make('organization') @@ -57,5 +61,4 @@ public function form(Form $form): Form ]); } - } diff --git a/app/Filament/Resources/UserResource/Pages/ViewUser.php b/app/Filament/Resources/UserResource/Pages/ViewUser.php new file mode 100644 index 00000000..59c38c6e --- /dev/null +++ b/app/Filament/Resources/UserResource/Pages/ViewUser.php @@ -0,0 +1,21 @@ +getRecord() + ->getAdministrators() + ->map(fn (User $user) => [ + 'name' => $user->name, + 'url' => UserResource::getUrl('view', $user), + ]); + } } diff --git a/app/Http/Controllers/ArticleController.php b/app/Http/Controllers/ArticleController.php index 730b57be..f57bc10f 100644 --- a/app/Http/Controllers/ArticleController.php +++ b/app/Http/Controllers/ArticleController.php @@ -38,6 +38,7 @@ public function article(Article $article) { $article->load('category'); $gallery = $article->getMedia('gallery'); + //dd($article->relatedArticles()->get()); return Inertia::render('Public/Articles/Article', [ 'article' => $article, diff --git a/app/Http/Controllers/Auth/PasswordController.php b/app/Http/Controllers/Auth/PasswordController.php index b61b3ee9..9355c17a 100644 --- a/app/Http/Controllers/Auth/PasswordController.php +++ b/app/Http/Controllers/Auth/PasswordController.php @@ -45,11 +45,13 @@ public function setInitialPassword(User $user, Request $request): \Inertia\Respo if ($user->hasSetPassword()) { abort(Response::HTTP_FORBIDDEN, __('auth.welcome.already_used')); } + return Inertia::render('Auth/SetInitialPassword', [ 'user' => $user, 'token' => sha1($user->email), ]); } + public function storeInitialPassword(Request $request, User $user): RedirectResponse { if ($request->token !== sha1($user->email)) { diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 66ce397c..70b5bb0d 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -66,10 +66,9 @@ public function store(RegistrationRequest $request): RedirectResponse if ($data['type'] == 'ngo-admin') { $ong = $data['ong']; $organization = Organization::create($ong); - $organization->addMediaFromRequest('ong.logo')->toMediaCollection('organizationFilesLogo'); - if ($request->hasFile('ong.statute')) - { - $organization->addMediaFromRequest('ong.statute')->toMediaCollection('organizationFilesStatute'); + $organization->addMediaFromRequest('ong.logo')->toMediaCollection('logo'); + if ($request->hasFile('ong.statute')) { + $organization->addMediaFromRequest('ong.statute')->toMediaCollection('statute'); } $organization->activityDomains()->attach($ong['activity_domains_ids']); $organization->counties()->attach($ong['counties_ids']); @@ -83,6 +82,7 @@ public function store(RegistrationRequest $request): RedirectResponse return redirect()->route('register')->with('success_message', ['message' => 'Contul a fost creat', 'usrid' => $user['id']]); } catch(\Throwable $th) { Log::log('error', $th->getMessage()); + return redirect()->back()->with('error_message', __('auth.failed')); } } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 2b4a53be..2e0b9c98 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -1,8 +1,9 @@ get('project_status'); $projects = Project::query()->with('organization')->where('organization_id', auth()->user()->organization_id); - if ($projectStatus) - { + if ($projectStatus) { $projects = $projects->where('status', $projectStatus); } @@ -60,6 +52,7 @@ public function store(StoreRequest $request) $project->addAllMediaFromRequest()->each(function ($fileAdder) { $fileAdder->toMediaCollection('project_files'); }); + return redirect()->route('admin.ong.project.edit', $project->id)->with('success', 'Project created.'); } @@ -87,6 +80,7 @@ public function update(Request $request, Project $project) $project->categories()->sync(collect($request->get('categories'))->pluck('id')); } $project->update($request->all()); + return redirect()->back()->with('success_message', 'Project updated.'); } @@ -97,6 +91,7 @@ public function changeStatus($id, Request $request) } catch (\Exception $exception) { return redirect()->back()->with('error_message', $exception->getMessage()); } + return redirect()->back()->with('success_message', 'Project status changed.'); } } diff --git a/app/Http/Controllers/Ngo/RegionalProjectController.php b/app/Http/Controllers/Ngo/RegionalProjectController.php index 09ab1e91..393fac31 100644 --- a/app/Http/Controllers/Ngo/RegionalProjectController.php +++ b/app/Http/Controllers/Ngo/RegionalProjectController.php @@ -1,21 +1,17 @@ addAllMediaFromRequest()->each(function ($fileAdder) { $fileAdder->toMediaCollection('regionalProjectFiles'); }); + return redirect()->route('admin.ong.project.edit', $project->id)->with('success', 'Project created.'); } @@ -79,6 +76,7 @@ public function edit(RegionalProject $project) { // $this->authorize('view', $project); $project->load('media'); + return Inertia::render('AdminOng/Projects/EditRegionalProject', [ 'project' => $project, 'counties' => County::get(['name', 'id']), @@ -96,8 +94,8 @@ public function update(Request $request, RegionalProject $project) $project->counties()->sync(collect($request->get('counties'))->pluck('id')); } $project->update($request->all()); - return redirect()->back()->with('success_message', 'Project updated.'); + return redirect()->back()->with('success_message', 'Project updated.'); } /** @@ -107,6 +105,7 @@ public function destroy(string $id) { // } + public function changeStatus(Request $request, string $id) { try { @@ -114,6 +113,7 @@ public function changeStatus(Request $request, string $id) } catch (\Exception $exception) { return redirect()->back()->with('error_message', $exception->getMessage()); } + return redirect()->back()->with('success_message', 'Project status changed.'); } } diff --git a/app/Http/Controllers/OrganizationController.php b/app/Http/Controllers/OrganizationController.php index fd38ca8d..d4ad4eea 100644 --- a/app/Http/Controllers/OrganizationController.php +++ b/app/Http/Controllers/OrganizationController.php @@ -6,10 +6,14 @@ use App\Enums\OrganizationQuery; use App\Enums\OrganizationStatus; -use App\Http\Requests\StoreOrganizationRequest; +use App\Http\Requests\Organization\UpdateOrganizationRequest; +use App\Http\Resources\OrganizationResource; +use App\Models\Activity; use App\Models\ActivityDomain; +use App\Models\County; use App\Models\Organization; use App\Models\Volunteer; +use App\Services\OrganizationService; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Inertia\Inertia; @@ -69,50 +73,43 @@ public function show(Organization $organization) public function edit() { $organization = auth()->user()->organization; + $activityDomains = cache()->remember('activityDomains', 60 * 60 * 24, function () { - return \App\Models\ActivityDomain::get(['name', 'id']); + return ActivityDomain::get(['name', 'id']); }); + $counties = cache()->remember('counties', 60 * 60 * 24, function () { - return \App\Models\County::get(['name', 'id']); + return County::get(['name', 'id']); }); + $changes = Activity::pendingChangesFor($organization) + ->get() + ->flatMap(fn (Activity $activity) => $activity->properties->keys()) + ->unique() + ->values(); + return Inertia::render('AdminOng/Ong/EditOng', [ - 'organization' => $organization, + 'organization' => new OrganizationResource($organization), 'activity_domains' => $activityDomains, 'counties' => $counties, + 'changes' => $changes, ]); } /** * Update the specified resource in storage. */ - public function update(Request $request, Organization $organization) + public function update(UpdateOrganizationRequest $request, Organization $organization) { - try { - /** Get all request data. */ - $modelData = $request->input(); - - if ($request->has('activity_domains')) { - $ids = collect($request->input('activity_domains'))->pluck('id')->toArray(); - $organization->activityDomains()->sync($ids); - } - if ($request->hasFile('cover_image')) { - $organization->clearMediaCollection('organizationFilesLogo'); - $organization->addMediaFromRequest('cover_image')->toMediaCollection('organizationFilesLogo'); - } - - $organization->update($modelData); + OrganizationService::update($organization, $request->validated()); - return redirect()->route('admin.ong.edit')->with('success_message', __('organization.messages.update_success')); - } catch (\Throwable $th) { - return redirect()->route('admin.ong.edit')->with('error_message', __('organization.messages.update_error')); - } + return redirect()->route('admin.ong.edit') + ->with('success_message', __('organization.messages.update_success')); } - public function removeCoverImage(Request $request) + public function removeLogo(Request $request) { - $organization = auth()->user()->organization; - $organization->clearMediaCollection('organizationFilesLogo'); + auth()->user()->organization->clearMediaCollection('logo'); return redirect()->back(); } diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index da9bfd91..17f675f7 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -5,7 +5,6 @@ namespace App\Http\Controllers; use App\Enums\EuPlatescStatus; -use App\Enums\ProjectStatus; use App\Models\ActivityDomain; use App\Models\County; use App\Models\Project; diff --git a/app/Http/Controllers/RegionalProjectController.php b/app/Http/Controllers/RegionalProjectController.php index ebf1d271..cfadcb0e 100644 --- a/app/Http/Controllers/RegionalProjectController.php +++ b/app/Http/Controllers/RegionalProjectController.php @@ -1,8 +1,8 @@ user()->can('update', $this->organization); + } + /** * Get the validation rules that apply to the request. * @@ -15,6 +21,21 @@ class UpdateOrganizationRequest extends FormRequest */ public function rules(): array { - return []; + return [ + 'name' => ['nullable', 'string'], + 'description' => ['nullable', 'string'], + 'logo' => ['nullable', 'file', 'mimes:jpg,png'], + 'statute' => ['nullable', 'file'], + 'street_address' => ['nullable', 'string'], + 'cif' => ['nullable', 'string', 'unique:organizations,cif', new ValidCIF], + 'contact_email' => ['nullable', 'email'], + 'contact_phone' => ['nullable', 'string'], + 'contact_person' => ['nullable', 'string'], + 'activity_domains' => ['nullable', 'array'], + 'counties' => ['nullable', 'array'], + 'volunteer' => ['nullable', 'boolean'], + 'why_volunteer' => ['nullable', 'string'], + 'website' => ['nullable', 'string'], + ]; } } diff --git a/app/Http/Requests/RegionalProject/StoreRequest.php b/app/Http/Requests/RegionalProject/StoreRequest.php index 971434f6..4825af96 100644 --- a/app/Http/Requests/RegionalProject/StoreRequest.php +++ b/app/Http/Requests/RegionalProject/StoreRequest.php @@ -1,5 +1,7 @@ project_status === ProjectStatus::pending->value) { // return [ // 'project_status' => ['required', 'string'], // 'pending' or 'approved diff --git a/app/Http/Requests/RegistrationRequest.php b/app/Http/Requests/RegistrationRequest.php index 1ea45b56..ccdf1255 100644 --- a/app/Http/Requests/RegistrationRequest.php +++ b/app/Http/Requests/RegistrationRequest.php @@ -4,18 +4,11 @@ namespace App\Http\Requests; +use App\Rules\ValidCIF; use Illuminate\Foundation\Http\FormRequest; class RegistrationRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - */ - public function authorize(): bool - { - return true; - } - /** * Get the validation rules that apply to the request. * @@ -29,28 +22,31 @@ public function rules(): array 'user.name' => ['string', 'required'], 'user.email' => ['email', 'required', 'unique:users,email'], 'user.password' => ['string', 'required', 'confirmed'], - ]; + if ($this->type === 'ngo-admin') { - $rules['ong'] = ['array', 'required']; - $rules['ong.name'] = ['string', 'required']; - $rules['ong.description'] = ['string', 'required']; - $rules['ong.logo'] = ['required', 'file']; - $rules['ong.statute'] = ['required', 'file']; - $rules['ong.street_address'] = ['string', 'required']; - $rules['ong.cif'] = ['string', 'required']; - $rules['ong.contact_email'] = ['required', 'email']; - $rules['ong.contact_phone'] = ['string', 'required']; - $rules['ong.contact_person'] = ['string', 'required']; - $rules['ong.activity_domains_ids'] = ['array', 'required']; - $rules['ong.counties_ids'] = ['array', 'required']; - $rules['ong.volunteer'] = ['boolean']; - $rules['ong.why_volunteer'] = ['string','nullable']; - $rules['ong.website'] = ['string','nullable']; + $rules = array_merge($rules, [ + 'ong' => ['array', 'required'], + 'ong.name' => ['string', 'required'], + 'ong.description' => ['string', 'required'], + 'ong.logo' => ['required', 'file'], + 'ong.statute' => ['required', 'file'], + 'ong.street_address' => ['string', 'required'], + 'ong.cif' => ['string', 'required', 'unique:organizations,cif', new ValidCIF], + 'ong.contact_email' => ['required', 'email'], + 'ong.contact_phone' => ['string', 'required'], + 'ong.contact_person' => ['string', 'required'], + 'ong.activity_domains_ids' => ['array', 'required'], + 'ong.counties_ids' => ['array', 'required'], + 'ong.volunteer' => ['boolean'], + 'ong.why_volunteer' => ['string', 'nullable'], + 'ong.website' => ['string', 'nullable'], + ]); } return $rules; } + public function messages(): array { return [ diff --git a/app/Http/Resources/OrganizationResource.php b/app/Http/Resources/OrganizationResource.php new file mode 100644 index 00000000..d4f445d3 --- /dev/null +++ b/app/Http/Resources/OrganizationResource.php @@ -0,0 +1,37 @@ + $this->id, + 'name' => $this->name, + 'cif' => $this->cif, + 'counties' => $this->counties->map->only('id', 'name'), + 'activity_domains' => $this->activityDomains, + 'logo' => $this->getFirstMediaUrl('logo', 'preview'), + 'statute_link' => $this->getFirstMediaUrl('statute'), + 'description' => $this->description, + 'street_address' => $this->street_address, + 'contact_person' => $this->contact_person, + 'contact_phone' => $this->contact_phone, + 'contact_email' => $this->contact_email, + 'website' => $this->website, + 'accepts_volunteers' => $this->accepts_volunteers, + 'why_volunteer' => $this->why_volunteer, + 'status' => $this->status, + 'eu_platesc_merchant_id' => filled($this->eu_platesc_merchant_id), + 'eu_platesc_private_key' => filled($this->eu_platesc_private_key), + ]; + } +} diff --git a/app/Models/Activity.php b/app/Models/Activity.php new file mode 100644 index 00000000..60ef863b --- /dev/null +++ b/app/Models/Activity.php @@ -0,0 +1,147 @@ + 'collection', + 'approved_at' => 'datetime', + 'rejected_at' => 'datetime', + ]; + + protected $with = [ + 'causer', 'subject', + ]; + + protected static function booted() + { + static::addGlobalScope('latest', function (Builder $query) { + return $query->latest(); + }); + } + + public function scopeBetweenDates(Builder $query, ?string $from = null, ?string $until = null): Builder + { + return $query + ->when($from, function (Builder $query, string $date) { + $query->whereDate('created_at', '>=', $date); + }) + ->when($until, function (Builder $query, string $date) { + $query->whereDate('created_at', '<=', $date); + }); + } + + public function getChangedFieldAttribute(): ?string + { + return $this->properties->keys()->first(); + } + + public function getChangedFieldOldValueAttribute(): ?string + { + $value = data_get($this->properties, $this->changed_field . '.old'); + + if ($this->description === 'statute') { + return Media::find($value)?->getUrl(); + } + + return $value; + } + + public function getChangedFieldNewValueAttribute(): ?string + { + $value = data_get($this->properties, $this->changed_field . '.new'); + + if ($this->description === 'statute') { + return Media::find($value)?->getUrl(); + } + + return $value; + } + + public function getStatusAttribute(): ?string + { + if ($this->log_name === 'auto_approved') { + return __('activity.status.auto_approved'); + } + + if ($this->isApproved()) { + return __('activity.status.approved'); + } + + if ($this->isRejected()) { + return __('activity.status.rejected'); + } + + return __('activity.status.pending'); + } + + public function isPending(): bool + { + return ! $this->isApproved() && ! $this->isRejected(); + } + + public function isApproved(): bool + { + return null !== $this->approved_at; + } + + public function isRejected(): bool + { + return null !== $this->rejected_at; + } + + public function scopePendingChangesFor(Builder $query, Model $subject, array $keys = []): Builder + { + return $query->inLog('pending') + ->forSubject($subject) + ->whereNull('approved_at') + ->whereNull('rejected_at') + ->when(filled($keys), function (Builder $query) use ($keys) { + $query->where(function (Builder $query) use ($keys) { + foreach ($keys as $key) { + $query->orWhereJsonContainsKey("properties->{$key}"); + } + }); + }); + } + + public function approve(): void + { + if ($this->isRejected()) { + return; + } + + activity()->withoutLogs(function () { + $this->properties->each(function ($value, $key) { + $this->subject->setAttribute($key, $value['new']); + }); + + $this->subject->save(); + + $this->update([ + 'approved_at' => now(), + ]); + }); + } + + public function reject(): void + { + if ($this->isApproved()) { + return; + } + + $this->update([ + 'rejected_at' => now(), + ]); + } +} diff --git a/app/Models/Organization.php b/app/Models/Organization.php index fdd6210b..3599fbda 100644 --- a/app/Models/Organization.php +++ b/app/Models/Organization.php @@ -4,7 +4,9 @@ namespace App\Models; -use App\Enums\UserRole; +use App\Concerns\LogsActivityForApproval; +use App\Enums\OrganizationStatus; +use App\Enums\ProjectStatus; use App\Traits\HasActivityDomain; use App\Traits\HasOrganizationStatus; use Illuminate\Database\Eloquent\Builder; @@ -12,8 +14,10 @@ use Illuminate\Database\Eloquent\Model; 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\Activitylog\Traits\LogsActivity; use Spatie\Image\Manipulations; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; @@ -25,7 +29,7 @@ class Organization extends Model implements HasMedia use InteractsWithMedia; use HasActivityDomain; use HasOrganizationStatus; - use LogsActivity; + use LogsActivityForApproval; /** * The attributes that are mass assignable. @@ -44,6 +48,7 @@ class Organization extends Model implements HasMedia 'accepts_volunteers', 'why_volunteer', 'status', + 'status_updated_at', 'eu_platesc_merchant_id', 'eu_platesc_private_key', ]; @@ -57,22 +62,38 @@ class Organization extends Model implements HasMedia 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', + 'status_updated_at' => 'datetime', 'accepts_volunteers' => 'boolean', ]; - protected $appends = ['cover_image', 'statute_link']; + public array $requiresApproval = [ + 'name', + 'cif', + 'street_address', + 'statute', + ]; public function projects(): HasMany { return $this->hasMany(Project::class)->without('organization'); } - public function registerMediaConversions(Media $media = null): void + public function registerMediaCollections(): void { - $this - ->addMediaConversion('preview') - ->fit(Manipulations::FIT_CROP, 300, 300) - ->nonQueued(); + $this->addMediaCollection('logo') + ->useFallbackUrl(Vite::asset('resources/images/organization.png')) + ->singleFile() + ->registerMediaConversions(function (Media $media) { + $this + ->addMediaConversion('preview') + ->fit(Manipulations::FIT_CONTAIN, 300, 300) + ->nonQueued(); + }); + + $this->addMediaCollection('statute') + ->singleFile(); + + $this->addMediaCollection('statute_pending'); } public function users(): HasMany @@ -95,30 +116,64 @@ public function tickets(): HasMany return $this->hasMany(Ticket::class); } + public function activities(): MorphMany + { + return $this->morphMany(Activity::class, 'subject'); + } + + public function volunteers(): BelongsToMany + { + return $this->belongsToMany(Volunteer::class); + } + /** * Scope a query to include the searched text. */ - public function scopeSearch(Builder $query, string $searchedText): void + public function scopeSearch(Builder $query, string $searchedText): Builder + { + return $query + ->orWhere('name', 'LIKE', "%{$searchedText}%") + ->orWhere('description', 'LIKE', "%{$searchedText}%") + ->orWhere('contact_person', 'LIKE', "%{$searchedText}%") + ->orWhere('website', 'LIKE', "%{$searchedText}%"); + } + + public function scopeWhereAcceptsVolunteers(Builder $query): Builder + { + return $query->where('accepts_volunteers', true); + } + + public function scopeWhereHasVolunteers(Builder $query): Builder + { + return $query->whereHas('volunteers'); + } + + public function scopeWhereHasProjects(Builder $query): Builder + { + return $query->whereHas('projects'); + } + + public function scopeWhereHasActiveProjects(Builder $query): Builder { - $query->orWhere('name', 'LIKE', "%{$searchedText}%"); - $query->orWhere('description', 'LIKE', "%{$searchedText}%"); - $query->orWhere('contact_person', 'LIKE', "%{$searchedText}%"); - $query->orWhere('website', 'LIKE', "%{$searchedText}%"); + return $query->whereRelation('projects', 'status', ProjectStatus::active); } - public function getCoverImageAttribute(): string + public function scopeWhereHasEuPlatesc(Builder $query): Builder { - return $this->getFirstMediaUrl('organizationFilesLogo', 'preview') ?? ''; + return $query->whereNotNull('eu_platesc_merchant_id') + ->whereNotNull('eu_platesc_private_key'); } - public function getStatuteLinkAttribute(): string + public function scopeWhereHasDonations(Builder $query): Builder { - return $this->getFirstMediaUrl('organizationFilesStatute') ?? ''; + return $query->whereHas('projects.donations'); } - public function getAdministrator() + public function getAdministrators(): Collection { - return $this->users()->where('role', UserRole::ngo_admin)->first(); + return $this->users() + ->onlyNGOAdmins() + ->get(); } public function getActivitylogOptions(): LogOptions @@ -128,4 +183,20 @@ public function getActivitylogOptions(): LogOptions ->logFillable() ->logOnlyDirty(); } + + public function markAsApproved(): bool + { + return $this->update([ + 'status' => OrganizationStatus::approved, + 'status_updated_at' => $this->freshTimestamp(), + ]); + } + + public function markAsRejected(): bool + { + return $this->update([ + 'status' => OrganizationStatus::rejected, + 'status_updated_at' => $this->freshTimestamp(), + ]); + } } diff --git a/app/Models/Project.php b/app/Models/Project.php index c349c909..07f40691 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -54,6 +54,7 @@ class Project extends Model implements HasMedia 'end' => 'date:Y-m-d', 'accepting_volunteers' => 'boolean', 'accepting_comments' => 'boolean', + 'status' => ProjectStatus::class, ]; protected $appends = ['total_donations', 'cover_image', 'active', 'is_period_active']; @@ -151,8 +152,7 @@ public function getRequiredFieldsForApproval(): array 'end', 'categories', 'reason_to_donate', - 'beneficiaries' + 'beneficiaries', ]; - } } diff --git a/app/Models/RegionalProject.php b/app/Models/RegionalProject.php index 1969aa3c..5ee16043 100644 --- a/app/Models/RegionalProject.php +++ b/app/Models/RegionalProject.php @@ -1,5 +1,7 @@ 'boolean', 'contact_info' => 'array', ]; - protected $appends = ['cover_image', 'type']; + protected $appends = ['cover_image', 'type']; public function registerMediaConversions(Media $media = null): void { @@ -62,6 +64,7 @@ public function registerMediaConversions(Media $media = null): void ->fit(Manipulations::FIT_CROP, 300, 300) ->nonQueued(); } + public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() @@ -72,7 +75,7 @@ public function getActivitylogOptions(): LogOptions public function categories(): BelongsToMany { - return $this->belongsToMany(ProjectCategory::class,'regional_project_category'); + return $this->belongsToMany(ProjectCategory::class, 'regional_project_category'); } public function counties(): BelongsToMany @@ -114,5 +117,4 @@ public function getRequiredFieldsForApproval(): array 'contact_info', ]; } - } diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php index 2f522d43..f6e5359e 100644 --- a/app/Policies/ProjectPolicy.php +++ b/app/Policies/ProjectPolicy.php @@ -21,6 +21,7 @@ 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; } diff --git a/app/Rules/ValidCIF.php b/app/Rules/ValidCIF.php new file mode 100644 index 00000000..845d027c --- /dev/null +++ b/app/Rules/ValidCIF.php @@ -0,0 +1,58 @@ +upper()->startsWith('RO')) { + $value = mb_substr($value, 2); + } + + $value = \intval($value); + } + + if ($value < 10 || $value > 999999999) { + $fail(__('validation.not_regex', ['attribute' => $attribute])); + + return; + } + + $c1 = $value % 10; + $value = (int) ($value / 10); + + $t = 0; + while ($value > 0) { + $t += ($value % 10) * ($key % 10); + $value = (int) ($value / 10); + $key = (int) ($key / 10); + } + + $c2 = $t * 10 % 11; + + if ($c2 === 10) { + $c2 = 0; + } + + if ($c1 !== $c2) { + $fail(__('validation.not_regex', ['attribute' => $attribute])); + } + } +} diff --git a/app/Services/OrganizationService.php b/app/Services/OrganizationService.php new file mode 100644 index 00000000..71d446a3 --- /dev/null +++ b/app/Services/OrganizationService.php @@ -0,0 +1,57 @@ +keys()->first(); + $value = $attributes->get($key); + + if (! \in_array($key, $organization->requiresApproval)) { + $organization->update($attributes->all()); + + return; + } + + return match ($key) { + 'counties' => $organization->counties() + ->sync(collect($value)->pluck('id')), + + 'activity_domains' => $organization->activityDomains() + ->sync(collect($value)->pluck('id')), + + 'logo' => $organization->addMedia($value) + ->toMediaCollection('logo'), + + 'statute' => static::saveStatue($organization, $value), + + default => $organization->fill($attributes->all())->saveForApproval(), + }; + } + + protected static function saveStatue(Organization $organization, UploadedFile $file): void + { + $statute = $organization->addMedia($file) + ->toMediaCollection('statute_pending'); + + activity('pending') + ->event('updated') + ->performedOn($organization) + ->withProperties([ + 'statute' => [ + 'old' => $organization->getFirstMedia('statute')?->id, + 'new' => $statute->id, + ], + ]) + ->log('statute'); + } +} diff --git a/app/Services/ProjectService.php b/app/Services/ProjectService.php index 375eaef7..492237c5 100644 --- a/app/Services/ProjectService.php +++ b/app/Services/ProjectService.php @@ -6,22 +6,19 @@ use App\Enums\ProjectStatus; use App\Enums\UserRole; -use App\Http\Requests\Project\StoreRequest; use App\Models\Project; use App\Models\RegionalProject; use App\Models\User; use App\Notifications\Admin\ProjectCreated as ProjectCreatedAdmin; use App\Notifications\Ngo\ProjectCreated; use Illuminate\Support\Facades\Notification; -use Illuminate\Validation\ValidationData; -use Illuminate\Validation\ValidationException; class ProjectService { private Project|RegionalProject $project; - public function __construct($projectClass=null) - { + public function __construct($projectClass = null) + { if ($projectClass !== null) { $projectClass = new $projectClass; $this->project = $projectClass; @@ -49,6 +46,7 @@ private function createDraftProject(array $data): Project|RegionalProject $data['name'] = 'Draft-' . date('Y-m-d H:i:s') . '-' . auth()->user()->name; } $data['slug'] = \Str::slug($data['name']); + return $this->project::create($data); } @@ -75,17 +73,17 @@ public function changeStatus($id, string $status): void { $this->project = $this->project::findOrFail($id); if ($this->project->status === ProjectStatus::draft && $status === ProjectStatus::pending->value) { - $fields = $this->project->toArray(); - $requiredFields = $this->project->getRequiredFieldsForApproval(); - $missingFields = []; - foreach ($fields as $key => $value) { - if (in_array($key, $requiredFields) && empty($value)) { + $fields = $this->project->toArray(); + $requiredFields = $this->project->getRequiredFieldsForApproval(); + $missingFields = []; + foreach ($fields as $key => $value) { + if (\in_array($key, $requiredFields) && empty($value)) { $missingFields[] = $key; - } - } - if (! empty($missingFields)) { - throw new \Exception('Project is missing required fields for approval, please fill in all required fields . Please fill: '. implode(', ', $missingFields) ); - } + } + } + if (! empty($missingFields)) { + throw new \Exception('Project is missing required fields for approval, please fill in all required fields . Please fill: ' . implode(', ', $missingFields)); + } } $this->project->update(['status' => $status]); if ($status === ProjectStatus::approved->value) { diff --git a/app/Tables/Columns/TitleWithImageColumn.php b/app/Tables/Columns/TitleWithImageColumn.php new file mode 100644 index 00000000..0383562d --- /dev/null +++ b/app/Tables/Columns/TitleWithImageColumn.php @@ -0,0 +1,60 @@ +image = $image; + + return $this; + } + + public function getImage(): ?string + { + return $this->evaluate($this->image); + } + + public function title(string | Closure | null $title): static + { + $this->title = $title; + + return $this; + } + + public function getTitle(): ?string + { + return $this->evaluate($this->title); + } + + public function description(string | Closure | null $description): static + { + $this->description = $description; + + return $this; + } + + public function getDescription(): ?string + { + return $this->evaluate($this->description); + } +} diff --git a/composer.json b/composer.json index a11c3771..9eb4f580 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "laravel/sanctum": "^3.2", "laravel/tinker": "^2.8", "league/flysystem-aws-s3-v3": "^3.15", + "pxlrbt/filament-excel": "^1.1", "spatie/laravel-activitylog": "^4.7", "spatie/laravel-medialibrary": "^10.11", "tightenco/ziggy": "^1.6" diff --git a/composer.lock b/composer.lock index d8c53496..ed5dc5d6 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": "f95bcfe469305ba2d9f19a696be7fe7f", + "content-hash": "deb08c8b83243df28f860cd37adabe74", "packages": [ { "name": "akaunting/laravel-money", @@ -75,6 +75,68 @@ }, "time": "2023-03-16T14:39:27+00:00" }, + { + "name": "anourvalar/eloquent-serialize", + "version": "1.2.15", + "source": { + "type": "git", + "url": "https://github.com/AnourValar/eloquent-serialize.git", + "reference": "d629f46301ffc02e804b33c7c68d720cdf2a221b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/d629f46301ffc02e804b33c7c68d720cdf2a221b", + "reference": "d629f46301ffc02e804b33c7c68d720cdf2a221b", + "shasum": "" + }, + "require": { + "laravel/framework": "^6.0|^7.0|^8.0|^9.0|^10.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "laravel/legacy-factories": "^1.1", + "orchestra/testbench": "~3.6.0|~3.7.0|~3.8.0|^4.0|^5.0|^6.0|^7.0|^8.0", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "EloquentSerialize": "AnourValar\\EloquentSerialize\\Facades\\EloquentSerializeFacade" + } + } + }, + "autoload": { + "psr-4": { + "AnourValar\\EloquentSerialize\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Laravel Query Builder (Eloquent) serialization", + "homepage": "https://github.com/AnourValar/eloquent-serialize", + "keywords": [ + "anourvalar", + "builder", + "copy", + "eloquent", + "job", + "laravel", + "query", + "querybuilder", + "queue", + "serializable", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/AnourValar/eloquent-serialize/issues", + "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.2.15" + }, + "time": "2023-07-14T07:49:10+00:00" + }, { "name": "awcodes/filament-tiptap-editor", "version": "v2.6.5", @@ -638,6 +700,87 @@ }, "time": "2023-02-15T15:17:37+00:00" }, + { + "name": "composer/semver", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-04-01T19:23:25+00:00" + }, { "name": "danharrin/date-format-converter", "version": "v0.3.0", @@ -1163,6 +1306,67 @@ ], "time": "2023-01-14T14:17:03+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.16.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8", + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0" + }, + "time": "2022-09-18T07:06:19+00:00" + }, { "name": "filament/filament", "version": "v2.17.52", @@ -3408,6 +3612,86 @@ ], "time": "2023-08-11T04:02:34+00:00" }, + { + "name": "maatwebsite/excel", + "version": "3.1.48", + "source": { + "type": "git", + "url": "https://github.com/SpartnerNL/Laravel-Excel.git", + "reference": "6d0fe2a1d195960c7af7bf0de760582da02a34b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/6d0fe2a1d195960c7af7bf0de760582da02a34b9", + "reference": "6d0fe2a1d195960c7af7bf0de760582da02a34b9", + "shasum": "" + }, + "require": { + "composer/semver": "^3.3", + "ext-json": "*", + "illuminate/support": "5.8.*|^6.0|^7.0|^8.0|^9.0|^10.0", + "php": "^7.0|^8.0", + "phpoffice/phpspreadsheet": "^1.18", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "orchestra/testbench": "^6.0|^7.0|^8.0", + "predis/predis": "^1.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Maatwebsite\\Excel\\ExcelServiceProvider" + ], + "aliases": { + "Excel": "Maatwebsite\\Excel\\Facades\\Excel" + } + } + }, + "autoload": { + "psr-4": { + "Maatwebsite\\Excel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Patrick Brouwers", + "email": "patrick@spartner.nl" + } + ], + "description": "Supercharged Excel exports and imports in Laravel", + "keywords": [ + "PHPExcel", + "batch", + "csv", + "excel", + "export", + "import", + "laravel", + "php", + "phpspreadsheet" + ], + "support": { + "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.48" + }, + "funding": [ + { + "url": "https://laravel-excel.com/commercial-support", + "type": "custom" + }, + { + "url": "https://github.com/patrickbrouwers", + "type": "github" + } + ], + "time": "2023-02-22T21:01:38+00:00" + }, { "name": "maennchen/zipstream-php", "version": "3.1.0", @@ -3489,6 +3773,113 @@ ], "time": "2023-06-21T14:59:35+00:00" }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, { "name": "masterminds/html5", "version": "2.8.1", @@ -4187,6 +4578,111 @@ }, "time": "2022-06-14T06:56:20+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.29.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fde2ccf55eaef7e86021ff1acce26479160a0fa0", + "reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.15", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^7.4 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^1.0 || ^2.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0 || ^10.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.0" + }, + "time": "2023-06-14T22:48:31+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.1", @@ -4802,6 +5298,73 @@ }, "time": "2023-07-31T14:32:22+00:00" }, + { + "name": "pxlrbt/filament-excel", + "version": "v1.1.13", + "source": { + "type": "git", + "url": "https://github.com/pxlrbt/filament-excel.git", + "reference": "771952cfb26a79fc3da0cf78c916188ccf893dcd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pxlrbt/filament-excel/zipball/771952cfb26a79fc3da0cf78c916188ccf893dcd", + "reference": "771952cfb26a79fc3da0cf78c916188ccf893dcd", + "shasum": "" + }, + "require": { + "anourvalar/eloquent-serialize": "^1.2", + "filament/filament": "^2.13.24|^3.0", + "maatwebsite/excel": "^3.1", + "php": "^8.0" + }, + "require-dev": { + "laravel/pint": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "pxlrbt\\FilamentExcel\\FilamentExcelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "pxlrbt\\FilamentExcel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dennis Koch", + "email": "info@pixelarbeit.de" + } + ], + "description": "Supercharged Excel exports for Filament Resources", + "keywords": [ + "PHPExcel", + "actions", + "filament", + "laravel", + "laravel-filament", + "phpspreadsheet" + ], + "support": { + "issues": "https://github.com/pxlrbt/filament-excel/issues", + "source": "https://github.com/pxlrbt/filament-excel/tree/v1.1.13" + }, + "funding": [ + { + "url": "https://github.com/pxlrbt", + "type": "github" + } + ], + "time": "2023-07-05T19:06:22+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -8965,87 +9528,6 @@ ], "time": "2022-11-17T09:50:14+00:00" }, - { - "name": "composer/semver", - "version": "3.3.2", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Semver\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } - ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.3.2" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2022-04-01T19:23:25+00:00" - }, { "name": "composer/xdebug-handler", "version": "3.0.3", diff --git a/config/activitylog.php b/config/activitylog.php new file mode 100644 index 00000000..e9c53cf1 --- /dev/null +++ b/config/activitylog.php @@ -0,0 +1,54 @@ + env('ACTIVITY_LOGGER_ENABLED', true), + + /* + * When the clean-command is executed, all recording activities older than + * the number of days specified here will be deleted. + */ + 'delete_records_older_than_days' => 365, + + /* + * If no log name is passed to the activity() helper + * we use this default log name. + */ + 'default_log_name' => 'default', + + /* + * You can specify an auth driver here that gets user models. + * If this is null we'll use the current Laravel auth driver. + */ + 'default_auth_driver' => null, + + /* + * If set to true, the subject returns soft deleted models. + */ + 'subject_returns_soft_deleted_models' => false, + + /* + * This model will be used to log activity. + * It should implement the Spatie\Activitylog\Contracts\Activity interface + * and extend Illuminate\Database\Eloquent\Model. + */ + 'activity_model' => \App\Models\Activity::class, + + /* + * This is the name of the table that will be created by the migration and + * used by the Activity model shipped with this package. + */ + 'table_name' => 'activity_log', + + /* + * This is the database connection that will be used by the migration and + * the Activity model shipped with this package. In case it's not set + * Laravel's database.default will be used instead. + */ + 'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'), +]; diff --git a/config/forms.php b/config/forms.php new file mode 100644 index 00000000..b60b3f88 --- /dev/null +++ b/config/forms.php @@ -0,0 +1,70 @@ + [ + + 'actions' => [ + + 'modal' => [ + + 'actions' => [ + 'alignment' => 'left', + ], + + ], + + ], + + 'date_time_picker' => [ + 'first_day_of_week' => 1, // 0 to 7 are accepted values, with Monday as 1 and Sunday as 7 or 0. + 'display_formats' => [ + 'date' => 'j.m.Y', + 'date_time' => 'j.m.Y H:i', + 'date_time_with_seconds' => 'j.m.Y H:i:s', + 'time' => 'H:i', + 'time_with_seconds' => 'H:i:s', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Default Filesystem Disk + |-------------------------------------------------------------------------- + | + | This is the storage disk Filament will use to put media. You may use any + | of the disks defined in the `config/filesystems.php`. + | + */ + + 'default_filesystem_disk' => env('FORMS_FILESYSTEM_DRIVER', 'public'), + + /* + |-------------------------------------------------------------------------- + | Dark mode + |-------------------------------------------------------------------------- + | + | By enabling this setting, your forms will be ready for Tailwind's Dark + | Mode feature. + | + | https://tailwindcss.com/docs/dark-mode + | + */ + + 'dark_mode' => false, + +]; diff --git a/config/tables.php b/config/tables.php new file mode 100644 index 00000000..7dc82953 --- /dev/null +++ b/config/tables.php @@ -0,0 +1,83 @@ + 'j.m.Y', + 'date_time_format' => 'j.m.Y H:i:s', + 'time_format' => 'H:i:s', + + /* + |-------------------------------------------------------------------------- + | Default Filesystem Disk + |-------------------------------------------------------------------------- + | + | This is the storage disk Filament will use to find media. You may use any + | of the disks defined in the `config/filesystems.php`. + | + */ + + 'default_filesystem_disk' => env('TABLES_FILESYSTEM_DRIVER', 'public'), + + /* + |-------------------------------------------------------------------------- + | Dark mode + |-------------------------------------------------------------------------- + | + | By enabling this setting, your tables will be ready for Tailwind's Dark + | Mode feature. + | + | https://tailwindcss.com/docs/dark-mode + | + */ + + 'dark_mode' => false, + + /* + |-------------------------------------------------------------------------- + | Pagination + |-------------------------------------------------------------------------- + | + | This is the configuration for the pagination of tables. + | + */ + + 'pagination' => [ + 'default_records_per_page' => 10, + 'records_per_page_select_options' => [5, 10, 25, 50, -1], + ], + + /* + |-------------------------------------------------------------------------- + | Layout + |-------------------------------------------------------------------------- + | + | This is the configuration for the general layout of tables. + | + */ + + 'layout' => [ + 'actions' => [ + 'cell' => [ + 'alignment' => 'right', + ], + 'modal' => [ + 'actions' => [ + 'alignment' => 'left', + ], + ], + ], + ], + +]; diff --git a/database/factories/PageFactory.php b/database/factories/PageFactory.php index 9e99e7ed..b8588890 100644 --- a/database/factories/PageFactory.php +++ b/database/factories/PageFactory.php @@ -1,5 +1,7 @@ afterCreating(function (Ticket $ticket) { - $ngoAdmin = $ticket->organization->getAdministrator(); + $ngoAdmin = $ticket->organization + ->getAdministrators() + ->first(); + $bbAdmin = User::query() ->role(UserRole::bb_admin) ->first(); diff --git a/database/migrations/2023_05_05_142228_create_organizations_table.php b/database/migrations/2023_05_05_142228_create_organizations_table.php index 5f403e6c..734f9a11 100644 --- a/database/migrations/2023_05_05_142228_create_organizations_table.php +++ b/database/migrations/2023_05_05_142228_create_organizations_table.php @@ -17,7 +17,7 @@ public function up(): void Schema::create('organizations', function (Blueprint $table) { $table->id(); $table->string('name')->index(); - $table->string('cif'); + $table->string('cif')->unique(); $table->text('description'); $table->string('street_address'); $table->string('contact_person'); diff --git a/database/migrations/2023_08_11_133921_create_regional_projects_table.php b/database/migrations/2023_08_11_133921_create_regional_projects_table.php index 3402bacc..5a05263d 100644 --- a/database/migrations/2023_08_11_133921_create_regional_projects_table.php +++ b/database/migrations/2023_08_11_133921_create_regional_projects_table.php @@ -1,5 +1,7 @@ timestamps(); }); Schema::create('project_category', function (Blueprint $table) { - $table->foreignIdFor( Project::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(Project::class)->constrained()->cascadeOnDelete(); $table->foreignIdFor(ProjectCategory::class)->constrained()->cascadeOnDelete(); }); Schema::create('regional_project_category', function (Blueprint $table) { diff --git a/database/migrations/2023_08_29_051126_alter_user_table_add_created_by.php b/database/migrations/2023_08_29_051126_alter_user_table_add_created_by.php index da960a26..755ded4d 100644 --- a/database/migrations/2023_08_29_051126_alter_user_table_add_created_by.php +++ b/database/migrations/2023_08_29_051126_alter_user_table_add_created_by.php @@ -1,5 +1,7 @@ timestamp('status_updated_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('organizations', function (Blueprint $table) { + $table->dropColumn('status_updated_at'); + }); + } +}; diff --git a/database/migrations/2023_08_30_083758_add_approval_columns_to_activity_log_table.php b/database/migrations/2023_08_30_083758_add_approval_columns_to_activity_log_table.php new file mode 100644 index 00000000..bf0a20ea --- /dev/null +++ b/database/migrations/2023_08_30_083758_add_approval_columns_to_activity_log_table.php @@ -0,0 +1,34 @@ +table(config('activitylog.table_name'), function (Blueprint $table) { + $table->timestamp('approved_at')->nullable(); + $table->timestamp('rejected_at')->nullable(); + $table->dropColumn('updated_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->timestamp('updated_at')->nullable(); + $table->dropColumn('rejected_at'); + $table->dropColumn('approved_at'); + }); + } +}; diff --git a/lang/ro/activity.php b/lang/ro/activity.php new file mode 100644 index 00000000..abe79ea9 --- /dev/null +++ b/lang/ro/activity.php @@ -0,0 +1,30 @@ + [ + 'pending' => 'În așteptare', + 'auto_approved' => 'Aprobată automat', + 'approved' => 'Aprobată', + 'rejected' => 'Refuzată', + ], + + 'column' => [ + 'created_at' => 'Data și ora', + 'changed_field' => 'Câmpul editat', + 'causer' => 'Editat de către', + 'status' => 'Status', + ], + + 'value' => [ + 'old' => 'Valoare anterioară', + 'new' => 'Valoare nouă', + ], + 'actions' => [ + 'approve' => 'Aprobă', + 'reject' => 'Respinge', + ], + +]; diff --git a/lang/ro/field.php b/lang/ro/field.php index f321443d..fcc76256 100644 --- a/lang/ro/field.php +++ b/lang/ro/field.php @@ -1,7 +1,10 @@ 'Nume', 'created_at' => 'Creată la', - 'updated_at'=> 'Actualizată la: ', + 'updated_at'=> 'Actualizată la', ]; diff --git a/lang/ro/navigation.php b/lang/ro/navigation.php new file mode 100644 index 00000000..76bab776 --- /dev/null +++ b/lang/ro/navigation.php @@ -0,0 +1,13 @@ + [ + 'manage' => 'Administrează', + 'championship' => 'Campionatul de bine', + 'gala' => 'Gale regionale', + 'content' => 'Conținut', + 'reports' => 'Rapoarte', + ], +]; diff --git a/lang/ro/organization.php b/lang/ro/organization.php index 3f04b490..421bab21 100644 --- a/lang/ro/organization.php +++ b/lang/ro/organization.php @@ -1,22 +1,45 @@ - [ - 'pending' => 'În așteptare', - 'active' => 'Activă', - 'disabled' => 'Inactivă', - ], + 'label' => [ + 'singular' => 'organizație', + 'plural' => 'Organizații', + ], + + 'status_arr' => [ + 'pending' => 'În așteptare', + 'active' => 'Activă', + 'disabled' => 'Inactivă', + ], 'actions' => [ 'view' => 'Vizualizează', 'edit' => 'Editează', 'approve' => 'Aprobă', 'reject' => 'Respinge', + 'deactivate' => 'Dezactivează', + 'reactivate' => 'Reactivează', ], 'organization' => 'Organizație', 'heading' => [ 'in_approval' => 'Cereri noi de înscriere', 'approved' => 'Organizații active', 'rejected' => 'Organizații dezactivate', + 'pending_changes' => 'Organizații aprobate cu modificări în așteptare', + ], + 'filters' => [ + 'counties' => 'Județe', + 'counties_placeholder' => 'Selectează județele', + 'activity_domains' => 'Domenii de activitate', + 'activity_domains_placeholder' => 'Selectează domeniile de activitate', + 'accepts_volunteers' => 'Acceptă voluntari', + 'has_volunteers' => 'Are voluntari', + 'has_projects' => 'Are proiecte', + 'has_active_projects' => 'Are proiecte active', + 'has_eu_platesc' => 'Are EuPlătesc', + 'has_donations' => 'Are donații', + ], 'labels' => [ 'has_statute_file' => 'Are statut', @@ -25,7 +48,7 @@ 'cif' => 'CIF/CUI', 'logo' => 'Logo-ul organizației', 'description' => 'Descriere organizație', - 'activity_domains' => 'Domenis de activitate', + 'activity_domains' => 'Domenii de activitate', 'statute' => 'Statutul organizației', 'volunteering_data' => 'Voluntari', 'accepts_volunteers' => 'Organizația acceptă voluntari?', @@ -39,14 +62,45 @@ 'eu_platesc_data' => 'Date EuPlătesc', 'eu_platesc_merchant_id' => 'Merchant ID', 'eu_platesc_private_key' => 'Key', - 'administrator' => 'Administrator' + 'administrator' => 'Administrator', + 'created_at' => 'Data înscrierii', + 'approved_at' => 'Data adăugării', + 'rejected_at' => 'Data dezactivării', + 'contact_email' => 'Email de contact', + 'contact_phone' => 'Telefon de contact', + 'counties' => 'Județe', + 'has_volunteers' => 'Organizația are voluntari?', + 'has_projects' => 'Organizația are proiecte?', + 'has_active_projects' => 'Organizația are proiecte active?', + 'has_eu_platesc' => 'Organizația are date EuPlătesc?', + 'has_donations' => 'Organizația are donații?', ], - 'messages'=>[ + 'messages' => [ 'update_success' => 'Organizația a fost actualizată cu succes!', 'update_error' => 'A apărut o eroare la actualizarea organizației!', 'approve_success' => 'Organizația a fost aprobată cu succes!', 'approve_error' => 'A apărut o eroare la aprobarea organizației!', 'reject_success' => 'Organizația a fost respinsă cu succes!', 'reject_error' => 'A apărut o eroare la respingerea organizației!', - ] + ], + + 'approve_modal' => [ + 'heading' => 'Aprobă organizația', + 'subheading' => 'Sunteți sigur că doriți să aprobați organizația ":name"?', + ], + + 'reject_modal' => [ + 'heading' => 'Respinge organizația', + 'subheading' => 'Sunteți sigur că doriți să respingeți organizația ":name"?', + ], + + 'deactivate_modal' => [ + 'heading' => 'Dezactivează organizația', + 'subheading' => 'Sunteți sigur că doriți să dezactivați organizația ":name"?', + ], + + 'reactivate_modal' => [ + 'heading' => 'Reactivează organizația', + 'subheading' => 'Sunteți sigur că doriți să reactivați organizația ":name"?', + ], ]; diff --git a/resources/images/organization.png b/resources/images/organization.png new file mode 100644 index 00000000..f4c1b0b1 Binary files /dev/null and b/resources/images/organization.png differ diff --git a/resources/js/Components/Field.vue b/resources/js/Components/Field.vue new file mode 100644 index 00000000..ab598a51 --- /dev/null +++ b/resources/js/Components/Field.vue @@ -0,0 +1,35 @@ + + + diff --git a/resources/js/Components/form/FileInput.vue b/resources/js/Components/form/FileInput.vue index fbd41fed..f9a96092 100644 --- a/resources/js/Components/form/FileInput.vue +++ b/resources/js/Components/form/FileInput.vue @@ -1,46 +1,64 @@ diff --git a/resources/js/Pages/AdminOng/Ong/EditOng.vue b/resources/js/Pages/AdminOng/Ong/EditOng.vue index 256893ed..502a1adb 100644 --- a/resources/js/Pages/AdminOng/Ong/EditOng.vue +++ b/resources/js/Pages/AdminOng/Ong/EditOng.vue @@ -1,4 +1,4 @@ -