Skip to content

Commit 47ecd91

Browse files
authored
Merge pull request #183 from ht3aa/cursor/2026-04-03-gitea-filament-users
Cursor/2026 04 03 gitea filament users
2 parents cd6160a + 97a93e0 commit 47ecd91

203 files changed

Lines changed: 5222 additions & 1370 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,10 @@ LINKEDIN_PERSONAL_CLIENT_SECRET=
8686
LINKEDIN_PERSONAL_ACCESS_TOKEN=
8787
LINKEDIN_PERSONAL_REDIRECT_URI=
8888
LINKEDIN_PERSONAL_PERSON_ID=
89+
90+
# Gitea (Filament: create user via admin API — token needs admin scope)
91+
GITEA_URL=
92+
GITEA_TOKEN=
93+
94+
# Qi Card number shown to admins for first-payment verification (remote work)
95+
QI_CARD_NUMBER=5862997060

app/Enums/ApplicationStatus.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Enums;
4+
5+
enum ApplicationStatus: string
6+
{
7+
case PENDING = 'pending';
8+
case ACCEPTED = 'accepted';
9+
case REJECTED = 'rejected';
10+
11+
public function getLabel(): string
12+
{
13+
return match ($this) {
14+
self::PENDING => 'Pending',
15+
self::ACCEPTED => 'Accepted',
16+
self::REJECTED => 'Rejected',
17+
};
18+
}
19+
20+
public function getColor(): string
21+
{
22+
return match ($this) {
23+
self::PENDING => 'warning',
24+
self::ACCEPTED => 'success',
25+
self::REJECTED => 'danger',
26+
};
27+
}
28+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\CompanyJobs;
4+
5+
use App\Filament\Resources\CompanyJobs\Pages\CreateCompanyJob;
6+
use App\Filament\Resources\CompanyJobs\Pages\EditCompanyJob;
7+
use App\Filament\Resources\CompanyJobs\Pages\ListCompanyJobs;
8+
use App\Filament\Resources\CompanyJobs\Schemas\CompanyJobForm;
9+
use App\Filament\Resources\CompanyJobs\Tables\CompanyJobsTable;
10+
use App\Models\CompanyJob;
11+
use BackedEnum;
12+
use Filament\Resources\Resource;
13+
use Filament\Schemas\Schema;
14+
use Filament\Support\Icons\Heroicon;
15+
use Filament\Tables\Table;
16+
17+
class CompanyJobResource extends Resource
18+
{
19+
protected static ?string $model = CompanyJob::class;
20+
21+
protected static ?string $modelLabel = 'Remote work post';
22+
23+
protected static ?string $pluralModelLabel = 'Remote work posts';
24+
25+
protected static ?string $navigationLabel = 'Remote work';
26+
27+
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedBriefcase;
28+
29+
public static function form(Schema $schema): Schema
30+
{
31+
return CompanyJobForm::configure($schema);
32+
}
33+
34+
public static function table(Table $table): Table
35+
{
36+
return CompanyJobsTable::configure($table);
37+
}
38+
39+
public static function getRelations(): array
40+
{
41+
return [
42+
//
43+
];
44+
}
45+
46+
public static function getPages(): array
47+
{
48+
return [
49+
'index' => ListCompanyJobs::route('/'),
50+
'create' => CreateCompanyJob::route('/create'),
51+
'edit' => EditCompanyJob::route('/{record}/edit'),
52+
];
53+
}
54+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\CompanyJobs\Pages;
4+
5+
use App\Filament\Resources\CompanyJobs\CompanyJobResource;
6+
use Filament\Resources\Pages\CreateRecord;
7+
8+
class CreateCompanyJob extends CreateRecord
9+
{
10+
protected static string $resource = CompanyJobResource::class;
11+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\CompanyJobs\Pages;
4+
5+
use App\Filament\Resources\CompanyJobs\CompanyJobResource;
6+
use Filament\Actions\DeleteAction;
7+
use Filament\Resources\Pages\EditRecord;
8+
9+
class EditCompanyJob extends EditRecord
10+
{
11+
protected static string $resource = CompanyJobResource::class;
12+
13+
protected function getHeaderActions(): array
14+
{
15+
return [
16+
DeleteAction::make(),
17+
];
18+
}
19+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\CompanyJobs\Pages;
4+
5+
use App\Filament\Resources\CompanyJobs\CompanyJobResource;
6+
use Filament\Actions\CreateAction;
7+
use Filament\Resources\Pages\ListRecords;
8+
use Illuminate\Contracts\Support\Htmlable;
9+
10+
class ListCompanyJobs extends ListRecords
11+
{
12+
protected static string $resource = CompanyJobResource::class;
13+
14+
public function getSubheading(): string|Htmlable|null
15+
{
16+
$card = config('services.qi.card_number');
17+
18+
return 'Minimum first payment (Qi Card): '.$card.'. Approve only after payment is confirmed.';
19+
}
20+
21+
protected function getHeaderActions(): array
22+
{
23+
return [
24+
CreateAction::make(),
25+
];
26+
}
27+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\CompanyJobs\Schemas;
4+
5+
use App\Enums\Currency;
6+
use App\Enums\JobStatus;
7+
use App\Enums\WorldGovernorate;
8+
use Filament\Forms\Components\DateTimePicker;
9+
use Filament\Forms\Components\Select;
10+
use Filament\Forms\Components\Textarea;
11+
use Filament\Forms\Components\TextInput;
12+
use Filament\Forms\Components\Toggle;
13+
use Filament\Schemas\Schema;
14+
15+
class CompanyJobForm
16+
{
17+
public static function configure(Schema $schema): Schema
18+
{
19+
return $schema
20+
->components([
21+
Select::make('user_id')
22+
->relationship('user', 'name')
23+
->default(null),
24+
TextInput::make('title')
25+
->required(),
26+
TextInput::make('slug')
27+
->required(),
28+
Textarea::make('description')
29+
->required()
30+
->columnSpanFull(),
31+
TextInput::make('company_name')
32+
->required(),
33+
TextInput::make('email')
34+
->label('Email address')
35+
->email()
36+
->required(),
37+
TextInput::make('contact_link')
38+
->default(null),
39+
Select::make('location')
40+
->options(WorldGovernorate::class)
41+
->default(null),
42+
Select::make('job_title_id')
43+
->relationship('jobTitle', 'name')
44+
->nullable(),
45+
TextInput::make('salary_from')
46+
->numeric()
47+
->default(null),
48+
TextInput::make('salary_to')
49+
->numeric()
50+
->default(null),
51+
Select::make('salary_currency')
52+
->options(Currency::class)
53+
->default('IQD')
54+
->required(),
55+
Textarea::make('requirements')
56+
->default(null)
57+
->columnSpanFull(),
58+
Select::make('status')
59+
->options(JobStatus::class)
60+
->default('pending')
61+
->required(),
62+
Toggle::make('first_payment_qi_confirmed')
63+
->default(false),
64+
TextInput::make('gitea_owner')
65+
->default(null),
66+
TextInput::make('gitea_repo_name')
67+
->default(null),
68+
DateTimePicker::make('gitea_provisioned_at'),
69+
]);
70+
}
71+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\CompanyJobs\Tables;
4+
5+
use App\Enums\JobStatus;
6+
use App\Jobs\ProvisionCompanyJobGiteaJob;
7+
use App\Models\CompanyJob;
8+
use Filament\Actions\Action;
9+
use Filament\Actions\BulkActionGroup;
10+
use Filament\Actions\DeleteBulkAction;
11+
use Filament\Actions\EditAction;
12+
use Filament\Support\Icons\Heroicon;
13+
use Filament\Tables\Columns\IconColumn;
14+
use Filament\Tables\Columns\TextColumn;
15+
use Filament\Tables\Table;
16+
17+
class CompanyJobsTable
18+
{
19+
public static function configure(Table $table): Table
20+
{
21+
return $table
22+
->columns([
23+
TextColumn::make('user.name')
24+
->label('Owner')
25+
->searchable(),
26+
TextColumn::make('title')
27+
->searchable()
28+
->limit(40),
29+
TextColumn::make('company_name')
30+
->searchable()
31+
->toggleable(),
32+
TextColumn::make('status')
33+
->badge()
34+
->sortable(),
35+
IconColumn::make('first_payment_qi_confirmed')
36+
->label('Qi paid')
37+
->boolean(),
38+
TextColumn::make('gitea_owner')
39+
->label('Gitea owner')
40+
->placeholder('')
41+
->toggleable(isToggledHiddenByDefault: true),
42+
TextColumn::make('gitea_repo_name')
43+
->label('Repo')
44+
->placeholder('')
45+
->toggleable(isToggledHiddenByDefault: true),
46+
TextColumn::make('gitea_provisioned_at')
47+
->dateTime()
48+
->sortable()
49+
->toggleable(isToggledHiddenByDefault: true),
50+
TextColumn::make('created_at')
51+
->dateTime()
52+
->sortable()
53+
->toggleable(isToggledHiddenByDefault: true),
54+
])
55+
->filters([
56+
//
57+
])
58+
->recordActions([
59+
EditAction::make(),
60+
Action::make('confirmQiPayment')
61+
->label('Confirm payment')
62+
->icon(Heroicon::OutlinedBanknotes)
63+
->color('gray')
64+
->visible(fn (CompanyJob $record): bool => $record->status === JobStatus::PENDING
65+
&& ! $record->first_payment_qi_confirmed)
66+
->requiresConfirmation()
67+
->modalHeading('Confirm first payment received')
68+
->modalDescription('Confirm that the minimum first payment was sent to the Qi Card number shown in the page subtitle.')
69+
->action(fn (CompanyJob $record): bool => $record->update([
70+
'first_payment_qi_confirmed' => true,
71+
])),
72+
Action::make('approve')
73+
->icon(Heroicon::OutlinedCheckCircle)
74+
->color('success')
75+
->visible(fn (CompanyJob $record): bool => $record->status === JobStatus::PENDING)
76+
->disabled(fn (CompanyJob $record): bool => ! $record->first_payment_qi_confirmed)
77+
->tooltip(fn (CompanyJob $record): ?string => $record->first_payment_qi_confirmed
78+
? null
79+
: 'Confirm Qi Card payment first.')
80+
->requiresConfirmation()
81+
->modalHeading('Approve remote work post')
82+
->modalDescription('Approves the post and provisions a private Gitea repository for the owner.')
83+
->action(function (CompanyJob $record): void {
84+
$record->update(['status' => JobStatus::APPROVED]);
85+
ProvisionCompanyJobGiteaJob::dispatch($record->id);
86+
})
87+
->successNotificationTitle('Post approved and Gitea provisioning queued'),
88+
Action::make('reject')
89+
->icon(Heroicon::OutlinedXCircle)
90+
->color('danger')
91+
->visible(fn (CompanyJob $record): bool => $record->status === JobStatus::PENDING)
92+
->requiresConfirmation()
93+
->action(fn (CompanyJob $record): bool => $record->update([
94+
'status' => JobStatus::REJECTED,
95+
]))
96+
->successNotificationTitle('Post rejected'),
97+
])
98+
->toolbarActions([
99+
BulkActionGroup::make([
100+
DeleteBulkAction::make(),
101+
]),
102+
]);
103+
}
104+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\Users\Pages;
4+
5+
use App\Filament\Resources\Users\UserResource;
6+
use Filament\Resources\Pages\CreateRecord;
7+
8+
class CreateUser extends CreateRecord
9+
{
10+
protected static string $resource = UserResource::class;
11+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\Users\Pages;
4+
5+
use App\Filament\Resources\Users\UserResource;
6+
use Filament\Actions\DeleteAction;
7+
use Filament\Resources\Pages\EditRecord;
8+
9+
class EditUser extends EditRecord
10+
{
11+
protected static string $resource = UserResource::class;
12+
13+
protected function getHeaderActions(): array
14+
{
15+
return [
16+
DeleteAction::make(),
17+
];
18+
}
19+
}

0 commit comments

Comments
 (0)