Címkék: #Digitalizáció (Digitalization) #Eloquent #Engedélyeztetés (Authorization) #Érvényesítés (Validation) #Felhasználók (Users) #Filament #Ipar 4.0 (Industry 4.0) #Jogosultság (Permission) #Laravel #Laravel 10 #Szerepkörök (Roles) #Telepítés (Installation) #Többnyelvűsítés (Localization) #Űrlap (Form)
Az egyik célunkat szeretnénk elérni a munkalapok feldolgozása során. Az volt az elvárás, hogy a gépkezelők tudjanak bejelenteni hibákat a berendezésekkel kapcsolatban és ezeket strukturáltan tudja eltárolni a Karbantartás Menedzsment Rendszer. Ezt még azzal is kiegészítjük, hogy a karbantartó szerepkörű felhasználok meg tudják határozni, hogy az a hiba mennyire okoz problémát, illetve ebből adódóan mennyire sürgős a javítása. Nyilván az a legsürgősebb javítási feladat, ami miatt esetleg megállhatna a termelés. A karbantartók tehát sürgősségi szintet, határidőt is tudnak definiálni majd a munkalapokon. Ezen felül a munkalapok segítségével több mindent is nyomon lehet majd követni a rendszer segítségével:
Tervezés szempontjából a munkalapokat tároló adattábla (bal felül: worksheets) így illeszkedhet be az adatbázis szerkezetébe: az adatbázis diagram most releváns részei:
Megjegyzés:
a Spatie Laravel Permission csomagnak éppen a munkám során jött ki a
legfrissebb fő verziója (6-os), így azt érdemes frissíteni úgy, hogy a
composer.json fájlban a "require" szekcióba beírjuk ezt a megfelelő
helyre: "spatie/laravel-permission": "^6.0" majd után composer update paranccsal érvényre is juttatjuk a legfrissebb csomagverziók használatát.
Hozzuk létre a Model-t és a migrációs fájlt (controller-re és factory-ra nincs most még szükségünk):
php artisan make:model Worksheet -m
Építsük fel az adattábla szerkezetét a migrációs fájlban:
public function up(): void
{
Schema::create('worksheets', function (Blueprint $table) {
$table->id();
$table->enum('priority', ['Normál', 'Sürgős', 'Leálláskor'])->default('Normál');
$table->text('description');
$table->date('due_date')->nullable();
$table->date('finish_date')->nullable();
$table->foreignId('device_id')->constrained()->onUpdate('no action')->onDelete('no action');
$table->unsignedBigInteger('creator_id');
$table->foreign('creator_id')->references('id')->on('users')->onUpdate('no action')->onDelete('no action');
$table->unsignedBigInteger('repairer_id')->nullable();
$table->foreign('repairer_id')->references('id')->on('users')->onUpdate('no action')->onDelete('no action');
$table->text('attachments');
$table->text('comment')->nullable();
$table->timestamps();
});
}
Magyarázatok:
Futtathatjuk a migrálást:
php artisan migrate
A migrációs fájl alapján gyűjtsük össze, hogy milyen mezőket kell engednünk kitölteni! A $fillable mező tömbjét bővítsük ki az alábbiak szerint:
protected $fillable = [
'priority',
'description',
'due_date',
'finish_date',
'device_id',
'creator_id',
'repairer_id',
'attachments',
'comment',
];
Két mező "kasztolását" (adatbázis mezőjének a típusát másképp használjuk a PHP programozás során, mindezt automatikusan) végezzük el a Model osztályban:
protected $casts = [
'priority' => WorksheetPriority::class,
'attachments' => 'array',
];
A WorksheetPriority-t az app / Enums új könyvtárban kell majd létrehozni WorksheetPriority.php fájlnévvel, amit itt a Worksheet Model osztályban importálni is kell. A WorksheetPriority.php osztály tartalmát úgy kell összeállítani, hogy előtte a Filament specifikus dolgokat áttekintjük a dokumentációban: https://filamentphp.com/docs/3.x/support/enums Az új WorksheetPriority.php fájl tartalmaz ezekután:
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
use Filament\Support\Contracts\HasColor;
enum WorksheetPriority: string implements HasLabel, HasColor, HasIcon
{
case NORMAL = 'Normál';
case SURGOS = 'Sürgős';
case LEALLASKOR = 'Leálláskor';
public function getLabel(): ?string
{
return match ($this) {
self::NORMAL => 'Normál',
self::SURGOS => 'Sürgős',
self::LEALLASKOR => 'Leálláskor',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::NORMAL => 'warning',
self::SURGOS => 'danger',
self::LEALLASKOR => 'gray',
};
}
public function getIcon(): ?string
{
return match ($this) {
self::NORMAL => 'heroicon-m-exclamation-circle',
self::SURGOS => 'heroicon-m-exclamation-triangle',
self::LEALLASKOR => 'heroicon-m-sun',
};
}
}
Az
osztályon belül definiáljuk a három sürgősségi szintet, mint prioritási
lehetőséget. A metódusokkal pedig definiáljuk a címke nevét
(getLabel()), a színét (getColor()) és végül az ikonját (getIcon()),
amik majd látszódni fognak a Filament erőforrás táblázatában.
Kössük hozzá először a Worksheet Model osztályt a "társaihoz"! Mindegyik kapcsolat 1-n típusú.
public function device(): BelongsTo
{
return $this->belongsTo(Device::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'creator_id'); // aki létrehozta a munkalapot
}
public function repairer(): BelongsTo
{
return $this->belongsTo(User::class, 'repairer_id'); // aki karbantartó!
}
A BelongsTo osztályt importálni kell a fájl tetején, az odaírt kommentek pedig beszédesek, hogy melyik-melyik kapcsolatért felelős.
Másik oldalról, mindezt a Device osztályban:
public function worksheets(): HasMany
{
return $this->hasMany(Worksheet::class);
}
Majd a User osztályban:
public function creators_worksheets(): HasMany
{
return $this->hasMany(Worksheet::class, 'creator_id');
}
public function repairers_worksheets(): HasMany
{
return $this->hasMany(Worksheet::class, 'repairer_id');
}
A HasMany osztályt importálni kell a fájl tetején.
Hozzuk létre a munkalap erőforrást úgy, hogy generáljuk a létrehozott adattáblájanak szerkezete alapján. Azonban, ha megpróbálnánk ezt így létrehozni, akkor figyelmeztetést kapnánk, mivel alapértelmezetten a rendszer nem ismeri az enum adattábla típust.
Maga az erőforrás létrejönne, viszont nem töltené fel tartalommal az űrlap és táblázat megjelenítő és beállító metódusokat. Emiatt néhány módosítást végre kell hajtanunk a megfelelő működéshez. Nyissuk meg az app / Providers / AppServiceProvider.php fájlt és annak a boot() metódusát bővítsük:
DB::connection()
->getDoctrineSchemaManager()
->getDatabasePlatform()
->registerDoctrineTypeMapping('enum', 'string');
Hozzá pedig importáljuk be a DB facade-ot a fájl elején: use Illuminate\Support\Facades\DB;
Utána telepítsük a doctrine/dbal csomagot a generálás zökkenőmentessé tétele miatt:
composer require doctrine/dbal --dev
Majd következhet az erőforrás létrehozása generálással (adattábla alapján):
php artisan make:filament-resource Worksheet --generate
Így a form() és table() metódusok és lényegi tartalommal töltődtek fel az újonnan létrejövő WorksheetResource.php fájlban.
Bővítsük ki a WorksheetResource osztályt az alábbi alapbeállításokkal:
protected static ?string $navigationIcon = 'heroicon-o-document-plus';
public static function getNavigationGroup(): string
{
return __('module_names.navigation_groups.failure_report');
}
protected static ?int $navigationSort = 7;
public static function getModelLabel(): string
{
return __('module_names.worksheets.label');
}
public static function getPluralModelLabel(): string
{
return __('module_names.worksheets.plural_label');
}
A hiányzó fordítás részeket a module names szótárban bővítsük ki: a 'navigation_groups'-hoz adjuk hozzá a 'failure_report'-ot 'Failure report' névvel angolul és 'Hibabejelentés' névvel magyarul. Továbbá egy 'worksheets' szekciót a 'documents'-hez hasonlóan.
A megszokott
módon pedig a CreateWorksheet és EditWorksheet osztályokhoz adjuk hozzá a
getRedirectUrl() metódust, amellyel a mentés után elirányítjuk a
felhasználót a munkalapok index oldalára.
Az
űrlap lesz a feladatban a legbonyolultabb: azért is ez, mivel különböző
fajta ellenőrzéseket, érvényesítéseket kell a bemeneti elemekhez
illeszteni, illetve a bemutató további részében jogosultságokhoz is
kötjük majd az egyes elemek kitöltésének / szerkesztésének
engedélyezését, így ami ebben az alfejezetben elkészül űrlap, az még nem
a végleges megoldása lesz ennek. Itt van az űrlap első változata,
alatta pedig az egyes mezők magyarázatai következnek:
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make()->schema([
Forms\Components\Select::make('device_id')->label(__('module_names.devices.label'))
->relationship('device', 'name')
->required(),
Forms\Components\Select::make('creator_id')->label(__('fields.creator'))
->relationship('creator', 'name')
->required(),
Forms\Components\Select::make('repairer_id')->label(__('fields.repairer'))
->options(User::role('repairer')->get()->pluck('name', 'id')),
Forms\Components\Select::make('priority')->label(__('fields.priority'))
->options(WorksheetPriority::class)
->default('Normal')
->required(),
Forms\Components\Textarea::make('description')->label(__('fields.description'))
->required()
->maxLength(65535)
->columnSpanFull(),
Forms\Components\DatePicker::make('due_date')->label(__('fields.due_date'))
->minDate(now()),
Forms\Components\DatePicker::make('finish_date')->label(__('fields.finish_date'))
->minDate(now())
->default(now()),
Forms\Components\FileUpload::make('attachments')->label(__('fields.attachments'))
->required()
->multiple()
->preserveFilenames()
->openable()
->downloadable()
->columnSpanFull(),
Forms\Components\Textarea::make('comment')->label(__('fields.note'))
->maxLength(65535)
->columnSpanFull(),
])
]);
}
Részletezések:
Tipp:
ha úgy szeretnénk beállítani, hogy lehessen késés is a karbantartásnál,
vagyis a befejezés dátuma későbbi legyen, mint a megadott határidő,
akkor ezt is érdemes lekezelni valahogy. Például ha későbbi a befejezés
dátuma, mint a határidő dátuma, akkor automatikusan kerüljön majd
kiszűrésre, hogy ez egy késedelmes feladat; esetleg legyen egy külön
opció (bool) eltárolva az adattáblában, hogy késős-e a feladat, mert
akkor be tudunk állítani egy maximális dátumot a befejezés dátumára, ami
a határidő lesz és csak akkor engedünk ennél későbbi dátumot beállítani
neki, ha átváltja a kapcsolót a karbantartó, hogy ez már egy késedelmes
javítás dokumentálása.
A táblázatban a hibás berendezés nevét, a hiba rövid leírását, a karbantartás prioritását és a létrehozás dátumát (idejét) helyezzük el láthatóan, a többi mező is maradhat, viszont alapértelmezetten rejtsük el őket. Az erőforrás automatikus generálása már jó néhányat mezőt elhelyezett a table() columns()-t érintő metódus visszatérési tömbjében ezek közül, úgyhogy nagyjából csak válogatásra és esetleg pontosításra van szükségünk.
Tables\Columns\TextColumn::make('id')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('created_at')->label(__('fields.created_at'))
->dateTime('Y-m-d H:i')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('device.name')->label(__('module_names.devices.label'))
->numeric()
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('description')->label(__('fields.description'))
->limit(30)
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('priority')->label(__('fields.priority'))
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('creator.name')->label(__('fields.creator'))
->numeric()
->searchable()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('repairer.name')->label(__('fields.repairer'))
->numeric()
->searchable()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('due_date')->label(__('fields.due_date'))
->date('Y-m-d')
->searchable()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('finish_date')->label(__('fields.finish_date'))
->date('Y-m-d')
->searchable()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')->label(__('fields.updated_at'))
->dateTime('Y-m-d H:i')
->searchable()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Az itt látható oszlop tulajdonságokat már mind ismerjük, kivétel talán a description (leírás) oszlopnál látható limit() segédmetódust, amelyben 30 karakterben maximalizáltuk azt a hosszt, amelyet a táblázat megmutat nekünk a hiba leírásáról. Ha ennél többet szeretnénk megmutatni, akkor át kell állítani a paraméter értékét, de ha valaki az adatsor értékeinek megtekintésére kattint, akkor úgyis részletesen láthatja a mezők értékeit. Ehhez azonban hozzuk létre a munkalapok megtekintés nézetét:
php artisan make:filament-page ViewWorksheet --resource=WorksheetResource --type=ViewRecord
Így létrejön a WorksheetResource / Pages mappában a ViewWorksheet.php fájl.
Ha pedig a WorksheetResource.php getPages() metódusának visszatérési tömbjéhez hozzáadjuk az alábbi kiemelt sort:
return [
'index' => Pages\ListWorksheets::route('/'),
'create' => Pages\CreateWorksheet::route('/create'),
'view' => Pages\ViewWorksheet::route('/{record}'),
'edit' => Pages\EditWorksheet::route('/{record}/edit'),
];
Így most már a table() metódusban a visszatérési $table objektum actions() metódusához hozzáadhatjuk (elsőként) a ViewAction-t és akkor a táblázatos nézetben egyből fel fog tűnni a Megtekintés link a sorokban.
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
])
Szeretnénk viszont megfordítani a sorrendet, így
adjuk hozzá a table() metódus visszatérési $table tömbjéhez ezt a
metódushívást (első paraméter az, hogy melyik oszlop szerint rendezzünk,
alapértelmezetten növekvőben, hacsak nem adjuk meg a második
paraméterben, hogy visszafelé rendezzük):
->defaultSort('created_at', 'desc')
Így a legfrissebb munkalapok fognak legelőször látszódni a táblázatban. Az eredmény itt látható mintaadatok felvitele után:
A prioritás szerinti szűrőket (tab-okat) helyezzük el még a táblázat
felett. A prioritás értékei a WorksheetPriority enum osztály alapján
kerülnek meghatározásra. A ListWorksheets.php -hoz adjuk hozzá még a tab-os szűrőt és az alapértelmezetten aktív tab-ot.
public function getTabs(): array
{
$tabs = [
'all' => Tab::make()->label(__('fields.all'))
->icon('heroicon-o-list-bullet')
->badge(Worksheet::query()->count()),
];
$priorities = array_column(WorksheetPriority::cases(), 'value');
foreach ($priorities as $priority) {
$tabs[$priority] = Tab::make()->label($priority)
->modifyQueryUsing(
fn (Builder $query) => $query
->where('priority', $priority)
)
->badge(
Worksheet::query()
->where('priority', $priority)
->count()
)
->icon(WorksheetPriority::tryFrom($priority)?->getIcon());
}
return $tabs;
}
public ?string $activeTab = 'all';
A
WorksheetPriority Enum osztályt használtuk ehhez, így nyertük ki a
prioritások értékeit, aztán pedig az értékeknek megfelelő címkéket és
ikonokat. Így most már a prioritás szerinti szűrők is működnek:
A
munkalapok menedzselése során különösen fontos, hogy pontosan
átgondoljuk, melyik mezőhöz ki férhet hozzá, ki láthatja, illetve ki
szerkesztheti és mikor.
Hozzuk létre, ahogy már korábban is tettük:
php artisan make:policy WorksheetPolicy --model=Worksheet
Az osztály lényegi tartalma pedig:
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->can('read worksheets');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Worksheet $worksheet): bool
{
return $user->can('read worksheets');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->can('create worksheets');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Worksheet $worksheet): bool
{
return $user->can('update worksheets');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Worksheet $worksheet): bool
{
return $user->can('delete worksheets');
}
Az újonnan létrehozott WorksheetPolicy osztályt érdemes már most bekötni a rendszerbe az AuthServiceProvider osztály $policies tömbjének bővítésével:
Worksheet::class => WorksheetPolicy::class,
Az importálásokról sem feledkezzünk meg az osztály előtt (Worksheet, WorksheetPolicy).
Ahogy említettem, érdemes átgondolni, hogy kinek mit engedélyezünk. A törlést leginkább csak az adminisztrátor felhasználóknak. A frissítést a karbantartóknak és az adminisztrátoroknak. A létrehozásnál már szóba jönnek az operátorok is, akiknek lehetőséget kell erre adni. Mindezeket a beállításokat az adminisztrációs felületen az adminisztrátor felhasználó végre tudja hajtani:
Inkább ne... Felmerülhet az az "issue", hogy ha az adott felhasználó, aki listázza a munkalapokat, nem rendelkezik "update worksheets" jogosultsággal, tehát nem karbantartó és nem adminisztrátor, csak operátor, akkor neki csak azt engedjük meg, hogy a saját maga által létrehozott munkalapokat láthassa. Ez azonban annyiból nem lenne jó, hogy akár különböző operátorok, ugyanahhoz a berendezéshez, ugyanazt a problémát többször, egymástól függetlenül lejelentik a karbantartóknak. Ezt természetesen nem szeretnénk.
Ha valaki mégis megtenné ezt, vagy valamilyen hasonló szűrést alkalmazna, akkor azt a WorksheetResource osztályban kellene definiálnia:
public static function getEloquentQuery(): Builder
{
if (!auth()->user()->can('update worksheets')) {
return parent::getEloquentQuery()->where('creator_id', auth()->user()->id);
}
return parent::getEloquentQuery();
}
Az űrlapot viszont módosítanunk kell több bemenetnél is:
Mindezeket a beállításokat így végezhetjük el a form() metódusban:
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make()->schema([
Forms\Components\Select::make('device_id')->label(__('module_names.devices.label'))
->relationship('device', 'name')
->required(),
Forms\Components\Select::make('creator_id')->label(__('fields.creator'))
->relationship('creator', 'name')
->default(!auth()->user()->can('update worksheets') ? auth()->user()->id : null)
->disabled(!auth()->user()->can('update worksheets') ? true : false)
->required(),
Forms\Components\Select::make('repairer_id')->label(__('fields.repairer'))
->options(User::role('repairer')->get()->pluck('name', 'id'))
->disabled(!auth()->user()->can('update worksheets')),
Forms\Components\Select::make('priority')->label(__('fields.priority'))
->options(WorksheetPriority::class)
->default('Normal')
->required(),
Forms\Components\Textarea::make('description')->label(__('fields.description'))
->required()
->maxLength(65535)
->columnSpanFull(),
Forms\Components\DatePicker::make('due_date')->label(__('fields.due_date'))
->hidden( ! auth()->user()->can('update worksheets'))
->minDate(now()),
Forms\Components\DatePicker::make('finish_date')->label(__('fields.finish_date'))
->disabled(!auth()->user()->can('update worksheets'))
->minDate(now())
->default(now()),
Forms\Components\FileUpload::make('attachments')->label(__('fields.attachments'))
->required()
->image()
->imageEditor()
->imageEditorAspectRatios([
null,
'16:9',
'4:3',
'1:1',
])
->imageEditorEmptyFillColor('#000000')
->imageEditorViewportWidth('1920')
->imageEditorViewportHeight('1080')
->multiple()
->preserveFilenames()
->openable()
->downloadable()
->columnSpanFull(),
Forms\Components\Textarea::make('comment')->label(__('fields.note'))
->maxLength(65535)
->columnSpanFull(),
])
]);
}
A kép szerkesztési modal ablak így néz ki:
Az utóbbi módosításokkal több mezőt (creator_id, repairer_id, finish_date) is letiltottunk az operátor számára, és valamit el is rejtettünk előle, miközben valamelyik mezőnek a kitöltése kötelező a worksheets adattábla kényszerei miatt. Ezeket a kiegészítéseket a CreateWorksheet és EditWorksheet osztályokban tudjuk megtenni ahhoz, hogy ne okozhasson problémát az adattábla feltöltésekor vagy módosításakor. Kényszerek szempontjából csak a creator_id-vel kell foglalkoznunk, a többi mezző nem problémás. CreateWorksheet osztály új metódusa:
protected function mutateFormDataBeforeCreate(array $data): array
{
if (!isset($data['creator_id']))
$data['creator_id'] = auth()->user()->id;
return $data;
}
Ha nincs beállítva a creator_id mező, mert az operátoroknak le van tiltva ennek módosítása már a létrehozáskor, akkor beállítjuk ennek az értéket az adattábla feltöltése előtt.
Ugyanezt kellene megtennünk szerkesztéskor is, viszont csak karbantartók és adminisztrátorok tudják szerkeszteni a munkalapot, akiknél ez a mező nincs letiltva az űrlapon, emiatt nem kell módosítanunk az EditWorksheet osztályt.
Ebben a részben a munkalapokkal foglalkoztam. Elég részletes űrlapot definiáltam nekik, amelyet aztán számos új technikával vérteztem fel validálási és engedélyeztetési oldalról. A fejezetben található programkód módosításokat ebben a GitHub commit-ben lehet megtalálni. (Megjegyzés: a Filament-et is érdemes felfrissíteni, ha már régebben használtuk, úgyhogy ezeket a módosításokat - csak CSS és JS fájlokat - is tartalmazza a GitHub commit.)
A munkalapokkal kapcsolatos rész nem ért itt még véget, folytatom még a finomhangolást, illetve rátérek a munkalapok kapcsolataira. Ezáltal látható lesz a berendezések "kórtörténete" (hiba- és javítási listája), a felhasználók látni fogják a saját munkalapjaikat, hogy hogyan alakult az általuk jelentett hiba javítása, kezelése. Majd rátérek még a widget-ekre is, hiszen a munkalapok sok értékes információt tartalmazhatnak a karbantartási részleg számára és a döntéshozóknak is.
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!