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:
parent
4a5436a3b4
commit
105123bb74
303
README.md
Normal file
303
README.md
Normal 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 2–6 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.
|
||||
22
app/Data/Ui/ManageRoles/RoleData.php
Normal file
22
app/Data/Ui/ManageRoles/RoleData.php
Normal 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,
|
||||
) {}
|
||||
}
|
||||
12
app/Data/Ui/ManageRoles/RolesCollection.php
Normal file
12
app/Data/Ui/ManageRoles/RolesCollection.php
Normal 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 {}
|
||||
17
app/Data/Ui/ManageRoles/RolesData.php
Normal file
17
app/Data/Ui/ManageRoles/RolesData.php
Normal 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,
|
||||
) {}
|
||||
}
|
||||
12
app/Data/Ui/ManageRoles/ServiceCollection.php
Normal file
12
app/Data/Ui/ManageRoles/ServiceCollection.php
Normal 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 {}
|
||||
18
app/Data/Ui/ManageRoles/ServiceData.php
Normal file
18
app/Data/Ui/ManageRoles/ServiceData.php
Normal 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,
|
||||
) {}
|
||||
}
|
||||
12
app/Data/Users/UserCollection.php
Normal file
12
app/Data/Users/UserCollection.php
Normal 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 {}
|
||||
16
app/Data/Users/UserData.php
Normal file
16
app/Data/Users/UserData.php
Normal 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,
|
||||
) {}
|
||||
|
||||
}
|
||||
14
app/Data/Users/UsersData.php
Normal file
14
app/Data/Users/UsersData.php
Normal 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
11
app/Enums/RolesStatus.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RolesStatus: string
|
||||
{
|
||||
case Active = 'active';
|
||||
case Inactive = 'inactive';
|
||||
}
|
||||
55
app/Providers/ScopeServiceProvider.php
Normal file
55
app/Providers/ScopeServiceProvider.php
Normal 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 }); ?>');
|
||||
}
|
||||
}
|
||||
269
app/View/Components/Table.php
Normal file
269
app/View/Components/Table.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,8 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
use App\Providers\FortifyServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
FortifyServiceProvider::class,
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\FortifyServiceProvider::class,
|
||||
App\Providers\ScopeServiceProvider::class,
|
||||
];
|
||||
|
||||
@ -2,29 +2,24 @@
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
new class extends Component {
|
||||
public string $selectedTab = 'all-tab';
|
||||
};
|
||||
?>
|
||||
|
||||
<div>
|
||||
<x-shared.card class="p-0">
|
||||
<div class="p-4 flex justify-between border-b border-gray-200">
|
||||
<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.card title="Connected Services" icon="lucide-grid-2x2-plus" 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 service') }}
|
||||
</div>
|
||||
</x-shared.button>
|
||||
</div>
|
||||
</x-slot:actions>
|
||||
<x-shared.tabs.tabs wire:model="selectedTab" class="px-4">
|
||||
<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 name="oidc-tab" label="OIDC">
|
||||
<div>Tricks</div>
|
||||
134
resources/views/components/dashboard/roles/⚡manager.blade.php
Normal file
134
resources/views/components/dashboard/roles/⚡manager.blade.php
Normal 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>
|
||||
3
resources/views/components/shared/badge.blade.php
Normal file
3
resources/views/components/shared/badge.blade.php
Normal file
@ -0,0 +1,3 @@
|
||||
<span
|
||||
{{$attributes->twMerge(['class'=> 'px-4 py-1 rounded-full bg-gray-200'])}}
|
||||
>{{ $slot }}</span>
|
||||
@ -1,3 +1,19 @@
|
||||
@props(['title', 'icon', 'actions'])
|
||||
<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}}
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
'selected' => null,
|
||||
'labelClass' => 'px-4 py-3 cursor-pointer',
|
||||
'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',
|
||||
])
|
||||
|
||||
|
||||
@ -4,11 +4,13 @@
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<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'"/>
|
||||
|
||||
<div class="flex flex-col flex-1 min-w-0 overflow-hidden">
|
||||
<x-dashbord-topbar/>
|
||||
<main class="">
|
||||
|
||||
<main class="flex-1 overflow-y-auto p-4">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -15,6 +15,7 @@ class extends Component {
|
||||
|
||||
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
|
||||
<x-dashboard-stats />
|
||||
<livewire:connected-services />
|
||||
<livewire:dashboard.connected-services />
|
||||
<livewire:dashboard.roles.manager />
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user