Karbantartás menedzsment rendszer - 7. rész: Szerepkör alapú felhasználókezelés a jogosultsági rendszerben

Attila | 2023. 10. 27. 19:34 | Olvasási idő: 6 perc

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)

A jogosultságok elkészítése után építsük fel a szerepkörök és a felhasználók alrendszerét. Mindehhez már a szerepköreink készen is vannak, hiszen a seeder osztály segítségével a szükséges szerepköröket már létre is hoztuk (admin, karbantartó, gépkezelő). Továbbá a hozzájuk kapcsolódó példa felhasználók is elkészültek már (szerepkörönként egy-egy). Így most ezeknek a Filament erőforrásaira koncentrálhatunk.
role-based-access-control

Szerepkör erőforrás

Hozzuk is rögtön létre a szerepkörökhöz tartozó Filament erőforrást:

php artisan make:filament-resource Role

Alapbeállítások

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:

  1. a menüpont ikonját, 
  2. a menüpont csoportja ugyanaz legyen, mint a jogosultságoké (permission resource),
  3. a menüpont sorrendet 1-esre, ez lesz a csoportban a legelső menüpontunk
  4. a többesszámot tartalmazó megnevezést
  5. az egyesszámot tartalmazó megnevezést

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.

Űrlap: bemeneti elemek és validálásuk

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".

Táblázat: oszlopok és akciók

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ó:


Felhasználó erőforrás és kiegészítő részei

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:

  1. Felhasználókat nem szeretnénk majd permanensen törölni, csak úgynevezett "soft delete" lehetőséggel, ami nem törli ténylegesen az adattáblából a felhasználót, csak megjelöli majd úgy, hogy "törölve lett", de ezt az admin szerepkörű felhasználó visszaállíthatja majd. A soft delete opcióról itt olvashatunk bővebben: https://filamentphp.com/docs/3.x/panels/resources/deleting-records#handling-soft-deletes
  2. A soft delete-es törlés után a users adatbázistábla még nem áll készen ennek a funkcionalitásnak a végrehajtására, de az erőforrásban a funkciók már eszerint kerülnek kialakításra. Bővebben a táblához tartozó mezők automatikus generálásáról: https://filamentphp.com/docs/3.x/panels/resources/getting-started#automatically-generating-forms-and-tables

Soft deleting és a Model előkészítése

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.

Létrehozás és alapbeállítások

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!

Űrlap: bemeneti elemek és validálásuk

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:

  1. A name mező maradhat, csak bővítsük ki a többnyelvűsítést szolgáló label() segédfüggvénnyel, ahogy korábban is mindig megtettük.
  2. Az email mező maradhat és a validációja is. Ennek viszont még nem volt bejegyzése a szótárakban, úgyhogy ezt a hiányosságot szüntessük meg, itt pedig a label() segédmetódussal hivatkozzunk az újonnan létrehozott szótárbejegyzésekre. A validáció itt az email() segédmetódussal is bővült.
  3. Az email_verified_at mezőre nem lesz szükségünk most itt, úgyhogy ez a rész törölhető.
  4. A password mezőt gyökeresen átalakítjuk ezen felsorolás után.
  5. A roles (szerepköröket) tartalmazó listát pedig checkbox-ok segítségével jelenítjük meg a felhasználóknak.

A password (jelszó) mező kitöltése mindenképpen trükkös egy kicsit, nézzük meg, hogy miért:

  • A szöveges beviteli mező password típusú, így nem lesz látható majd a beírt szöveg.
  • A jelszó kitöltése kötelező lesz, de csak akkor, ha a létrehozási űrlapon vagyunk, a szerkesztési űrlapon ha nem töltjük ki a jelszó mezőt, akkor a korábban beállított mező érvényben marad, nem változik.
  • Ha egy mező értékét módosítani szeretnénk, mielőtt ténylegesen elküldésre és mentésre kerülne, akkor a dehydrateStateUsing() metódussal tudjuk megtenni. Mi a jelszó kezelését hasheléssel fogjuk bővíteni. További információ erről itt: https://filamentphp.com/docs/3.x/forms/advanced#field-dehydration
  • Magát ezt a módosítást le tudjuk tiltani a dehydrated metódussal, de valósítsuk ezt úgy meg, hogy ha ki van töltve a jelszó mező, akkor gyakorlatilag engedélyezzük a dehydrateStateUsing()-ban definiált módosítás végrehajtását, ha nincs kitöltve a jelszó mező, akkor is engedje az adatok elküldését és elmentését (a régi jelszót meghagyva érvényben). További információ erről itt: https://filamentphp.com/docs/3.x/forms/advanced#auto-hashing-password-field
  • A jelszó mező címkéjét (label) tegyük függővé attól, hogy létrehozzuk (password) vagy módosítjuk (new_password) a felhasználót és vele együtt a jelszavát esetleg.

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:

Táblázat: oszlopok, szűrő és akciók

Vegyük át az egyes részeit ennek a táblázatos megjelenítésnek:

  • A táblázat oszlopainál (columns) jelenítsük meg a felhasználók neveit, e-mail címeit, létrehozási dátumukat és idejüket, valamint adjunk lehetőséget arra, hogy a "deleted_at" törlési mezőt is meg lehessen tekinteni.
  • A szűrőknél (filters) állítsunk be lehetőséget arra, hogy a törölt felhasználókat is meg lehessen jeleníteni.
  • Ennek megfelelően pedig az akcióknál (actions) jelenítsük meg a szerkesztési (edit) mellett a törlési (delete) vagy pedig a helyreállítási (restore) funkciót (ha már törlésre került az adott felhasználó). A törlés funkció alatt nem a végleges, csak az ideiglenes törlést értem. Ha a végleges törlést is szeretnénk engedélyezni, akkor az actions metódus tömbjében a ForceDeleteAction-t, a bulkActions metódus tömbjében pedig a ForceDeleteBulkAction make() metódusát kell hozzáadni.
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:

  • Kezdetben a $tabs-hoz hozzáadjuk az összes felhasználói csoportot jelképező lekérdezést, tab-ot a "lista" ikonnal, valamint jelvényként az összes felhasználó számával.
  • Utána tovább töltjük fel a $tabs tömböt a szerepkörök neveihez rendelünk hozzá neveket (név egyezőség vizsgálat) és szűrő Eloquent lekérdezéseket (modifyQueryUsing()-ben) az eredménylista nagyságával (badge()-ben) együtt.
  • Ikonként mindegyik szerepkörhöz ugyanazt a csoportot jelképező ikont rendeljük.
  • Aktív tabnak (amikor betöltődik a felhasználókat listázó oldal) az összes felhasználó lapot ('all') választjuk ki.

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.

Widget: chart.js kördiagram

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:

  1. getType(): milyen típusú legyen a diagram, jelenleg a 'doughnut'. Az elérhető diagramtípusok itt találhatók meg: https://filamentphp.com/docs/3.x/widgets/charts#available-chart-types
  2. getData(): adatok kigyűjtése a diagramhoz.

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.


Összegzés, továbblépés

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.


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!