Címkék: #Chart.js #Digitalizáció (Digitalization) #Eloquent #Felhasználók (Users) #Filament #Jogosultság (Permission) #Szerepkörök (Roles) #Többnyelvűsítés (Localization) #Űrlap (Form)
Hozzuk is rögtön létre a szerepkörökhöz tartozó Filament erőforrást:
php artisan make:filament-resource Role
A létrejövő RoleResource.php fájlban töröljük ki a fájl tetején lévő Role osztály importálását:
use App\Models\Role;
Mivel ilyen osztályunk nincsen a Model-ek között, ehelyett mi a Spatie által nyújtott Role osztályt fogjuk használni, szúrjuk be helyette ezt:
use Spatie\Permission\Models\Role;
Állítsuk be a következőket:
Utóbbi kettőt aztán majd a megfelelő szótárban is hozzuk létre.
protected static ?string $navigationIcon = 'heroicon-o-user-group';
public static function getNavigationGroup(): string
{
return __('module_names.navigation_groups.administration');
}
protected static ?int $navigationSort = 1;
public static function getModelLabel(): string
{
return __('module_names.roles.label');
}
public static function getPluralModelLabel(): string
{
return __('module_names.roles.plural_label');
}
Hajtsuk végre továbbá, hogy a CreateRole.php-ban és az EditRole.php-ban a létrehozás és a frissítés után majd az index nézetbe jusson vissza a felhasználó. Erre már korábban többször is láttunk példát, úgyhogy alkalmazzuk a megoldást újra.
Egy jó kiindulási alap, hogy ha a PermissionResource.php-ból átmásoljuk a teljes form() metódusban lévő return utasítást, így a szerepkör neve már rá is kerül a létrehozási/szerkesztési űrlapra. De itt még nem végeztünk, mert definiálni kell a kapcsolatot a jogosultságokhoz, amit egy több elemet tartalmazó listával tudunk megtenni.
Forms\Components\Select::make('permissions')->label(__('module_names.permissions.plural_label'))
->relationship('permissions', 'name')
->multiple()
->preload()
->required(),
Így a háttérben már meglévő role-permission kapcsolat felhasználásával létrejöhet a Filament-es multiple select, amely például az admin szerepkörnél így néz ki (a már lefuttatott seeder használata után):
A "Válassz..." bemeneti mezőbe tudunk írni új értékeket az összerendeléshez, bár itt most éppen az admin-nál az összes jogosultság már hozzá van rendelve a szerepkörhöz. Az egyes jogosultságokat pedig a nevük mellett jobbra lévő x megnyomásával tudjuk törölni a kapcsolatok közül. Az üzleti logika szerint legalább egyetlen jogosultságot minden szerepkörnek "tartalmaznia kell".
A táblázat létrehozásánál is egy nagyon jó alap az, amit a Permission (jogosultságos) erőforrásnál láttunk, és igazából nincs is másra szükség, mint amit ott használtunk, úgyhogy a PermissionResource.php-ból a table() metódus teljes return utasítása átmásolható ide a RoleResource osztály table() metódusába. A kód futásának eredménye itt látható:
Folytathatjuk a munkát a felhasználói erőforrással. Most viszont két új dolgot is kipróbálunk, amelyek segítik a munkánkat az erőforrások kapcsán:
Adjunk hozzá egy új migrációs fájlt a projekthez:
php artisan make:migration add_soft_deletes_to_users_table
A létrejövő migrációs fájl up() és down() metódusaiban implementáljuk a soft deleting funkcionalitás users adattáblát érintő részeit:
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->softDeletes()->after('updated_at');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
Az up()-ban gyakorlatilag létrehozzuk majd az új deleted_at timestamp mezőt az updated_at oszlop után, a down()-ban pedig törölni tudjuk majd a deleted_at mezőt, ha mégsem lenne rá szükségünk a későbbiekben. Hajtsuk végre utána a migrációt:
php artisan migrate
Ezután módosítsuk a User Model osztályt, azon belül használjuk a SoftDeletes trait-et, amit még az osztály előtt importálunk:
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Authenticatable implements FilamentUser
{
use HasApiTokens, HasFactory, Notifiable, HasRoles, SoftDeletes;
A User $fillable tömbhöz pedig adjunk hozzá egy új értéket: 'new_password'. Erre majd a felhasználó szerkesztésénél lesz szükség a későbbiekben, ha új jelszót szeretnénk hozzárendelni egy felhasználóhoz.
A még nem használt User Model mezőket, illetve kulcsokat (password, new_password) hozzuk létre a fields.php szótárakban is és adjunk nekik angol / magyar elnevezéseket.
Most már hozzuk létre az erőforrást:
php artisan make:filament-resource User --soft-deletes --generate
Megjegyzés: ha esetleg hiányolná, akkor telepítsük a projektünkbe a doctrine/dbal csomagot így a terminal-ban: composer require doctrine/dbal
A fields szótárakba hozzuk létre a deleted_at bejegyzéseket "Deleted at" angol és "Törölve" magyar értékekkel.
Az
előbbi erőforrást létrehozó utasítás hatására létrejött a UserResource osztály úgy, hogy a form()
és table() metódusokba is kerültek a users adattábla alapján generált
értékek, de kezdjük az alapbeállításokkal. A UserResource a User Model osztályra épül (ezt importálja a fájl elején) és ez jól is van így.
Hasonlóan a szerepkör erőforráshoz, itt is először az alapbeállításokat definiáljuk:
protected static ?string $navigationIcon = 'heroicon-o-users';
public static function getNavigationGroup(): string
{
return __('module_names.navigation_groups.administration');
}
protected static ?int $navigationSort = 3;
public static function getModelLabel(): string
{
return __('module_names.users.label');
}
public static function getPluralModelLabel(): string
{
return __('module_names.users.plural_label');
}
Ennél is nyilván szükség van arra, hogy az elnevezéseink szerepeljenek a szótárakban, ezeknek a bővítését végezzük el az itteni kódban írt helyeken (module_names.php).
A beállítás után már az eddig véglegesnek tekinthető adminisztrációs menüstruktúránk fog látszódni:
A CreateUser és EditUser osztályokhoz adjuk hozzá azokat a metódusokat, amelyek a mentés / frissítés után visszairányítják a felhasználót a táblázatos index oldalra!
A felhasználókhoz majd csak az admin szerepkörű felhasználók fognak tudni hozzáférni a jövőben, úgyhogy törekedjünk arra, hogy a legteljesebb információhalmazt engedjük módosítani, illetve engedjünk hozzáférni az űrlap és a táblázatos lista kapcsán.
A generálással alapból bekerült jó néhány mező a form() metódus visszatérési tömbjébe:
A password (jelszó) mező kitöltése mindenképpen trükkös egy kicsit, nézzük meg, hogy miért:
Mindezek után így fog kinézni a password-re vonatkozó form()-ban lévő mező definíciója:
Forms\Components\TextInput::make('password')
->password()
->maxLength(255)
->required(static fn (Page $livewire): string => $livewire instanceof CreateUser,)
->dehydrateStateUsing(
fn (?string $state): ?string =>
filled($state) ? Hash::make($state) : null
)
->dehydrated(
fn (?string $state): bool =>
filled($state)
)
->label(
fn (Page $livewire): string => ($livewire instanceof EditUser) ? __('fields.new_password') : __('fields.password')
),
A megfelelő működéshez az importálásokat is hajtsuk végre hozzá az osztály felett:
use Filament\Pages\Page;
use Illuminate\Support\Facades\Hash;
use App\Filament\Resources\UserResource\Pages\EditUser;
use App\Filament\Resources\UserResource\Pages\CreateUser;
A szerepköröket tartalmazó checkbox lista így néz ki (egy szerepkört legalább minden felhasználónál kötelezően elvárunk):
Forms\Components\CheckboxList::make('roles')->label(__('module_names.roles.label'))
->columnSpanFull()
->relationship('roles', 'name')
->columns(3)
->required(),
Az űrlap így néz ki új felhasználó létrehozásánál:
A meglévő felhasználó szerkesztési űrlapja pedig így néz ki:
Vegyük át az egyes részeit ennek a táblázatos megjelenítésnek:
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')->label(__('fields.name'))
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('email')->label(__('fields.email'))
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('roles.name')->label(__('module_names.roles.plural_label'))
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('created_at')->label(__('fields.created_at'))
->dateTime('Y-m-d H:i')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('deleted_at')->label(__('fields.deleted_at'))
->dateTime('Y-m-d H:i')
->sortable()
->searchable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\TrashedFilter::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
Tables\Actions\RestoreAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
Tables\Actions\RestoreBulkAction::make(),
]),
])
->emptyStateActions([
Tables\Actions\CreateAction::make(),
]);
}
Ezek közül a filters()-ben lévő TrashedFilter az, ami számunkra újdonságot jelent. Ennek a szűrőnek az ikonja a táblázat jobb felső sarkában jelenik meg és alapértelmezetten a "Filters" címke látható, ha rákattintással lenyitjuk. Ez azért van, mert a megfelelő fordítás hiányzik hozzá. Nyissuk meg a lang / vendor / filament-tables / hu / table.php fájlt, majd keressük meg a 'filters' tömböt és adjunk hozzá egy új kulcs-érték párost:
'heading' => 'Szűrők',
Utána már a helyes megjelenítés itt látható:
Alapértelmezetten csak az élő (nem törölt) elemeket láthatjuk a táblázatban és a szerkesztési, törlési sor végi funkcionalitásokkal. A törölt elemekre való szűréssel egy üres táblázatot kapunk.
Látható, hogy be van állítva aktív szűrő ("Csak a törölt elemek").
Ha törlünk egy felhasználót (meg is kell erősíteni a törlés tényét egy felugró modal ablakban), akkor ez a táblázatos lista is mutatni fogja, és a tényleges törlés helyett a "Visszaállítás" fog megjelenni funkcióként (ha esetleg a deleted_at - illetve a lefordított változata a "Törölve" - oszlopnév nem látszódna, akkor a jobb felső sarokban lévő három függőleges vonalra kattintva előhívható az addig elrejtett oszlop kipipálással).
Állítsuk vissza utána a felhasználót, hogy ne legyen törölve (ezt szintén meg kell erősíteni egy felugró modal ablakban).
Még egy szűrőt hozzunk létre a táblázatos lista felett, amit tab-okkal valósítunk meg. Nyissuk meg a app / Filament / Resources / UserResource / Pages / ListUsers.php fájlt és adjuk hozzá a getTabs() metódust, valamint az $activeTab tulajdonságot.
public function getTabs(): array
{
$tabs = [
'all' => Tab::make()->label(__('fields.all'))
->icon('heroicon-o-list-bullet')
->badge(User::query()->count()),
];
$roles = Role::all()->pluck('name');
foreach ($roles as $role) {
$tabs[$role] = Tab::make()->label($role)
->modifyQueryUsing(fn (Builder $query) => $query
->whereHas(
'roles', function ($q) use ($role) {
$q->where('name', $role);
})
)
->badge(User::query()
->whereHas(
'roles', function ($q) use ($role) {
$q->where('name', $role);
})->count()
)
->icon('heroicon-o-user-group');
}
return $tabs;
}
public ?string $activeTab = 'all';
Kódmagyarázat:
A kód működéséhez kiegészítő importálásokra van itt is szükségünk a fájl tetején:
use Spatie\Permission\Models\Role;
use Illuminate\Database\Eloquent\Builder;
use Filament\Resources\Pages\ListRecords\Tab;
Utána már látható is lesz a működő kódunk a böngészőben:
Itt látható az eredménye, a példa kedvéért a "repairer" felhasználóhoz egy másik szerepkört is hozzárendeltem a karbantartón kívül. A szerepkör nevére kattintva (például gépkezelő) a táblázatnak egy szűrt eredménysorát láthatjuk:
Amint látható, a szűrés így is működik, kiadja azokat a felhasználókat is, amelyeknek több szerepkörük van, de a kiválasztottal is rendelkeznek.
Megjegyzés: ezek alapján esetleg a berendezés típusok - berendezések kapcsolatban, a berendezések táblázatos listájához lehetne hozzáadni ilyen további tab-os szűrőt, amelyben a berendezés típusok lennének megtalálhatóak.
Legvégül hozzunk létre a felhasználóknak a vezérlőpulton egy widget-et! A widget-ünk ezúttal tartalmazzon egy kördiagramot, illetve "lyukas kördiagramot", amelyen a felhasználók számát láthatjuk szerepkörök szerinti csoportosításban.
php artisan make:filament-widget UsersByRolesChart --chart
Az erőforrást, ami opcionális, ne írjuk be, csak üssünk enter-t, mert majd a vezérlőpultra szeretnénk kitenni a widget-et, úgyhogy a következő kérdésnél írjuk be, hogy admin. Végül a Chart típusának kiválasztásánál a Doughnut-ot (magyarul gyűrű vagy perec diagram), a 2 számot válasszuk! Létre is jön az app / Filament / Widgets / UsersByRoleChart.php fájlunk.
A Filament a diagram widget-jeihez a Chart.js osztálykönyvtárat használja, amelyet már én is több projektemnél alkalmaztam. 2023. novemberében a Filament-tel együtt a Chart.js 3.9.1-es verziója települt a public / js / filament / widgets / components / chart.js helyre. Nekünk ebben az állapotban ez most elegendő, de ha frissíteni szeretnénk, akkor a 4.4-es verzió már elérhető a chart.js weboldalán és egy minimalizált változatával felülírhatő ez a projektünkben lévő fájl.
A fájlban $heading attribútumot törölhetjük, mert a getHeading() metódussal többnyelvűsíteni szeretnénk a diagram widget nevét.
public function getHeading(): string
{
return __('module_names.widgets.usersbyroles');
}
Ehhez nyilván helyezzük el a module_names szótárban a megfelelő bejegyzést.
További két metódus található még alapértelmezetten a fájlban:
Folytassuk az utóbbival a munkát: gyűjtsük össze a szerepkörökhöz tartozó felhasználók darabszámát.
protected function getData(): array
{
$roles = Role::all()->pluck('name');
$data = [];
$colors = ['red', 'green', 'blue'];
foreach ($roles as $role) {
$data[] = User::with('roles')->get()->filter(
fn ($user) => $user->roles->where('name', $role)->toArray()
)->count();
}
return [
'datasets' => [
[
'label' => 'Users by roles',
'data' => $data,
'backgroundColor' => $colors,
],
],
'labels' => $roles,
];
}
Mivel tudom, hogy nálunk a Karbantartás Menedzsment Rendszerben egyelőre három darab szerepkör van, ezért három színt definiáltam a $colors tömbbe, ha többre lenne szükség, ez a tömb is szabadon bővíthető.
Az eredmény itt látható a vezérlőpulton:
Ez már majdnem jó, de még nem tökéletes, mivel teljesen feleslegesek a tengelyek (x, y) és a rácsvonalak is egy ilyen típusú diagramnál. A chart.js szerencsére segít a legkülönfélébb módosítások végrehajtásában, úgyhogy ezt fogjuk tenni a beállításai módosításával a Filament-en keresztül: https://filamentphp.com/docs/3.x/widgets/charts#setting-chart-configuration-options
A dinamikus $options tömb használata helyett, írjuk felül a getOptions() metódust, így ha többnyelvűsíteni szeretnénk az adott diagramot, azt könnyen meg tudjuk tenni, de mindenekelőtt tüntessük el a rácsvonalakat és a tengelyeket.
protected function getOptions(): array
{
return [
'scales' => [
'x' => [
'display' => false,
],
'y' => [
'display' => false,
],
],
'plugins' => [
'title' => [
'display' => true,
'text' => __('module_names.users.plural_label') . ' (' . User::all()->count() . ')',
]
]
];
}
A tengelyek eltüntetésével a rácsvonalak is eltüntek. Ezenkívül még a diagram címét is megjeleníthetjük így, a szöveget pedig dinamikusan szerkeszthetjük, itt éppen úgy, hogy többnyelvűen kiírjuk a felhasználókat, mögötte pedig azt, hogy hány darab - nem törölt - felhasználó van a rendszerben.
Három felhasználónk van, de egyiküknek két szerepköre is van, így alakult ki ez a diagram. Ha az adott gyűrűcikk fölé visszük az egeret, akkor a konkrét szerepkörhöz tartozó felhasználó számot is mutatja.
A bejegyzés során tovább építettük a jogosultsági rendszerünket a Karbantartás Menedzsment Rendszerhez. Bekerültek a szerepkörök, amelyek kezelését még az ismert technikákkal végeztük el. A felhasználó erőforrásnál aztán már sok újdonságot tanultunk meg. Megismerkedtünk a soft deleting technikával, illetve a felhasználói erőforrást már úgy generáltuk a users adattábla alapján. A felhasználó űrlapjánál a jelszó volt a legtrükkösebb mező, így annak implementálását és beállítását elég részletesen vettük. A táblázatos megjelenítésnél a szűrőkre és a funkciókra koncentráltunk. Végül egy diagramot is elhelyeztünk a vezérlőpulton, hogy még látványosabb legyen ez a felület is.
A változtatásokat ebben a GitHub commit-ben lehet megtalálni.
Terveim szerint az erőforrások használatának engedélyeztetésével folytatom a munka bemutatását, illetve az egyéni felhasználói menükkel. Innen lépünk majd tovább a karbantartási munkalapok kezelésére.
Ha egyéni oktatás, mentorálás, vagy fejlesztési projekt kapcsán szeretnél segítséget kérni tőlem, esetleg együttműködni velem, akkor keress meg a weboldalamon található elérhetőségeken keresztül!