chore: introduce role management functionality with DTOs and Livewire components

- Added Role and User DTOs for dynamic role handling.
- Implemented table and badge components for displaying role data.
- Registered a Blade directive for scoped slots in `ScopeServiceProvider`.
- Updated dashboard views to include role management section.
This commit is contained in:
= 2026-05-08 13:15:00 +00:00
parent 4a5436a3b4
commit 105123bb74
21 changed files with 929 additions and 19 deletions

303
README.md Normal file
View File

@ -0,0 +1,303 @@
# Company SSO — Admin Panel
A single sign-on (SSO) administration panel built with **Laravel 13**, **Livewire 4**, and **Flux UI 2**. It provides user management, authentication flows (including two-factor authentication), profile & security settings, and a dashboard — all rendered server-side with reactive Livewire components.
---
## Table of Contents
- [Tech Stack](#tech-stack)
- [Requirements](#requirements)
- [Installation](#installation)
- [Running the Application](#running-the-application)
- [Available Commands](#available-commands)
- [Project Structure](#project-structure)
- [Authentication Features](#authentication-features)
- [Testing](#testing)
- [Code Style](#code-style)
- [Libraries & Documentation](#libraries--documentation)
- [License](#license)
---
## Tech Stack
| Layer | Technology | Version |
| ---------- | --------------------------- | ------- |
| Framework | Laravel | 13.x |
| PHP | PHP | 8.3+ |
| Frontend | Livewire | 4.x |
| CSS | Tailwind CSS | 4.x |
| Bundler | Vite | 8.x |
| Auth | Laravel Fortify | 1.x |
| Icons | Blade Lucide Icons | 1.x |
| DTOs | Spatie Laravel Data | 4.x |
| Testing | Pest | 4.x |
| Linting | Laravel Pint | 1.x |
| Database | SQLite (default) / MySQL | — |
---
## Requirements
- **PHP** ≥ 8.3
- **Composer** ≥ 2.x
- **Node.js** ≥ 20.x & **npm** ≥ 10.x
- **SQLite** (default) or any Laravel-supported database
---
## Installation
### 1. Clone the repository
```bash
git clone <repository-url> sentientgeeks_admin_sso
cd sentientgeeks_admin_sso
```
### 2. Install PHP dependencies
```bash
composer install
```
### 3. Configure environment
```bash
cp .env.example .env
php artisan key:generate
```
Edit `.env` to configure your database, mail, and other services as needed. The default configuration uses **SQLite** — no extra setup required.
### 4. Create the database
For SQLite (default):
```bash
touch database/database.sqlite
```
For MySQL/PostgreSQL, update the `DB_*` variables in `.env` accordingly.
### 5. Run migrations
```bash
php artisan migrate
```
### 6. Install Node dependencies & build assets
```bash
npm install
npm run build
```
### Quick Setup (all-in-one)
Alternatively, run the bundled setup script that performs steps 26 automatically:
```bash
composer setup
```
---
## Running the Application
### Development (recommended)
Start **all** development services concurrently — web server, queue worker, log tail, and Vite dev server:
```bash
composer run dev
```
This launches:
| Service | URL / Info |
| ------------- | ---------------------------------- |
| Laravel server | `http://localhost:8000` |
| Queue worker | Listens with `--tries=1` |
| Pail (logs) | Real-time log streaming |
| Vite | HMR & asset compilation |
### Production build
```bash
npm run build
php artisan serve
```
---
## Available Commands
| Command | Description |
| --------------------- | -------------------------------------------------------- |
| `composer setup` | Full project setup (install, env, migrate, build) |
| `composer run dev` | Start all dev services concurrently |
| `composer run lint` | Fix code style with Pint |
| `composer run lint:check` | Check code style without modifying files |
| `composer run test` | Clear config, lint-check, and run all tests |
| `npm run dev` | Start Vite dev server with HMR |
| `npm run build` | Build production assets |
---
## Livewire Commands
```bash
# Create a new Livewire component, follow dot (.) notation for sub-folders in name
php artisan make:livewire <name>
# Create a page component (full-page Livewire component)
php artisan make:livewire pages::<name> --page
```
## Project Structure
```
├── app/
│ ├── Actions/Fortify/ # Auth action classes (create user, reset password)
│ ├── Concerns/ # Shared traits (validation rules)
│ ├── Data/Ui/ # Spatie Data DTOs for UI (Connected Services)
│ ├── Enums/ # ConnectionStatus, ConnectionTechTypes
│ ├── Http/Controllers/ # HTTP controllers
│ ├── Livewire/
│ │ ├── Actions/ # Livewire action classes (Logout)
│ │ └── Settings/ # Settings components (Profile, Security, Appearance, 2FA)
│ ├── Models/ # Eloquent models (User)
│ └── Providers/ # Service providers (App, Fortify)
├── config/ # Application configuration
├── database/
│ ├── factories/ # Model factories
│ ├── migrations/ # Database migrations
│ └── seeders/ # Database seeders
├── resources/
│ ├── css/ # Tailwind CSS entry point
│ ├── js/ # JavaScript entry point
│ └── views/
│ ├── components/ # Blade components (sidebar, topbar, logos)
│ ├── flux/ # Flux UI component overrides
│ ├── layouts/ # Application layouts
│ ├── livewire/
│ │ ├── auth/ # Auth views (login, register, 2FA, etc.)
│ │ └── settings/ # Settings views (profile, security, appearance)
│ ├── pages/ # Full-page Livewire components (dashboard)
│ └── partials/ # Reusable view partials
├── routes/
│ ├── web.php # Main web routes
│ ├── settings.php # Settings routes (profile, security, appearance)
│ └── console.php # Artisan console routes
├── tests/
│ ├── Feature/ # Feature tests (Auth, Settings, Dashboard)
│ └── Unit/ # Unit tests
├── vite.config.js # Vite + Tailwind CSS v4 + Laravel plugin config
├── composer.json
└── package.json
```
---
## Authentication Features
Powered by **Laravel Fortify**, the application supports:
- **Registration** — New user sign-up
- **Login / Logout** — Session-based authentication with rate limiting
- **Password Reset** — Forgot password & reset via email
- **Email Verification** — Verify email address flow
- **Two-Factor Authentication (2FA)** — TOTP-based with QR codes and recovery codes
- **Password Confirmation** — Re-confirm password before sensitive actions
- **Profile Management** — Update name, email
- **Account Deletion** — Self-service account removal
---
## Testing
This project uses **Pest** for testing.
```bash
# Run all tests
php artisan test --compact
# Run a specific test file
php artisan test --compact --filter=LoginTest
# Full CI check (lint + test)
composer run test
```
---
## Code Style
Code style is enforced with **Laravel Pint**. After modifying PHP files:
```bash
# Auto-fix formatting
vendor/bin/pint --parallel
# Check without modifying
vendor/bin/pint --test
```
---
## Libraries & Documentation
### Core Framework
| Library | Description | Docs |
| --- | --- | --- |
| **Laravel 13** | PHP web application framework | [laravel.com/docs](https://laravel.com/docs) |
| **PHP 8.3+** | Server-side language | [php.net/docs](https://www.php.net/docs.php) |
### Frontend & UI
| Library | Description | Docs |
| --- | --- | --- |
| **Livewire 4** | Full-stack reactive components for Laravel | [livewire.laravel.com/docs](https://livewire.laravel.com/docs) |
| **Tailwind CSS 4** | Utility-first CSS framework | [tailwindcss.com/docs](https://tailwindcss.com/docs) |
| **Alpine.js** | Lightweight JS framework (bundled with Livewire) | [alpinejs.dev](https://alpinejs.dev/) |
| **Blade Lucide Icons** | Lucide icon set for Blade templates | [github.com/mallardduck/blade-lucide-icons](https://github.com/mallardduck/blade-lucide-icons) |
### Authentication
| Library | Description | Docs |
| --- | --- | --- |
| **Laravel Fortify 1** | Backend authentication scaffolding | [laravel.com/docs/fortify](https://laravel.com/docs/fortify) |
### Data & Architecture
| Library | Description | Docs |
| --- | --- | --- |
| **Spatie Laravel Data 4** | Typed data transfer objects | [spatie.be/docs/laravel-data](https://spatie.be/docs/laravel-data) |
| **Tailwind Merge Laravel** | Intelligent Tailwind class merging in PHP | [github.com/gehrisandro/tailwind-merge-laravel](https://github.com/gehrisandro/tailwind-merge-laravel) |
### Build Tools
| Library | Description | Docs |
| --- | --- | --- |
| **Vite 8** | Next-generation frontend build tool | [vite.dev/guide](https://vite.dev/guide/) |
| **Laravel Vite Plugin 3** | Laravel integration for Vite | [laravel.com/docs/vite](https://laravel.com/docs/vite) |
### Development & Testing
| Library | Description | Docs |
| --- | --- | --- |
| **Pest 4** | Elegant PHP testing framework | [pestphp.com/docs](https://pestphp.com/docs) |
| **Laravel Pint 1** | Opinionated PHP code style fixer | [laravel.com/docs/pint](https://laravel.com/docs/pint) |
| **Laravel Sail 1** | Docker development environment | [laravel.com/docs/sail](https://laravel.com/docs/sail) |
| **Laravel Pail 1** | Real-time log viewer | [laravel.com/docs/pail](https://laravel.com/docs/pail) |
| **Laravel Tinker 3** | REPL for Laravel | [laravel.com/docs/artisan#tinker](https://laravel.com/docs/artisan#tinker) |
| **FakerPHP** | Fake data generator for testing | [fakerphp.org](https://fakerphp.org/) |
| **Mockery** | Mock object framework for PHP | [docs.mockery.io](https://docs.mockery.io/) |
---
## License
This project is proprietary software. All rights reserved.

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ManageRoles;
use App\Data\Users\UsersData;
use App\Enums\RolesStatus;
use Livewire\Wireable;
use Spatie\LaravelData\Concerns\WireableData;
use Spatie\LaravelData\Data;
final class RoleData extends Data implements Wireable
{
use WireableData;
public function __construct(
public string $name,
public UsersData $users,
public ServiceCollection $services,
public RolesStatus $status,
) {}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ManageRoles;
use Illuminate\Support\Collection;
/**
* @extends Collection<int, RoleData>
*/
final class RolesCollection extends Collection {}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ManageRoles;
use Livewire\Wireable;
use Spatie\LaravelData\Concerns\WireableData;
use Spatie\LaravelData\Data;
final class RolesData extends Data implements Wireable
{
use WireableData;
public function __construct(
public RolesCollection $roles,
) {}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ManageRoles;
use Illuminate\Support\Collection;
/**
* @extends Collection<int, ServiceData>
*/
final class ServiceCollection extends Collection {}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ManageRoles;
use Livewire\Wireable;
use Spatie\LaravelData\Concerns\WireableData;
use Spatie\LaravelData\Data;
final class ServiceData extends Data implements Wireable
{
use WireableData;
public function __construct(
public string $name,
) {}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Data\Users;
use Illuminate\Support\Collection;
/**
* @extends Collection<int, UserData>
*/
final class UserCollection extends Collection {}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Data\Users;
use Spatie\LaravelData\Data;
final class UserData extends Data
{
public function __construct(
public string $name,
public string $email,
) {}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Data\Users;
use Spatie\LaravelData\Data;
final class UsersData extends Data
{
public function __construct(
public UserCollection $users,
) {}
}

11
app/Enums/RolesStatus.php Normal file
View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Enums;
enum RolesStatus: string
{
case Active = 'active';
case Inactive = 'inactive';
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
final class ScopeServiceProvider extends ServiceProvider
{
/**
* Bootstrap services.
*/
public function boot(): void
{
$this->registerScopeDirective();
}
public function registerScopeDirective(): void
{
/**
* All credits from this blade directive goes to Konrad Kalemba.
* Just copied and modified for my very specific use case.
*
* https://github.com/konradkalemba/blade-components-scoped-slots
*/
Blade::directive('scope', function ($expression) {
// Split the expression by `top-level` commas (not in parentheses)
$directiveArguments = preg_split("/,(?![^\(\(]*[\)\)])/", $expression);
$directiveArguments = array_map('trim', $directiveArguments);
[$name, $functionArguments] = $directiveArguments;
// Build function "uses" to inject extra external variables
$uses = Arr::except(array_flip($directiveArguments), [$name, $functionArguments]);
$uses = array_flip($uses);
$uses[] = '$__env';
$uses[] = '$__bladeCompiler';
$uses = implode(',', $uses);
/**
* Slot names can`t contains dot , eg: `user.city`.
* So we convert `user.city` to `user___city`
*
* Later, on component it will be replaced back.
*/
$name = str_replace('.', '___', $name);
return "<?php \$__bladeCompiler = \$__bladeCompiler ?? null; \$__env->slot({$name}, function({$functionArguments}) use ({$uses}) { \$loop = !empty(\$__env->getLoopStack()) ? (object) \$__env->getLoopStack()[0] : null; ?>";
});
Blade::directive('endscope', fn() => '<?php }); ?>');
}
}

View File

@ -0,0 +1,269 @@
<?php
declare(strict_types=1);
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Illuminate\View\Component;
final class Table extends Component
{
public string $uuid;
public function __construct(
public array $headers,
public mixed $rows, // Supports Arrays, standard Collections, and Spatie PaginatedDataCollections
public ?string $id = null,
public bool $striped = false,
public bool $noHeaders = false,
public ?string $link = null,
public bool $withPagination = false,
public array $rowDecoration = [],
public array $cellDecoration = [],
public bool $showEmptyText = false,
public string $emptyText = 'No records found.',
public string $containerClass = 'relative overflow-x-auto rounded-2xl -pb-2',
public bool $noHover = false,
// Slots
public mixed $actions = null,
public mixed $cell = null,
public mixed $empty = null,
public mixed $footer = null,
) {
// Temp save closures to prevent Livewire/PHP 8.5 serialization crashes
$rowDecoration = $this->rowDecoration;
$cellDecoration = $this->cellDecoration;
$headers = $this->headers;
// Remove closures from serialization
unset($this->rowDecoration, $this->cellDecoration, $this->headers);
// Serialize a unique ID for component tracking
$this->uuid = "table-" . md5(serialize($this)) . $id;
// Restore closures
$this->rowDecoration = $rowDecoration;
$this->cellDecoration = $cellDecoration;
$this->headers = $headers;
}
// Check if header is hidden
public function isHidden(mixed $header): bool
{
return $header['hidden'] ?? false;
}
// Format header contents
public function format(mixed $row, mixed $field, mixed $header): mixed
{
$format = $header['format'] ?? null;
if ( ! $format) {
return $field;
}
if (is_callable($format)) {
return $format($row, $field);
}
if ('currency' === $format[0]) {
return ($format[2] ?? '') . number_format((float) $field, ...mb_str_split($format[1]));
}
if ('date' === $format[0] && $field) {
return Carbon::parse($field)->translatedFormat($format[1]);
}
return $field;
}
// Check if link should be shown in cell
public function hasLink(mixed $header): bool
{
return $this->link && empty($header['disableLink']);
}
// Build row link
public function redirectLink(mixed $row): string
{
$link = $this->link;
// Transform from `route()` pattern
$link = Str::of($link)->replace('%5B', '{')->replace('%5D', '}');
// Extract tokens like {id}, {city.name} ...
$tokens = Str::of($link)->matchAll('/\{(.*?)\}/');
// Replace tokens with actual row values
$tokens->each(function (string $token) use ($row, &$link): void {
$link = Str::of($link)->replace("{" . $token . "}", data_get($row, $token))->toString();
});
return $link;
}
public function rowClasses(mixed $row): ?string
{
$classes = [];
foreach ($this->rowDecoration as $class => $condition) {
if ($condition($row)) {
$classes[] = $class;
}
}
return Arr::join($classes, ' ');
}
public function cellClasses(mixed $row, array $header): ?string
{
$classes = Str::of($header['class'] ?? '')->explode(' ')->all();
foreach ($this->cellDecoration[$header['key']] ?? [] as $class => $condition) {
if ($condition($row)) {
$classes[] = $class;
}
}
return Arr::join($classes, ' ');
}
public function render(): View|Closure|string
{
return <<<'HTML'
<div class="w-full">
<div class="{{ $containerClass }}">
<table
{{
$attributes
->whereDoesntStartWith('wire:model')
->class([
'w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400',
])
}}
>
<thead @class([
"text-xs text-gray-700 uppercase bg-gray-50 border-b border-gray-200",
"hidden" => $noHeaders
])>
<tr>
@foreach($headers as $header)
@php
if($isHidden($header)) continue;
# Scoped slot's name like `user.city` are compiled to `user___city` through `@scope / @endscope`.
# So we use current `$header` key to find that slot on context.
$temp_key = str_replace('.', '___', $header['key'])
@endphp
<th scope="col" class="px-6 py-4 {{ $header['class'] ?? '' }}">
{{ isset(${"header_".$temp_key}) ? ${"header_".$temp_key}($header) : $header['label'] }}
</th>
@endforeach
@if($actions)
<th scope="col" class="px-6 py-3 w-1 whitespace-nowrap">
<span class="sr-only">Actions</span>
</th>
@endif
</tr>
</thead>
<tbody>
@foreach($rows as $k => $row)
<tr
wire:key="{{ $uuid }}-{{ $k }}"
@class([
'bg-white border-b',
$rowClasses($row),
'even:bg-gray-50 ' => $striped,
'hover:bg-gray-50' => !$noHover,
'cursor-pointer' => $attributes->has('@row-click') || $link
])
@if($attributes->has('@row-click'))
@click="$dispatch('row-click', {{ json_encode($row) }});"
@endif
>
@foreach($headers as $header)
@php
if($isHidden($header)) continue;
$temp_key = str_replace('.', '___', $header['key'])
@endphp
@if(isset(${"cell_".$temp_key}))
<td @class(["px-6 py-4", $cellClasses($row, $header), "p-0" => $hasLink($header)])>
@if($hasLink($header))
<a href="{{ $redirectLink($row) }}" wire:navigate class="block px-6 py-4">
@endif
{{ ${"cell_".$temp_key}($row) }}
@if($hasLink($header))
</a>
@endif
</td>
@else
<td @class(["px-6 py-4", $cellClasses($row, $header), "p-0" => $hasLink($header)])>
@if($hasLink($header))
<a href="{{ $redirectLink($row) }}" wire:navigate class="block px-6 py-4 text-inherit">
@endif
{{ $format($row, data_get($row, $header['key']), $header) }}
@if($hasLink($header))
</a>
@endif
</td>
@endif
@endforeach
@if($actions)
<td class="px-6 py-4 text-right whitespace-nowrap">
{{ $actions($row) }}
</td>
@endif
</tr>
@endforeach
</tbody>
@isset ($footer)
<tfoot @class([
"text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400",
$footer->attributes->get('class') ?? ''
])>
{{ $footer }}
</tfoot>
@endisset
</table>
@if(count($rows) === 0)
@if($showEmptyText)
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ $emptyText }}
</div>
@endif
@if($empty)
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ $empty }}
</div>
@endif
@endif
</div>
@if($withPagination && method_exists($rows, 'links'))
<div class="mt-4 px-2">
{{ $rows->links() }}
</div>
@endif
</div>
HTML;
}
}

View File

@ -2,10 +2,8 @@
declare(strict_types=1); declare(strict_types=1);
use App\Providers\AppServiceProvider;
use App\Providers\FortifyServiceProvider;
return [ return [
AppServiceProvider::class, App\Providers\AppServiceProvider::class,
FortifyServiceProvider::class, App\Providers\FortifyServiceProvider::class,
App\Providers\ScopeServiceProvider::class,
]; ];

View File

@ -2,29 +2,24 @@
use Livewire\Component; use Livewire\Component;
new class extends Component new class extends Component {
{
public string $selectedTab = 'all-tab'; public string $selectedTab = 'all-tab';
}; };
?> ?>
<div> <div>
<x-shared.card class="p-0"> <x-shared.card title="Connected Services" icon="lucide-grid-2x2-plus" class="p-0">
<div class="p-4 flex justify-between border-b border-gray-200"> <x-slot:actions>
<h2 class="font-medium flex items-center gap-x-2">
<x-lucide-grid-2x2-plus class="w-5 h-5"/>
{{ __('Connected Services') }}
</h2>
<x-shared.button> <x-shared.button>
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<x-lucide-plus class="w-5 h-5"/> <x-lucide-plus class="w-5 h-5"/>
{{ __('Add service') }} {{ __('Add service') }}
</div> </div>
</x-shared.button> </x-shared.button>
</div> </x-slot:actions>
<x-shared.tabs.tabs wire:model="selectedTab" class="px-4"> <x-shared.tabs.tabs wire:model="selectedTab" class="px-4">
<x-shared.tabs.tab name="all-tab" label="All" class=""> <x-shared.tabs.tab name="all-tab" label="All" class="">
<livewire:connected-services.all-connected-services /> <livewire:dashboard.connected-services.all/>
</x-shared.tabs.tab> </x-shared.tabs.tab>
<x-shared.tabs.tab name="oidc-tab" label="OIDC"> <x-shared.tabs.tab name="oidc-tab" label="OIDC">
<div>Tricks</div> <div>Tricks</div>

View File

@ -0,0 +1,134 @@
<?php
use App\Data\Ui\ManageRoles\RoleData;
use App\Data\Ui\ManageRoles\RolesCollection;
use App\Data\Ui\ManageRoles\RolesData;
use App\Data\Ui\ManageRoles\ServiceCollection;
use App\Data\Ui\ManageRoles\ServiceData;
use App\Data\Users\UserCollection;
use App\Data\Users\UserData;
use App\Data\Users\UsersData;
use Livewire\Component;
new class extends Component {
/**
* @var array<int, array{
* key: string,
* label: string,
* format: array{0: string, 1: string}|null
* }>
*/
public array $header;
public RolesData $rolesData;
public function mount(): void
{
$this->header = [
[
'key' => 'name',
'label' => 'Role Name',
],
[
'key' => 'users',
'label' => 'User Assigned',
],
[
'key' => 'services',
'label' => 'Services',
],
[
'key' => 'status',
'label' => 'Status',
]
];
$this->rolesData = new RolesData(
roles: new RolesCollection([
new RoleData(
name: 'Super Admin',
users: new UsersData(
users: new UserCollection([
new UserData(
name: 'Kushal Saha',
email: 'kushal@example.com'
),
new UserData(
name: 'Test Example',
email: 'test@example.com'
)
]
)
),
services: new ServiceCollection(
[
new ServiceData(
name: 'Teams'
)
]
),
status: \App\Enums\RolesStatus::Active
),
new RoleData(
name: 'Super Admin',
users: new UsersData(
users: new UserCollection([
new UserData(
name: 'Kushal Saha',
email: 'kushal@example.com'
)
]
)
),
services: new ServiceCollection(
[
new ServiceData(
name: 'Teams'
),
new ServiceData(
name: 'Outlook'
)
]
),
status: \App\Enums\RolesStatus::Active
),
])
);
}
};
?>
<div>
<x-shared.card title="Roles" icon="lucide-shield-ellipsis" class="p-0">
<x-slot:actions>
<x-shared.button>
<div class="flex items-center gap-x-2">
<x-lucide-plus class="w-5 h-5"/>
{{ __('Add role') }}
</div>
</x-shared.button>
</x-slot:actions>
<x-table :headers="$header" :rows="$rolesData->roles">
@scope('cell_users', $row)
@foreach($row->users->users as $user)
<span
class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-gray-200 text-gray-800 font-bold text-[10px] tracking-tight">
{{implode('', array_map(fn($w) => strtoupper($w[0]), explode(' ', $user->name)))}}
</span>
@endforeach
@endscope
@scope('cell_status', $row)
<x-shared.badge class="bg-lime-200 text-lime-800 font-bold">
{{ ucfirst($row->status->name) }}
</x-shared.badge>
@endscope
@scope('cell_services', $row)
@foreach($row->services as $service)
<x-shared.badge class="bg-indigo-200 text-indigo-800 font-bold">
{{$service->name}}
</x-shared.badge>
@endforeach
@endscope
</x-table>
</x-shared.card>
</div>

View File

@ -0,0 +1,3 @@
<span
{{$attributes->twMerge(['class'=> 'px-4 py-1 rounded-full bg-gray-200'])}}
>{{ $slot }}</span>

View File

@ -1,3 +1,19 @@
@props(['title', 'icon', 'actions'])
<div {{$attributes->twMerge('rounded-xl border border-gray-200 p-4 shadow-xs bg-white')}}> <div {{$attributes->twMerge('rounded-xl border border-gray-200 p-4 shadow-xs bg-white')}}>
@isset($title)
<div class="p-4 flex justify-between border-b border-gray-200">
<h2 class="font-medium flex items-center gap-x-2">
@isset($icon)
<span class="w-5 h-5">
@svg($icon)
</span>
@endisset
{{ __($title) }}
</h2>
@isset($actions)
{{$actions}}
@endisset
</div>
@endisset
{{$slot}} {{$slot}}
</div> </div>

View File

@ -2,7 +2,7 @@
'selected' => null, 'selected' => null,
'labelClass' => 'px-4 py-3 cursor-pointer', 'labelClass' => 'px-4 py-3 cursor-pointer',
'activeClass' => 'font-medium border-b border-b-gray-400', 'activeClass' => 'font-medium border-b border-b-gray-400',
'labelDivClass' => 'border-b border-b-base-content/10 flex overflow-x-auto', 'labelDivClass' => 'border-b bg-gray-50 border-b-base-content/10 flex overflow-x-auto',
'tabsClass' => 'relative w-full', 'tabsClass' => 'relative w-full',
]) ])

View File

@ -4,11 +4,13 @@
@include('partials.head') @include('partials.head')
</head> </head>
<body class="min-h-screen bg-gray-50 antialiased"> <body class="min-h-screen bg-gray-50 antialiased">
<div class="flex h-full"> <div class="flex h-screen">
<livewire:dashboard-sidebar :active="$activeNav ?? 'dashboard'"/> <livewire:dashboard-sidebar :active="$activeNav ?? 'dashboard'"/>
<div class="flex flex-col flex-1 min-w-0 overflow-hidden"> <div class="flex flex-col flex-1 min-w-0 overflow-hidden">
<x-dashbord-topbar/> <x-dashbord-topbar/>
<main class="">
<main class="flex-1 overflow-y-auto p-4">
{{ $slot }} {{ $slot }}
</main> </main>
</div> </div>

View File

@ -15,6 +15,7 @@ class extends Component {
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl"> <div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
<x-dashboard-stats /> <x-dashboard-stats />
<livewire:connected-services /> <livewire:dashboard.connected-services />
<livewire:dashboard.roles.manager />
</div> </div>