Karbantartás menedzsment rendszer - 3. rész: Új Filament erőforrás és a kapcsolatok

Attila | 2023. 09. 17. 17:51 | Olvasási idő: 6 perc

Címkék: #CRUD #Digitalizáció (Digitalization) #Eloquent #Filament #Laravel

Ebben a bejegyzésben egy újabb Filament erőforrást hozunk létre, ami kapcsolatban lesz az előző bejegyzésben létrehozott erőforrással (berendezés típusok és berendezések). Az ismétléseket kihagyva, főleg azokra a részekre koncentrálunk, ami az újdonságot jelenti az új erőforrásnál és a régi-új erőforrások kapcsolatánál.
filament-resource-relationship

Új erőforrás: berendezések

Hozzuk létre neki a model-t és a migrációs fájlt.

php artisan make:model Device -m

Kezdjük a migrációs fájllal és annak mezőivel a devices adattáblában:

$table->string('name')->unique();
$table->string('erp_code')->unique();
$table->foreignId('type_id')->constrained('device_types')->onUpdate('cascade')->onDelete('cascade');
$table->string('plant');
$table->boolean('active')->default(false);
$table->string('history')->nullable();
$table->string('note')->nullable();

Az itt látható mezők természetesen lehetnek mások, de ez egy jó kiindulási pont lehet:

  • name: minden berendezésnek van egy egyedi neve, amit kötelező megadni,
  • az erp_code jelöli, hogy a cég vállalatirányítási rendszerében (vagy más rendszereiben) milyen egyedi azonosítóval rendelkezik az adott berendezés, ez szintén egyedi és kötelezően kitöltendő mező,
  • type_id: külső kulcs, ami a berendezés típusát adja meg a device_types adattáblából, a kapcsolat szabályait kaszkádolással definiáltuk, így, ha törlésre vagy módosításra kerül egy berendezés típus, akkor az tovább gyűrűzik a berendezéseire is. Ez a megfontolás a későbbiekben módosítható, mivel nem biztos, hogy így szeretnénk majd megvalósítani, vagy például, ha integráljuk a rendszert más rendszerekkel, akkor nem biztos, hogy ilyen továbbgyűrűző törléseket végre szeretnénk hajtani...
  • plant: üzem megadása szintén kötelező,
  • active: aktivitást jelző kapcsoló, ami alapértelmezetten hamis értéket tartalmaz azért, hogy a későbbiekben lehessen be-ki kapcsolni az esetleges berendezéseket, ha elromlanak / megjavításra vagy beüzemelésre kerülnek.
  • history: főleg a berendezés korábbi történetét tárolhatjuk itt el röviden, de a karbantartási rendszer működésbe lépésétől majd az új rendszer fogja már nyomon követni a berendezés történetét.
  • note: bármilyen megjegyzés beírható ide, de üresen is lehet hagyni.

Mentés után hajtsuk végre a mingrálást:

php artisan migrate

Ezek alapján az Eloquent ORM-mel kitölthető mezői és az egy-több-es (1-n) kapcsolat a berendezés típusokhoz kapcsolódóan:

protected $fillable = [
  'name',
  'erp_code',
  'type_id',
  'plant',
  'active',
  'history',
  'note'
];

public function type(): BelongsTo
{
  return $this->belongsTo(DeviceType::class, 'type_id');
}

A BelongsTo visszatérési típushoz importáljuk a fájl tetején ezt: Illuminate\Database\Eloquent\Relations\BelongsTo

A berendezés típusokhoz több berendezés is tartozhat, így arra az oldalra is csináljuk meg a kapcsolatot:

public function devices(): HasMany
{
  return $this->hasMany(Device::class, 'type_id');
}

A HasMany visszatérési típushoz importáljuk a fájl tetején ezt: Illuminate\Database\Eloquent\Relations\HasMany

Berendezés erőforrás

Hozzuk létre a berendezés erőforrást, állítsuk be az alapvető értékeit, definiáljuk a létrehozási/szerkesztési űrlapját és a listázó nézetét.

Erőforrás létrehozása és alapbeállításai

php artisan make:filament-resource Device --view

Itt már úgy hoztuk létre az erőforrást, hogy legyen adott hozzá a "show" nézet is, mivel ezt fogják tudni böngészni azok, akik az erőforrás adataihoz (később dokumentumaihoz) hozzáférhetnek, de szerkeszteni / törölni nem szabad majd nekik a berendezéseket.

Alapbeállítások módosítása és hozzáadása a DeviceResource.php-ban:

protected static ?string $navigationIcon = 'heroicon-o-wrench';

public static function getNavigationGroup(): string
{
  return __('module_names.navigation_groups.administration');
}

public static function getModelLabel(): string
{
  return __('module_names.devices.label');
}

public static function getPluralModelLabel(): string
{
  return __('module_names.devices.plural_label');
}

A lang mappa, en és hu almappáiban lévő module_names.php fájlokban pedig adjuk hozzá a berendezésekhez tartozó szótár bejegyzéseket. Példaként a magyart ideteszem, de az angol párjáról se feledkezzünk meg (ha nem menne, akkor érdemes csekkolni a bejegyzés végén linkelt GitHub commit-et):

'devices' => [
  'label' => 'Berendezés',
  'plural_label' => 'Berendezések',
],

Létrehozási és szerkesztési űrlap mező újdonságok

Magán az űrlapon számos elem van, amelyeknek különböző a típusa és az egyes beállításai, finomságai. Előbb bemásolom ide magát az űrlapot, majd utána végigvesszük, hogy melyik elemnél mik a különlegességek. A DeviceResource.php form() metódusának visszatérési form sémájába illesszük be ezt:

Forms\Components\Section::make()
  ->schema([
      Forms\Components\TextInput::make('name')->label(__('fields.name'))
        ->required()
        ->unique(ignoreRecord: true)
        ->maxLength(255),
      Forms\Components\TextInput::make('erp_code')->label(__('fields.erp_code'))
        ->required()
        ->unique(ignoreRecord: true)
        ->maxLength(255),
      Forms\Components\Select::make('type_id')->label(__('fields.type'))
        ->relationship('type', 'name')
        ->searchable()
        ->preload()
        ->createOptionForm([
            Forms\Components\TextInput::make('name')->label(__('fields.name'))
              ->required()
              ->unique()
              ->maxLength(255)
        ])
        ->required(),
      Forms\Components\TextInput::make('plant')->label(__('fields.plant'))
        ->required()
        ->maxLength(255),
      Forms\Components\Toggle::make('active')->label(__('fields.active'))
        ->onColor('success')
        ->offColor('danger')
        ->columnSpan('full'),
      Forms\Components\TextInput::make('history')->label(__('fields.history'))
        ->maxLength(255),
      Forms\Components\TextInput::make('note')->label(__('fields.note'))
        ->maxLength(255),
  ])

Űrlap komponensek:

  • name: ezt már láttuk a berendezés típusoknál, nincs újdonság vele kapcsolatban.
  • erp_code: ugyanaz vonatkozik rá, mint a name-re.
  • type_id: következik a kapcsolatért felelős mező, amely egy select lesz. A relationship() metódusban defináltuk hozzá, hogy melyik model kapcsolatot használja az értékek lekéréséhez (első paraméter) és milyen mező értékei jelenjenek meg a select-ben: berendezés típusok name mezője (második paraméter). A select úgy lett létrehozva, hogy keresőmező szerepel benne alapértelmezetten, ezt a searchable() metódus biztosítja nekünk. A preload() metódussal azt érjük el, hogy a select opciói már hamarabb betöltődjenek, ezáltal egy picit gyorsabb a működése. A createOptionForm() metódussal pedig egy "+" jel fog megjelenni a lenyíló lista jobb szélén, amivel itt a berendezés erőforrás űrlapján "on-the-fly" tudunk új berendezés típust definiálni neki, ha előtte elfelejtettük volna létrehozni neki a típust és így nem muszáj elhagynunk a berendezés létrehozási / szerkesztési űrlapot, mert helyben létre tudjuk ezt neki hozni.
  • plant: nagyjából ugyanaz vonatkozik rá, mint az erp_code-ra, leszámítva az egyediséget.
  • active: az aktivitás jelző egy kapcsoló lesz az űrlapon, az "on" színe a sikert jelző zöld lesz, az "off" színe a veszélyt jelző piros lesz. Magát a kapcsolót egy egész sorban (full) jelenítjük meg az űrlapon.
  • history és note: ugyanaz vonatkozik rájuk, mint a plant-re.

Így néz ki jelenleg bal oldalon a navigációs menü, jobb oldalon pedig a létrehozási űrlap a berendezésekhez. Ha létrehozunk egy új berendezést (validációs hibákat elkerülve), akkor létrehozás utána nézet oldalra jutunk, nem úgy, mint a berendezés típusoknál a szerkesztésre. Ez egy alapbeállítás a Filament-ben, hogy ha van az erőforrásnak View nézete, akkor először azt fogja megjeleníteni a létrehozás után és nem az Edit nézetet. Ugyanez vonatkozik majd a lista megjelenítésben található táblázatra is, tehát ha rákattintunk egy sorra, akkor alapból a View nézet fog majd bejönni és nem a szerkesztési nézet.

Az iménti dolog azért van, mert az erőforrás létrehozásánál a --view kapcsolóval jön létre hozzá a CRUD témaköréből ismert "show" nézet: ez alapértelmezetten azokat a mezőket fogja tartalmazni, amit a létrehozási / szerkesztési űrlap is tartalmaz, csak az űrlap mezői nem lesznek szerkeszthetőek. Ha azt szeretnénk, hogy ne csak "disabled" (nem szerkeszthető) űrlapelemeket szöveges formában láthassunk a "show" nézetben, akkor egy infolist() nevű  metódust kell definiálnunk itt:

public static function infolist(Infolist $infolist): Infolist
{
  return $infolist
    ->schema([
      Infolists\Components\TextEntry::make('name')->label(__('fields.name')),
      Infolists\Components\TextEntry::make('erp_code')->label(__('fields.erp_code')),
      Infolists\Components\TextEntry::make('type.name')->label(__('fields.type')),
      Infolists\Components\TextEntry::make('plant')->label(__('fields.plant')),
      Infolists\Components\TextEntry::make('active')->label(__('fields.active'))
        ->state(function (Model $record): string {
          return $record->active ? __('fields.yes') :  __('fields.no');
        }),
      Infolists\Components\TextEntry::make('history')->label(__('fields.history')),
      Infolists\Components\TextEntry::make('notes')->label(__('fields.note')),
    ]);
}

A szükséges importálásokat (Infolist, TextEntry) hajtsuk végre ahhoz, hogy működjön.

Ezek közül talán a type.name, ami nem érthető egyből, az arra vonatkozik, hogy a Device model osztályban lévő type kapcsolaton keresztül a device_types táblából érkező name mező kerüljön megjelenítésre. Míg az aktivitást jelző mezőnél, ha azt szeretnénk, hogy ne csak 0-t vagy 1-et jelezzen nekünk, akkor az állapotra vonatkozóan egy visszajelzést tudunk adni, hogy igen vagy nem (a megfelelő használathoz a fields.php szótáraiba vegyük fel a yes és no értékeket angolul és magyarul is, illetve importáljuk ezt: Illuminate\Database\Eloquent\Model).

Így a következő nézetet kapjuk a devices/1 útvonalon:

Így a mezők értékét könnyebben tudjuk "kimásolni", mint egy szöveget.

Ha azt szeretnénk, hogy jobb felülről eltűnjön a szerkesztés gomb, akkor a DeviceResource / Pages / ViewDevice.php-ban kell kivennünk az EditAction-t tartalmazó sort.

Én most is azt javaslom, hogy a létrehozás / szerkesztés után léptessen át a rendszer a lista nézetre, úgyhogy a CreateDevice és az EditDevice osztályhoz is hozzáadom ezt a metódust:

protected function getRedirectUrl(): string
{
  return $this->getResource()::getUrl('index');
}

Berendezések lista nézete

Ismét kihangsúlyozom, hogy ne feledjük el azokat, amiket a berendezés típusoknál már megismertünk és használtunk. Most pedig igyekszem új dolgokat mutatni, amelyek szintén hasznosak lehetnek a jövőben.

A DeviceResource.php fájl table() metódusában a columns() metódus visszatérési tömbjét töltsük fel az alábbi tartalommal, utána pedig elmagyarázom a nehezebben érthető részeket:

Tables\Columns\TextColumn::make('id')->searchable()->sortable(),
Tables\Columns\TextColumn::make('name')->label(__('fields.name'))
  ->searchable()->sortable(),
Tables\Columns\TextColumn::make('erp_code')->label(__('fields.erp_code'))
  ->searchable()->sortable(),
Tables\Columns\TextColumn::make('type.name')->label(__('fields.type'))
  ->searchable()->sortable(),
Tables\Columns\IconColumn::make('active')->label(__('fields.active'))
  ->boolean()
  ->action(function($record, $column) {
    $name = $column->getName();
    $record->update([
        $name => !$record->$name
  ]);
}),
Tables\Columns\TextColumn::make('created_at')->label(__('fields.created_at'))
  ->dateTime('Y-m-d H:i')
  ->searchable()->sortable()
  ->toggleable(isToggledHiddenByDefault: true),

Az id, name, erp_code mezőknél nincs újdonság és talán a type.name paramétert is már tudjuk értelmezni, hogy a type kapcsolaton keresztüli másik tábla name mezője fog itt megjelenni. Az aktivitást jelző mező egy ikon típusú mező lesz és a boolean() metódushívás miatt az igaz esetben egy zöld pipa, a hamis esetben egy piros x fog megjelenni az ikonban. Az utána lévő action() metódusban pedig azt definiáljuk, hogy az ikonra kattintással meg tudjuk már itt a táblázatos nézetben változtatni, hogy az adott berendezés az aktív vagy inaktív legyen.

A created_at mezőnél mindössze egy újdonság van, amely a toggleable() metódus és annak paramétere: ez arra vonatkozik, hogy ha túl sok oszlopot szeretnénk itt megjeleníteni a táblázatos nézetben és nem minden fér majd ki rögtön a táblázatban, akkor lehetőségünk van ezeket az oszlopokat elrejteni, ha igazra állítjuk a paramétert. Ekkor a mező csak akkor kerül megjelenítésre, ha a táblázat jobb felső részén lévő három vonal ikonra kattintva előhívjuk az elrejtett oszlopot.

Itt rögtön látható, hogy hiányzik egy fordítás a szótárból, úgyhogy ezt a hiányzó elemet adjuk hozzá a lang / vendor / filament-tables / hu / table.php fájlhoz:

'column_toggle' => [
  'heading' => 'Oszlopok',
],

Ha most elmentjük, akkor a "Columns" felirat helyett már az "Oszlopok" fog ott szerepelni és a "Létrehozva" oszlopnév előtti checkbox bepipálásával megjeleníthető a táblázatban ez az oszlop is.

Bár még csak egy darab aktív/inaktív berendezésünk van, valószínűleg az éles használat során ennél sokkal több lesz majd, úgyhogy hozzunk létre egy szűrő mezőt a táblázat fölé, amivel rögtön szűrhetünk az adott aktivitás értékre. Nyissuk meg szerkesztésre a ListDevices.php fájlt és adjuk hozzá ezeket az osztályhoz:

public function getTabs(): array
{
  return [
    'all' => Tab::make()->label(__('fields.all'))
      ->icon('heroicon-o-list-bullet')
      ->badge(Device::query()->count()),
    'active' => Tab::make()->label(__('fields.active'))
      ->modifyQueryUsing(fn (Builder $query) => $query->where('active', true))
      ->icon('heroicon-o-check-circle')
      ->badge(Device::query()->where('active', true)->count()),
    'inactive' => Tab::make()->label(__('fields.inactive'))
      ->modifyQueryUsing(fn (Builder $query) => $query->where('active', false))
      ->icon('heroicon-o-x-circle')
      ->badge(Device::query()->where('active', false)->count()),
  ];
}

public ?string $activeTab = 'active';

Három tab (lapfül) lesz, amiket már testre is szabtunk itt (a hiányzó szótár bejegyzéseket - all, inactive - vegyük fel a fields.php-ba). A teljes lekérdezéshez (all) nem kell módosítani a lekérdezést, mivel a teljes tartalmat lekérjük. Az aktívnál és az inaktívnál erre szűrünk a where metódussal. Különböző ikonokat állítunk be a három tab-hoz, illetve a badge() metódus segítségével egy számlálót teszünk oda a tab-okhoz, hogy hány berendezés van összessen, továbbá hány aktív és inaktív. A getTabs() metódus után vagy előtt pedig beállíthatjuk azt a szöveges elemet, hogy melyik legyen az aktív tab (nálunk a második lesz, tehát oldalbetöltődés után a második tab lesz kiválasztva).

Widget

Hozzuk létre a widget-et, ami a berendezésekről nyújt nekünk információkat:

php artisan make:filament-widget DeviceOverview --stats-overview

A getStats() metódus visszatérési tömbje pedig legyen ez:

Stat::make('Device', Device::query()->count())->label(__('module_names.devices.plural_label') . ': ' . __('fields.all')),
Stat::make('Device', Device::query()->where('active', true)->count())->label(__('module_names.devices.plural_label') . ': ' . __('fields.active')),
Stat::make('Device', Device::query()->where('active', false)->count())->label(__('module_names.devices.plural_label') . ': ' . __('fields.inactive')),

Az eredmény így fog kinézni a vezérlőpulton:


Erőforrás kapcsolat menedzser

Milyen hasznos is lenne, ha a kiválasztott (szerkesztett) berendezés típus alatt látható lenne a hozzá kapcsolódó berendezések listája is. Ehhez egy erőforrás kapcsolat menedzsert kell létrehoznunk.

php artisan make:filament-relation-manager DeviceTypeResource devices name

Az utasítás első paraméterében a "szülő" erőforrást kell megadni (DeviceTypeResource), második paraméterében azt a táblát, ami "gyerekként" kapcsolódni fog hozzá (devices), harmadik paraméterében pedig azt, hogy ennek a "gyereknek" melyik mezője kerüljön megjelenítésre a lista táblázatban és a létrehozási/szerkesztési űrlapban. Az utasítás kiadásának hatására létrejött a DeviceTypeResource mappában a RelationManagers mappa, amin belül a DevicesRelationManager.php

Ennek az új fájlnak a tartalma nagyon hasonlít az erőforrás fájlok szerkezetére (form() és table() metódus az ismert funkcionalitásaikkal). De mielőtt rátérnénk ennek a fájlnak a szerkesztésére, előtte (ahogy a terminal is figyelmeztetett rá) regisztráljuk be a kapcsolatot a DeviceTypeResource getRelations() metódusába, annak visszatérési tömbjébe helyezzük el ezt:

RelationManagers\DevicesRelationManager::class,

A kapcsolat már elő is állt a kiválasztott berendezés típus szerkesztési oldalán:

Az rögtön látszódik, hogy az alsó kapcsolati táblázatban csak a berendezés neve látható, mivel ezt adtuk meg az erőforrás kapcsolat menedzsert létrehozó utasításban. Az is látszik még, hogy a többnyelvűsítés itt még nem működik teljesen jól, de ezt is mindjárt orvosoljuk.

Visszatérhetünk a DevicesRelationManager.php-hoz: kezdjük azzal, hogy az új berendezés létrehozása űrlap megjelenésekor csak egy név mező jelenne meg, ami nyilván kevés és reklamálna is a rendszer, hogy ha csak a név mezőt töltenénk ki a létrehozáshoz a többi kötelező mező nélkül. Ami még felmerülhetne, hogy a berendezés létrehozási űrlap elemeit ide is bemásoljuk (illetve kiszervezhetnénk egy metódusba, amit utána mindkét helyen csak meghívnánk). De csináljuk inkább meg úgy, hogy itt a DevicesRelationManager-ben a form() metódus magját figyelmen kívül hagyjuk és a table() akcióira koncentrálunk: ha a felhasználó rákattint az adott erőforrás létrehozására, szerkesztésére, törlésére, akkor a már létező űrlapok jelenjenek meg neki. Így ha például itt a berendezés létrehozási vagy szerkesztési űrlapon a "Mégsem" gombra nyomunk, akkor újra visszairányít minket az aktuálisan szerkesztett berendezés típushoz. Itt a berendezés törlést véleményem szerint ne engedélyezzük, úgyhogy az erre vonatkozó akciókat vegyük ki a táblázat akciói közül. A táblázat oszlopaiban pedig csak annyi mezőt helyezzünk el, amennyi szükséges az azonosításhoz (persze a mezők köre szabadon bővíthető).

return $table
  ->recordTitleAttribute('name')
  ->columns([
    Tables\Columns\TextColumn::make('name')->label(__('fields.name'))
      ->searchable()->sortable(),
    Tables\Columns\TextColumn::make('erp_code')->label(__('fields.erp_code'))
      ->searchable()->sortable(),
  ])
  ->filters([
    //
  ])
  ->headerActions([
    Tables\Actions\CreateAction::make()->url(fn (): string => DeviceResource::getUrl('create')),
  ])
  ->actions([
    Tables\Actions\ViewAction::make()->url(fn (Model $record): string => DeviceResource::getUrl('view', ['record' => $record])),
    Tables\Actions\EditAction::make()->url(fn (Model $record): string => DeviceResource::getUrl('edit', ['record' => $record])),
  ])
  ->bulkActions([])
  ->emptyStateActions([
    Tables\Actions\CreateAction::make()->url(fn (): string => DeviceResource::getUrl('create')),
  ]);

A headerActions() és az emptyStateActions() metódusokban az új berendezés létrehozásához definiálom az akciót. Az actions() részben pedig megtekintésre és szerkesztésre van lehetőség a definiált URL-eken keresztül. Ehhez persze szükség van a fájl elején a DeviceResource és a Model importálására:

use Illuminate\Database\Eloquent\Model;
use App\Filament\Resources\DeviceResource;

Többnyelvűsítés kapcsán itt magának a kapcsolatnak a kezelése adja alapértelmezetten a bal felső sarokban lévő táblázat címét, amit így tudunk átírni (a getTitle() metódus felüldefiniálásával):

public static function getTitle(Model $ownerRecord, string $pageClass): string
{
  return __('module_names.devices.plural_label');
}

Míg az új elem felvétele gomb ugyanúgy működik, mint korábban az erőforrásnál, definiáljuk felül a getModelLabel() metódust:

public static function getModelLabel(): string
{
  return __('module_names.devices.label');
}

Így most már megfelelő lesz a kinézete és az akciógombjainak az útvonalai is a szerkesztett berendezés típus alatti táblázatban:


Összegzés

A bejegyzésben létrehoztunk egy új Filament erőforrást, ami a berendezések kezelését teszi könnyebbé, hatékonyabbá, kényelmesebbé. Bővítettük tudásunkat a Filament-tel kapcsolatban úgy, hogy a "régi" dolgokat gyakoroltuk, az újakat pedig kipróbáltuk, hogy utána majd ezeket is csak gyakorlásként tudjuk alkalmazni a további erőforrásoknál. Új űrlap típusokat ismertünk meg, táblázat akciókat, szűrési lehetőségeket. Végül pedig egy erőforrás kapcsolat (berendezés típusok - berendezések 1-n) kezelését is hatékonyan végrehajtottuk. A többnyelvűsítésre mindvégig figyeltünk és ahol csak lehetett, alkalmaztuk azt.

A bejegyzésben végrehajtott változtatások GitHub commit-je itt található meg.

A közvetlen folytatásban: most már lesz értelme a QR kódoknak, hiszen azok alapján fogjuk tudni majd elirányítani a karbantartókat az adott berendezéshez (és a berendezés alatt kilistázásra kerülő dokumentumokhoz, de ez majd csak utána következik). Így tehát a QR kódok beépítésével fogjuk folytatni a karbantartás menedzser rendszer építését.


Dolgozzunk együtt: egyéni oktatás, mentorálás, fejlesztés, tanácsadás

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!