= dfb47f7d19 Feat: Add unlimited flag to app roles and enhance Keka integration
- Introduced `is_unlimited` field in `app_roles` table to support roles with unlimited duration.
- Enhanced front-end and back-end implementations to manage unlimited roles, including UI updates and validation.
- Refactored `KekaOutlookController` and `KekaOutlookService` for streamlined exception handling and code consistency.
- Applied linting and formatting improvements across updated files.
2026-06-15 12:59:30 +00:00

248 lines
8.1 KiB
PHP

<?php
use App\Concerns\Confirmation;
use App\Concerns\HandlesOperations;
use App\Livewire\Forms\AssignAppToRoleForm;
use App\Livewire\Forms\CreateRoleForm;
use App\Services\AppRoleService;
use App\Data\Role\{AssignAppsToRoleData, CreateRoleData, RoleAppData, RoleUserData};
use App\Data\Ui\TableHeader;
use App\Services\RoleService;
use Livewire\Attributes\{Computed, Layout, Title, Validate};
use Livewire\Component;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\DataCollection;
use Spatie\LaravelData\PaginatedDataCollection;
new
#[Title('Roles and Apps')]
class extends Component {
use HandlesOperations, Confirmation;
protected AppRoleService $appRoleService;
public bool $showCreateModal = false;
public CreateRoleForm $form;
public AssignAppToRoleForm $assignForm;
protected RoleService $roleService;
public array $appsSearchable = [];
#[DataCollectionOf(TableHeader::class)]
public DataCollection $headers;
public function boot(RoleService $roleService, AppRoleService $appRoleService): void
{
$this->roleService = $roleService;
$this->appRoleService = $appRoleService;
}
public function mount(): void
{
$this->headers = TableHeader::collect([
new TableHeader(key: 'name', label: 'Name'),
new TableHeader(key: 'priority', label: 'Priority'),
new TableHeader(key: 'apps', label: 'Apps'),
new TableHeader(key: 'users', label: 'Users'),
], DataCollection::class);
}
#[Computed]
public function getRoles(): PaginatedDataCollection
{
return $this->roleService->getAll();
}
public function searchApps(string $value = ''): void
{
$this->appsSearchable = $this
->appRoleService
->searchAppsForSelect($value)
->toArray();
}
public function selectAllApps(): void
{
$this->form->appIds = $this->appRoleService->getAllAppIds();
$this->appsSearchable = $this->appRoleService->searchAppsForSelect('')->toArray();
}
public function clearApps(): void
{
$this->form->appIds = [];
}
public function test()
{
dd('hi');
}
public function openCreateModal(): void
{
$this->form->reset();
$this->searchApps();
$this->showCreateModal = true;
}
public function save(): void
{
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
$this->form->validate();
if ((int) $this->form->priority <= $userPriority) {
$this->addError('form.priority',
'The priority must be greater than your own highest role priority ('.$userPriority.').');
return;
}
$this->attempt(
action: function (): void {
$role = $this->roleService->create(new CreateRoleData(
name: $this->form->name,
priority: (int) $this->form->priority,
));
$this->appRoleService->assign(new AssignAppsToRoleData(
roleId: $role->id,
validUpto: $this->form->isUnlimited ? now()->addYears(15) : $this->form->validUpto,
appIds: $this->form->appIds,
isUnlimited: $this->assignForm->isUnlimited
));
$this->showCreateModal = false;
$this->form->reset();
unset($this->getRoles);
},
successMessage: 'Role created successfully!',
);
}
public function delete(int $roleId): void
{
$this->attempt(
action: function () use ($roleId): void {
$this->roleService->delete($roleId);
unset($this->getRoles);
},
successMessage: 'Role deleted successfully!',
);
}
};
?>
<div>
<x-shared.card title="Roles" icon="lucide-shield-check" class="pb-2">
<x-slot:actions>
<x-mary-button wire:click="openCreateModal" icon="lucide.plus">
Add new role
</x-mary-button>
</x-slot:actions>
<x-mary-table
:headers="$headers->toArray()"
:rows="$this->getRoles->items()"
>
@scope('cell_name', $row)
<span class="font-medium text-gray-900">{{ $row->name }}</span>
@endscope
@scope('cell_priority', $row)
<span class="text-gray-500 font-medium">{{ $row->priority }}</span>
@endscope
@scope('cell_apps', $row)
@foreach($row->apps as $app)
@php /** @var RoleAppData $app */ @endphp
<x-mary-badge class="badge-soft badge-primary" :value="$app->appName"/>
@endforeach
@endscope
@scope('cell_users', $row)
<div class="flex -space-x-2 overflow-hidden">
@foreach($row->users as $user)
@php /** @var RoleUserData $user */ @endphp
<x-mary-avatar :placeholder="$user->initials"
class="w-8! h-8! bg-blue-100 text-blue-600 border-2 border-white ring-0"
tooltip="{{ $user->name }}"/>
@endforeach
</div>
@endscope
@scope('actions', $row)
<div class="flex gap-1">
<x-mary-button
icon="lucide.eye"
:link="route('access-manager.roles-and-apps.show', $row->id)"
wire:navigate
tooltip="View details"
class="btn-sm btn-circle btn-soft"
/>
<x-mary-button
icon="lucide.trash"
wire:click="requireConfirmation('delete', {{ $row->id }})"
spinner="requireConfirmation('delete', {{ $row->id }})"
variant="danger"
class="btn-sm btn-circle btn-error btn-soft"
/>
</div>
@endscope
</x-mary-table>
</x-shared.card>
<x-mary-modal wire:model="showCreateModal" title="Add New Role">
<x-mary-form wire:submit="save">
<x-mary-input
label="Role Name"
wire:model="form.name"
id="role-name-input"
type="text"
placeholder="e.g. editor, manager"
autofocus
/>
<x-mary-input
label="Priority"
wire:model="form.priority"
id="role-priority-input"
type="number"
placeholder="e.g. 1, 2, 3"
/>
<x-shared.choices
wire:model="form.appIds"
:options="$appsSearchable"
search-function="searchApps"
select-all-action="selectAllApps"
clear-action="clearApps"
label="Apps"
/>
<div x-data="{ isUnlimited: @entangle('assignForm.isUnlimited') }"
class="flex gap-2 items-baseline-last">
<div class="flex-1 transition-opacity duration-200"
x-bind:class="isUnlimited ? 'opacity-50 pointer-events-none' : ''">
<x-mary-datepicker
label="Date"
wire:model="form.validUpto"
icon="lucide.calendar"
x-bind:disabled="isUnlimited"
x-bind:readonly="isUnlimited"
/>
</div>
<x-mary-toggle
class="toggle-secondary"
label="Unlimited"
x-model="isUnlimited"
/>
</div>
<x-slot:actions>
<x-mary-button @click="$wire.showCreateModal = false" class="btn-ghost">
Cancel
</x-mary-button>
<x-mary-button type="submit" class="btn-primary" icon="lucide.save" spinner="save">
Save
</x-mary-button>
</x-slot:actions>
</x-mary-form>
</x-mary-modal>
</div>