Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Laravel – Core
/public/fonts/
/public/storage/
/public/storage
/storage/framework/
/storage/generated-json/
/storage/logs/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class () extends Migration {
public function up(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->string('language')->default('en')->after('email_verified_at');
});
}

public function down(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->dropColumn('language');
});
}
};
138 changes: 138 additions & 0 deletions Modules/Core/Filament/Company/Pages/Auth/EditProfile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

namespace Modules\Core\Filament\Company\Pages\Auth;

use Filament\Auth\Pages\EditProfile as BaseEditProfile;
use Filament\Facades\Filament;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Panel;
use Filament\Pages\PageConfiguration;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\Rules\Password;
use Modules\Core\Models\User;
use Modules\Core\Services\UserService;

class EditProfile extends BaseEditProfile
{
protected static ?string $slug = 'my-profile';

/**
* Filament's ->profile() mechanism registers routes outside the
* tenant-prefixed route group, which breaks URL generation for
* tenant-scoped navigation items while this page is active. Register
* this page like a normal tenant page (via ->pages()) instead, so its
* route lives under {tenant}/my-profile.
*/
public static function registerRoutes(Panel $panel, ?PageConfiguration $configuration = null): void
{
Route::name('pages.')->group(fn () => static::routes($panel, $configuration));
}

public static function getRouteName(?Panel $panel = null): string
{
$panel ??= Filament::getCurrentOrDefaultPanel();

return $panel->generateRouteName('pages.' . static::getRelativeRouteName($panel));
}

public function getLayout(): string
{
return 'filament-panels::components.layout.index';
}

public function form(Schema $schema): Schema
{
return $schema
->components([
$this->getNameFormComponent(),
$this->getEmailFormComponent(),
Select::make('language')
->label(trans('ip.language'))
->options(config('languages'))
->default('en')
->required(),
FileUpload::make('avatar')
->label(trans('ip.avatar'))
->avatar()
->disk('public')
->directory('avatars')
->visibility('public'),
$this->getPasswordFormComponent(),
$this->getPasswordConfirmationFormComponent(),
]);
}

protected function mutateFormDataBeforeFill(array $data): array
{
/** @var User $user */
$user = $this->getUser();

$data['avatar'] = $user->avatarUpload?->upload_stored_name;
$data['language'] ??= $user->language ?? 'en';

return $data;
}

protected function handleRecordUpdate(Model $record, array $data): Model
{
/** @var User $record */
$avatarPath = Arr::pull($data, 'avatar');

if (Filament::hasEmailChangeVerification() && array_key_exists('email', $data)) {
if ($data['email'] !== $record->email) {
$this->sendEmailChangeVerification($record, $data['email']);
}

unset($data['email']);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

app(UserService::class)->updateProfile($record, $data);

if (filled($avatarPath)) {
app(UserService::class)->updateAvatar($record, $avatarPath);
} else {
app(UserService::class)->removeAvatar($record);
}

return $record;
}

protected function getPasswordFormComponent(): Component
{
return TextInput::make('password')
->label(trans('filament-panels::auth/pages/edit-profile.form.password.label'))
->validationAttribute(trans('filament-panels::auth/pages/edit-profile.form.password.validation_attribute'))
->password()
->revealable(filament()->arePasswordsRevealable())
->rule(Password::default())
->showAllValidationMessages()
->autocomplete('new-password')
->dehydrated(fn (?string $state): bool => filled($state))
->dehydrateStateUsing(fn (?string $state): string => Hash::make($state))
->same('password_confirmation');
}

protected function getPasswordConfirmationFormComponent(): Component
{
return TextInput::make('password_confirmation')
->label(trans('filament-panels::auth/pages/edit-profile.form.password_confirmation.label'))
->password()
->revealable(filament()->arePasswordsRevealable())
->required()
->visible(fn (Get $get): bool => filled($get('password')))
->dehydrated(false);
}

protected function getSavedNotificationTitle(): ?string
{
return trans('ip.profile_updated');
}
}
59 changes: 59 additions & 0 deletions Modules/Core/Filament/Company/Pages/MyCompanies.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Modules\Core\Filament\Company\Pages;

use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Pages\Page;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Modules\Core\Enums\UserRole;
use Modules\Core\Models\Company;
use Modules\Core\Models\User;

class MyCompanies extends Page implements HasTable
{
use InteractsWithTable;

protected string $view = 'core::filament.company.pages.my-companies';

public function table(Table $table): Table
{
/** @var User $user */
$user = auth()->user();

return $table
->query(fn () => $user->companies()->getQuery())
->columns([
TextColumn::make('name')
->label(trans('ip.name'))
->searchable(),

TextColumn::make('search_code')
->label(trans('ip.search_code')),

TextColumn::make('role')
->label(trans('ip.role'))
->state(fn (): string => $user->getRoleNames()
->map(fn (string $role): string => UserRole::tryFrom($role)?->label() ?? $role)
->implode(', ')),
])
->recordActions([
Action::make('switch')
->label(trans('ip.switch'))
->icon('heroicon-o-arrow-right-start-on-rectangle')
->action(function (Company $record): void {
session(['current_company_id' => $record->id]);
Filament::setTenant($record);

$this->redirect(route('filament.company.pages.dashboard', [
'tenant' => Str::lower($record->search_code),
]));
}),
])
->paginated(false);
}
}
17 changes: 16 additions & 1 deletion Modules/Core/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Modules\Clients\Models\Relation;
use Modules\Core\Database\Factories\UserFactory;
use Modules\Core\Enums\UserRole;
use Modules\Core\Support\ProfileImage\ProfileImageFactory;
use Modules\Expenses\Models\Expense;
use Modules\Invoices\Models\Invoice;
use Modules\Quotes\Models\Quote;
Expand All @@ -31,6 +34,7 @@
* @property string $name
* @property string $email
* @property mixed $email_verified_at
* @property string $language
* @property string $password
* @property string $remember_token
* @property mixed $created_at
Expand Down Expand Up @@ -132,6 +136,13 @@ public function uploads(): HasMany
return $this->hasMany(Upload::class);
}

public function avatarUpload(): MorphOne
{
return $this->morphOne(Upload::class, 'uploadable')
->where('file_description', 'avatar')
->latestOfMany();
}

public function relation(): BelongsTo
{
return $this->belongsTo(Relation::class);
Expand Down Expand Up @@ -165,7 +176,11 @@ public function getFilamentName(): string

public function getFilamentAvatarUrl(): ?string
{
return null;
if ($avatar = $this->avatarUpload) {
return Storage::disk($avatar->upload_disk)->url($avatar->upload_stored_name);
}

return ProfileImageFactory::create()->getProfileImageUrl($this);
}

public function isSuperAdmin(): bool
Expand Down
12 changes: 7 additions & 5 deletions Modules/Core/Providers/CompanyPanelProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Modules\Clients\Filament\Company\Resources\Contacts\ContactResource;
use Modules\Clients\Filament\Company\Resources\Relations\RelationResource;
use Modules\Core\Filament\Company\Pages\Auth\EditProfile;
use Modules\Core\Filament\Company\Pages\Dashboard;
use Modules\Core\Filament\Pages\Auth\EditProfile;
use Modules\Core\Filament\Company\Pages\MyCompanies;
use Modules\Core\Filament\Pages\Auth\Login;
use Modules\Core\Http\Middleware\ConfigureTenant;
use Modules\Core\Http\Middleware\EnsureUserCanAccessCompany;
Expand Down Expand Up @@ -55,9 +56,9 @@ public function panel(Panel $panel): Panel
->path('')
->viteTheme('resources/css/filament/company/nord.css')
->login(Login::class)
->profile(EditProfile::class, isSimple: false)
->passwordReset()
->emailVerification()
->emailChangeVerification()
->maxContentWidth(Width::Full)
->font('Poppins', provider: GoogleFontProvider::class)
->unsavedChangesAlerts()
Expand Down Expand Up @@ -173,6 +174,8 @@ public function panel(Panel $panel): Panel
->discoverWidgets(in: app_path('Filament/Company/Widgets'), for: 'App\Filament\Company\Widgets')
->pages([
Dashboard::class,
EditProfile::class,
MyCompanies::class,
])
->widgets([
RecentQuotesWidget::class,
Expand Down Expand Up @@ -238,10 +241,9 @@ public function panel(Panel $panel): Panel
})
->userMenuItems([
Action::make('switch-company')
->label('Switch Company')
->label(trans('ip.my_companies'))
->icon('heroicon-o-building-office-2')
->modalHeading('Switch Company')
->modalContent(fn () => view('filament.company.widgets.switch-company-table')),
->url(fn () => MyCompanies::getUrl()),
'profile' => fn (Action $action) => $action
->label(trans('ip.edit_profile'))
->icon('heroicon-o-user')
Expand Down
Loading
Loading