All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m6s
1. 修復帳號管理與角色權限頁面搜尋功能,支援 Enter 鍵快捷提交。 2. 完成 B013 (機台故障上報) API 實作,改用非同步隊列 (ProcessMachineError) 處理日誌上報。 3. 精簡 B013 API 參數,移除冗餘的 message 欄位,統一由雲端對照表翻譯。 4. 更新技術規格文件 (SKILL.md) 與系統 API 文件配置 (api-docs.php)。 5. 修正平台管理員帳號在搜尋過濾時的資料隔離邏輯。
812 lines
55 KiB
PHP
812 lines
55 KiB
PHP
@extends('layouts.admin')
|
|
|
|
@php
|
|
$routeName = request()->route()->getName();
|
|
$baseRoute = str_contains($routeName, 'sub-accounts') ? 'admin.data-config.sub-accounts' : 'admin.permission.accounts';
|
|
|
|
$tab = request('tab', 'accounts');
|
|
$roleSelectConfig = [
|
|
"placeholder" => __('Select Role'),
|
|
"hasSearch" => true,
|
|
"searchPlaceholder" => __('Search Role...'),
|
|
"isHidePlaceholder" => false,
|
|
"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500",
|
|
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
|
|
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
|
|
"dropdownClasses" => "hs-select-menu w-full bg-white/95 dark:bg-slate-900/95 backdrop-blur-xl border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[100] animate-luxury-in",
|
|
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between transition-all duration-300",
|
|
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></span></div>'
|
|
];
|
|
@endphp
|
|
|
|
@section('content')
|
|
<div class="space-y-2 pb-20" x-data="accountManager({
|
|
roles: @js($roles),
|
|
errors: @js($errors->any()),
|
|
oldValues: {
|
|
method: @js(old('_method')),
|
|
id: @js(old('id')),
|
|
name: @js(old('name')),
|
|
username: @js(old('username')),
|
|
email: @js(old('email')),
|
|
phone: @js(old('phone')),
|
|
company_id: @js(old('company_id', auth()->user()->isSystemAdmin() ? '' : auth()->user()->company_id)),
|
|
role: @js(old('role', '')),
|
|
status: @js(old('status', 1))
|
|
},
|
|
tab: @js($tab),
|
|
roleSelectConfig: @js($roleSelectConfig)
|
|
})">
|
|
<!-- Header -->
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
|
<div>
|
|
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ $title }}</h1>
|
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
|
|
{{ $tab === 'roles' ? __('Define and manage security roles and permissions.') : __('Manage administrative and tenant accounts') }}
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
@if($tab === 'roles')
|
|
<a href="{{ route('admin.data-config.sub-account-roles.create') }}" class="btn-luxury-primary">
|
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
</svg>
|
|
<span>{{ __('Add Role') }}</span>
|
|
</a>
|
|
@else
|
|
<button @click="openCreateModal()" class="btn-luxury-primary">
|
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
</svg>
|
|
<span>{{ __('Add Account') }}</span>
|
|
</button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
@if(request()->routeIs('admin.data-config.sub-accounts') && auth()->user()->can('menu.data-config.sub-accounts'))
|
|
<!-- Tabs Navigation (Pills Style) -->
|
|
<div class="flex items-center gap-1 p-1.5 bg-slate-100 dark:bg-slate-900/50 rounded-2xl w-fit border border-slate-200/50 dark:border-slate-800/50" aria-label="Tabs">
|
|
<button type="button"
|
|
@click="switchTab('accounts')"
|
|
:class="tab === 'accounts' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
|
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
|
{{ __('Account List') }}
|
|
</button>
|
|
<button type="button"
|
|
@click="switchTab('roles')"
|
|
:class="tab === 'roles' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
|
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
|
{{ __('Role Management') }}
|
|
</button>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Accounts Content (Integrated Card) -->
|
|
<div class="luxury-card rounded-3xl p-8 animate-luxury-in mt-6">
|
|
@if($tab === 'roles')
|
|
<!-- Role Filters & Search -->
|
|
<form action="{{ route($baseRoute) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-10">
|
|
<input type="hidden" name="tab" value="roles">
|
|
<div class="relative group">
|
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
|
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
</svg>
|
|
</span>
|
|
<input type="text" name="search" value="{{ request('search') }}"
|
|
class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input"
|
|
placeholder="{{ __('Search roles...') }}"
|
|
@keydown.enter="$el.form.submit()">
|
|
</div>
|
|
|
|
@if(auth()->user()->isSystemAdmin())
|
|
<div class="relative">
|
|
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')" placeholder="{{ __('All Affiliations') }}" class="w-full md:w-auto min-w-[280px]" onchange="this.form.submit()">
|
|
<option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }} data-title="{{ __('System Level') }}">{{ __('System Level') }}</option>
|
|
</x-searchable-select>
|
|
</div>
|
|
@endif
|
|
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
|
<button type="submit" class="hidden"></button>
|
|
</form>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left border-separate border-spacing-y-0">
|
|
<thead>
|
|
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Role Name') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Affiliation') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Permissions') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Users') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
|
@forelse($paginated_roles as $role)
|
|
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
|
<td class="px-6 py-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
|
|
</div>
|
|
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $role->name }}</span>
|
|
@if($role->is_system)
|
|
<span class="p-1.5 bg-cyan-50 dark:bg-cyan-900/30 text-cyan-600 dark:text-cyan-400 rounded-lg tooltip" title="{{ __('System Role') }}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
</span>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-6">
|
|
@if($role->is_system)
|
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 uppercase tracking-widest">
|
|
{{ __('System Level') }}
|
|
</span>
|
|
@else
|
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">
|
|
{{ $role->company->name ?? __('Company Level') }}
|
|
</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-6" width="30%">
|
|
<div class="flex flex-wrap gap-1 max-w-xs">
|
|
@forelse($role->permissions->take(6) as $permission)
|
|
<span class="px-2 py-0.5 text-xs bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 rounded border border-slate-200 dark:border-slate-700 uppercase font-bold tracking-widest">{{ __($permission->name) }}</span>
|
|
@empty
|
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest">{{ __('No permissions') }}</span>
|
|
@endforelse
|
|
@if($role->permissions->count() > 6)
|
|
<span class="px-2 py-0.5 text-xs bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 rounded border border-slate-200 dark:border-slate-700 uppercase font-bold tracking-widest">+{{ $role->permissions->count() - 6 }}</span>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-6 text-center">
|
|
<span class="text-sm font-extrabold text-slate-700 dark:text-slate-300">{{ $role->users()->count() }}</span>
|
|
</td>
|
|
<td class="px-6 py-6 text-right">
|
|
<div class="flex items-center justify-end gap-2">
|
|
<a href="{{ route('admin.data-config.sub-account-roles.edit', $role->id) }}" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20 tooltip" title="{{ __('Edit') }}">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
|
|
</a>
|
|
@if($role->name !== 'super-admin' && (auth()->user()->isSystemAdmin() || !$role->is_system))
|
|
<form action="{{ route('admin.data-config.sub-account-roles.destroy', $role->id) }}" method="POST" @submit.prevent="if({{ $role->users()->count() }} > 0) { triggerDeleteWarning('{{ __('Cannot delete role with active users.') }}'); return; } confirmDelete('{{ route('admin.data-config.sub-account-roles.destroy', $role->id) }}')" class="inline text-slate-400">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="submit" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20 tooltip" title="{{ __('Delete') }}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
|
</button>
|
|
</form>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="5" class="px-6 py-20 text-center">
|
|
<div class="flex flex-col items-center justify-center gap-4 text-slate-400">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 opacity-20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 8v8"/><path d="M8 12h8"/></svg>
|
|
<p class="text-lg font-bold">{{ __('No roles found.') }}</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
|
|
{{ $paginated_roles->links('vendor.pagination.luxury') }}
|
|
</div>
|
|
|
|
@else
|
|
<!-- Accounts Filters & Search -->
|
|
<form action="{{ route($baseRoute) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-10">
|
|
<input type="hidden" name="tab" value="accounts">
|
|
<div class="relative group">
|
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
|
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
|
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
stroke-linejoin="round">
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
</svg>
|
|
</span>
|
|
<input type="text" name="search" value="{{ request('search') }}"
|
|
class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input"
|
|
placeholder="{{ __('Search users...') }}"
|
|
@keydown.enter="$el.form.submit()">
|
|
</div>
|
|
|
|
@if(auth()->user()->isSystemAdmin())
|
|
<div class="relative">
|
|
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
|
|
:placeholder="__('All Affiliations')" class="w-full md:w-auto min-w-[280px]"
|
|
onchange="this.form.submit()" />
|
|
</div>
|
|
@endif
|
|
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
|
<button type="submit" class="hidden"></button>
|
|
</form>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left border-separate border-spacing-y-0">
|
|
<thead>
|
|
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
|
{{ __('User Info') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
|
{{ __('Contact Info') }}</th>
|
|
@if(auth()->user()->isSystemAdmin())
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
|
{{ __('Affiliation') }}</th>
|
|
@endif
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
|
|
{{ __('Role') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
|
|
{{ __('Status') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">
|
|
{{ __('Actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
|
@forelse($users as $user)
|
|
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
|
<td class="px-6 py-6">
|
|
<div class="flex items-center gap-x-4">
|
|
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all overflow-hidden shadow-sm">
|
|
@if($user->avatar)
|
|
<img src="{{ Storage::url($user->avatar) }}" class="w-full h-full object-cover">
|
|
@else
|
|
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
|
</svg>
|
|
@endif
|
|
</div>
|
|
<div class="flex flex-col">
|
|
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $user->name }}</span>
|
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-widest uppercase"><span class="font-mono">{{ $user->username }}</span></span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-6 font-display">
|
|
<div class="flex flex-col">
|
|
@if($user->phone)
|
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest">{{ $user->phone }}</span>
|
|
@endif
|
|
<span class="text-xs font-bold text-slate-400 dark:text-slate-500 @if($user->phone) mt-1 @endif tracking-widest">{{ $user->email ?? '-' }}</span>
|
|
</div>
|
|
</td>
|
|
@if(auth()->user()->isSystemAdmin())
|
|
<td class="px-6 py-6">
|
|
@if($user->company)
|
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{ $user->company->name }}</span>
|
|
@else
|
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 uppercase tracking-widest">{{ __('SYSTEM') }}</span>
|
|
@endif
|
|
</td>
|
|
@endif
|
|
<td class="px-6 py-6 text-center">
|
|
@foreach($user->roles as $role)
|
|
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 uppercase tracking-widest">
|
|
{{ $role->name }}
|
|
</span>
|
|
@endforeach
|
|
</td>
|
|
<td class="px-6 py-6 text-center">
|
|
@if($user->status)
|
|
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 tracking-widest uppercase">
|
|
{{ __('Active') }}
|
|
</span>
|
|
@else
|
|
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-rose-500/10 text-rose-600 dark:text-rose-400 border border-rose-500/20 tracking-widest uppercase">
|
|
{{ __('Disabled') }}
|
|
</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-6 text-right">
|
|
<div class="flex justify-end items-center gap-2">
|
|
@if(!$user->hasRole('super-admin') || auth()->user()->hasRole('super-admin'))
|
|
@if($user->status)
|
|
<button type="button"
|
|
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; statusToggleSource = 'list'; isStatusConfirmOpen = true"
|
|
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 transition-all border border-transparent hover:border-amber-500/20"
|
|
title="{{ __('Disable') }}">
|
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
|
</svg>
|
|
</button>
|
|
@else
|
|
<button type="button"
|
|
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; $nextTick(() => $refs.statusToggleForm.submit())"
|
|
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
|
|
title="{{ __('Enable') }}">
|
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z" />
|
|
</svg>
|
|
</button>
|
|
@endif
|
|
<button @click="openEditModal(@js($user))"
|
|
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
|
|
title="{{ __('Edit') }}">
|
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1-2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
|
</svg>
|
|
</button>
|
|
<form action="{{ route($baseRoute . '.destroy', $user->id) }}" method="POST"
|
|
class="inline-block">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="button"
|
|
@click="confirmDelete('{{ route($baseRoute . '.destroy', $user->id) }}')"
|
|
class="p-2 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 dark:hover:text-rose-400 hover:bg-rose-500/5 dark:hover:bg-rose-500/10 border border-transparent hover:border-rose-500/20 transition-all group/btn"
|
|
title="{{ __('Delete Account') }}">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
|
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
@else
|
|
<span
|
|
class="text-[10px] font-black text-slate-300 dark:text-slate-600 uppercase tracking-[0.15em] px-2">{{ __('Protected') }}</span>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="{{ auth()->user()->isSystemAdmin() ? 6 : 5 }}" class="px-6 py-24 text-center">
|
|
<div class="flex flex-col items-center gap-3 opacity-20">
|
|
<svg class="size-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
|
d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" />
|
|
</svg>
|
|
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
|
{{ $users->links('vendor.pagination.luxury') }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- User Modal -->
|
|
<div x-show="showModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak>
|
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
|
<div x-show="showModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
|
|
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
|
class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="showModal = false">
|
|
</div>
|
|
|
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
|
|
|
<div x-show="showModal" x-transition:enter="ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
|
x-transition:leave="ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
class="inline-block px-8 py-10 text-left align-bottom transition-all transform luxury-card rounded-3xl dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full overflow-visible">
|
|
|
|
<div class="flex justify-between items-center mb-8">
|
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight"
|
|
x-text="editing ? '{{ __('Edit Account') }}' : '{{ __('Add Account') }}'"></h3>
|
|
<button @click="showModal = false"
|
|
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
|
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
|
d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<form x-ref="accountForm"
|
|
:action="!editing ? '{{ route($baseRoute . '.store') }}' : '{{ route($baseRoute) }}/' + currentUser.id"
|
|
method="POST" class="space-y-6">
|
|
@csrf
|
|
<template x-if="editing">
|
|
<input type="hidden" name="_method" value="PUT">
|
|
</template>
|
|
|
|
<div class="space-y-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="space-y-2">
|
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
|
|
{{ __('Full Name') }} <span class="text-rose-500">*</span>
|
|
</label>
|
|
<input type="text" name="name" x-model="currentUser.name" required
|
|
class="luxury-input @error('name') border-rose-500 @enderror"
|
|
placeholder="{{ __('e.g. John Doe') }}">
|
|
</div>
|
|
<div class="space-y-2">
|
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
|
|
{{ __('Username') }} <span class="text-rose-500">*</span>
|
|
</label>
|
|
<input type="text" name="username" x-model="currentUser.username" required
|
|
class="luxury-input @error('username') border-rose-500 @enderror"
|
|
placeholder="{{ __('e.g. johndoe') }}">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="space-y-2">
|
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
|
|
{{ __('Email') }}
|
|
</label>
|
|
<input type="email" name="email" x-model="currentUser.email"
|
|
class="luxury-input @error('email') border-rose-500 @enderror"
|
|
placeholder="{{ __('john@example.com') }}">
|
|
</div>
|
|
<div class="space-y-2">
|
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Phone') }}</label>
|
|
<input type="text" name="phone" x-model="currentUser.phone"
|
|
class="luxury-input @error('phone') border-rose-500 @enderror">
|
|
</div>
|
|
</div>
|
|
|
|
@if(auth()->user()->isSystemAdmin())
|
|
<div class="space-y-2 mb-6 relative z-30">
|
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Affiliation') }}</label>
|
|
<x-searchable-select id="modal-account-company" name="company_id"
|
|
placeholder="{{ __('SYSTEM') }}" x-model="currentUser.company_id"
|
|
@change="currentUser.company_id = $event.target.value; updateRoleSelect()">
|
|
@foreach($companies as $company)
|
|
<option value="{{ $company->id }}" data-title="{{ $company->name }}">
|
|
{{ $company->name }}
|
|
</option>
|
|
@endforeach
|
|
</x-searchable-select>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 relative z-20">
|
|
<div class="space-y-2">
|
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
|
|
{{ __('Role') }} <span class="text-rose-500">*</span>
|
|
</label>
|
|
<div id="role-select-wrapper" class="relative">
|
|
<!-- Dynamic -->
|
|
</div>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Status') }}</label>
|
|
<x-searchable-select id="modal-account-status" name="status"
|
|
x-model="currentUser.status" :hasSearch="false">
|
|
<option value="1">{{ __('Active') }}</option>
|
|
<option value="0">{{ __('Disabled') }}</option>
|
|
</x-searchable-select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
|
|
<span x-text="editing ? '{{ __('New Password (leave blank to keep current)') }}' : '{{ __('Password') }}'"></span>
|
|
<template x-if="!editing">
|
|
<span class="text-rose-500">*</span>
|
|
</template>
|
|
</label>
|
|
<div x-data="{ show: false }" class="relative items-center">
|
|
<input :type="show ? 'text' : 'password'" name="password" :required="!editing"
|
|
class="luxury-input w-full pr-12 @error('password') border-rose-500 @enderror"
|
|
placeholder="••••••••">
|
|
<button type="button" @click="show = !show"
|
|
class="absolute inset-y-0 end-0 flex items-center z-20 px-4 cursor-pointer text-slate-400 hover:text-cyan-500 transition-colors">
|
|
<svg x-show="!show" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
<svg x-show="show" x-cloak class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-x-4 pt-8">
|
|
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
|
|
<button type="submit" class="btn-luxury-primary px-12">
|
|
<span x-text="editing ? '{{ __('Update') }}' : '{{ __('Create') }}'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modals -->
|
|
<x-delete-confirm-modal :message="__('Are you sure you want to delete this account? This action cannot be undone.')" />
|
|
<x-status-confirm-modal :title="__('Confirm Account Deactivation')" :message="__('Are you sure you want to deactivate this account? After deactivating, this account will no longer be able to log in to the system.')" />
|
|
|
|
<form x-ref="statusToggleForm" :action="toggleFormAction" method="POST" class="hidden">
|
|
@csrf
|
|
@method('PATCH')
|
|
</form>
|
|
|
|
<!-- Global Delete Warning Modal -->
|
|
<div x-show="isWarningModalOpen" class="fixed inset-0 z-[200] overflow-y-auto" x-cloak>
|
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
|
<div x-show="isWarningModalOpen" x-transition:enter="ease-out duration-300"
|
|
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
|
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0" class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm"
|
|
@click="isWarningModalOpen = false"></div>
|
|
|
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
|
|
|
<div x-show="isWarningModalOpen" x-transition:enter="ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-8 border border-slate-100 dark:border-slate-800">
|
|
|
|
<div class="sm:flex sm:items-start">
|
|
<div class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-rose-100 dark:bg-rose-500/10 rounded-2xl sm:mx-0 sm:h-12 sm:w-12 text-rose-600 dark:text-rose-400">
|
|
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
|
</svg>
|
|
</div>
|
|
<div class="mt-3 text-center sm:mt-0 sm:ml-6 sm:text-left">
|
|
<h3 class="text-xl font-black text-slate-800 dark:text-white leading-6 tracking-tight outfit-font">
|
|
{{ __('Cannot Delete Role') }}
|
|
</h3>
|
|
<div class="mt-4">
|
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 leading-relaxed" x-text="deleteWarningMsg"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-8 sm:mt-10 sm:flex sm:flex-row-reverse">
|
|
<button type="button" @click="isWarningModalOpen = false"
|
|
class="inline-flex justify-center w-full px-8 py-3 text-sm font-black text-white transition-all bg-slate-800 dark:bg-slate-700 rounded-xl hover:bg-slate-900 dark:hover:bg-slate-600 sm:w-auto tracking-widest uppercase">
|
|
{{ __('Got it') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@section('scripts')
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('accountManager', (initData) => ({
|
|
showModal: initData.errors,
|
|
editing: initData.oldValues.method === 'PUT' || (initData.oldValues.id && initData.errors),
|
|
allRoles: initData.roles,
|
|
currentUser: {
|
|
id: initData.oldValues.id || '',
|
|
name: initData.oldValues.name || '',
|
|
username: initData.oldValues.username || '',
|
|
email: initData.oldValues.email || '',
|
|
phone: initData.oldValues.phone || '',
|
|
company_id: initData.oldValues.company_id || '',
|
|
role: initData.oldValues.role || '',
|
|
status: initData.oldValues.status || 1
|
|
},
|
|
tab: initData.tab || 'accounts',
|
|
roleSelectConfig: initData.roleSelectConfig,
|
|
isDeleteConfirmOpen: false,
|
|
deleteFormAction: '',
|
|
isStatusConfirmOpen: false,
|
|
toggleFormAction: '',
|
|
statusToggleSource: 'list',
|
|
|
|
isWarningModalOpen: false,
|
|
deleteWarningMsg: '',
|
|
triggerDeleteWarning(msg) {
|
|
this.deleteWarningMsg = msg;
|
|
this.isWarningModalOpen = true;
|
|
},
|
|
|
|
switchTab(newTab) {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.set('tab', newTab);
|
|
// Clear search and filters when switching tabs if desired, but here we just go to the URL
|
|
window.location.href = url.toString();
|
|
},
|
|
|
|
confirmDelete(action) {
|
|
this.deleteFormAction = action;
|
|
this.isDeleteConfirmOpen = true;
|
|
},
|
|
|
|
submitConfirmedForm() {
|
|
if (this.statusToggleSource === 'list') {
|
|
this.$refs.statusToggleForm.submit();
|
|
} else {
|
|
this.$refs.accountForm.submit();
|
|
}
|
|
},
|
|
|
|
get filteredRoles() {
|
|
const companyId = this.currentUser.company_id;
|
|
if (!companyId || companyId.toString().trim() === '' || companyId === ' ') {
|
|
return this.allRoles.filter(r => !r.company_id || r.company_id.toString().trim() === '');
|
|
} else {
|
|
let companyRoles = this.allRoles.filter(r => r.company_id == companyId);
|
|
if (companyRoles.length > 0) {
|
|
return companyRoles;
|
|
} else {
|
|
return this.allRoles.filter(r => (!r.company_id || r.company_id.toString().trim() === '') && r.name !== 'super-admin');
|
|
}
|
|
}
|
|
},
|
|
|
|
openCreateModal() {
|
|
this.editing = false;
|
|
const initialCompanyId = initData.oldValues.company_id || '';
|
|
let initialRole = '';
|
|
|
|
const roles = this.filteredRoles;
|
|
if (roles.length > 0) {
|
|
initialRole = roles[0].name;
|
|
}
|
|
|
|
this.currentUser = {
|
|
id: '',
|
|
name: '',
|
|
username: '',
|
|
email: '',
|
|
phone: '',
|
|
company_id: initialCompanyId,
|
|
role: initialRole,
|
|
status: 1
|
|
};
|
|
this.showModal = true;
|
|
this.$nextTick(() => {
|
|
this.updateRoleSelect();
|
|
});
|
|
},
|
|
|
|
openEditModal(user) {
|
|
this.editing = true;
|
|
this.currentUser = {
|
|
...user,
|
|
company_id: user.company_id || ' ',
|
|
role: user.roles && user.roles.length > 0 ? user.roles[0].name : '',
|
|
status: user.status
|
|
};
|
|
this.showModal = true;
|
|
this.$nextTick(() => {
|
|
this.syncSelect('modal-account-company', this.currentUser.company_id);
|
|
this.syncSelect('modal-account-status', this.currentUser.status);
|
|
this.updateRoleSelect();
|
|
});
|
|
},
|
|
|
|
syncSelect(id, value) {
|
|
this.$nextTick(() => {
|
|
const el = document.getElementById(id);
|
|
if (el) {
|
|
const valStr = (value !== undefined && value !== null && value.toString().trim() !== '') ? value.toString() : ' ';
|
|
el.value = valStr;
|
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
|
if (window.HSSelect && window.HSSelect.getInstance(el)) {
|
|
window.HSSelect.getInstance(el).setValue(valStr);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
updateRoleSelect() {
|
|
this.$nextTick(() => {
|
|
const wrapper = document.getElementById('role-select-wrapper');
|
|
if (!wrapper) return;
|
|
|
|
const cleanConfig = JSON.parse(JSON.stringify(this.roleSelectConfig), (key, value) => {
|
|
return typeof value === 'string' ? value.replace(/\r?\n|\r/g, ' ').trim() : value;
|
|
});
|
|
const configStr = JSON.stringify(cleanConfig);
|
|
const roles = this.filteredRoles;
|
|
|
|
if (roles.length > 0 && !roles.find(r => r.name === this.currentUser.role)) {
|
|
this.currentUser.role = roles[0].name;
|
|
} else if (roles.length === 0) {
|
|
this.currentUser.role = '';
|
|
}
|
|
|
|
const oldSelects = wrapper.querySelectorAll('select');
|
|
oldSelects.forEach(oldSelect => {
|
|
if (window.HSSelect && window.HSSelect.getInstance(oldSelect)) {
|
|
try { window.HSSelect.getInstance(oldSelect).destroy(); } catch (e) { console.warn('HSSelect destroy warning:', e); }
|
|
}
|
|
});
|
|
|
|
wrapper.innerHTML = '';
|
|
const selectEl = document.createElement('select');
|
|
selectEl.name = 'role';
|
|
const uniqueSelectId = 'modal-account-role-' + Date.now() + '-' + Math.round(Math.random() * 1000);
|
|
selectEl.id = uniqueSelectId;
|
|
selectEl.className = 'hidden';
|
|
selectEl.setAttribute('data-hs-select', configStr);
|
|
|
|
if (roles.length === 0) {
|
|
const opt = document.createElement('option');
|
|
opt.value = '';
|
|
opt.textContent = '{{ __("No roles available") }}';
|
|
opt.disabled = true;
|
|
opt.selected = true;
|
|
selectEl.appendChild(opt);
|
|
} else {
|
|
roles.forEach(r => {
|
|
const opt = document.createElement('option');
|
|
opt.value = r.name;
|
|
opt.textContent = r.name;
|
|
opt.setAttribute('data-title', r.name);
|
|
if (r.name === this.currentUser.role) opt.selected = true;
|
|
selectEl.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
wrapper.appendChild(selectEl);
|
|
selectEl.addEventListener('change', (e) => {
|
|
this.currentUser.role = e.target.value;
|
|
});
|
|
|
|
this._roleGeneration = (this._roleGeneration || 0) + 1;
|
|
const currentGen = this._roleGeneration;
|
|
|
|
const waitForHSSelect = (attempts = 0) => {
|
|
if (currentGen !== this._roleGeneration) return;
|
|
const select = window.HSSelect ? window.HSSelect.getInstance(selectEl) : null;
|
|
if (select) {
|
|
select.setValue(this.currentUser.role || '');
|
|
} else if (attempts < 20) {
|
|
setTimeout(() => waitForHSSelect(attempts + 1), 50);
|
|
}
|
|
};
|
|
|
|
const initPreline = (attempts = 0) => {
|
|
if (currentGen !== this._roleGeneration) return;
|
|
if (window.HSStaticMethods && window.HSStaticMethods.autoInit) {
|
|
try {
|
|
window.HSStaticMethods.autoInit(['select']);
|
|
waitForHSSelect();
|
|
} catch (e) {
|
|
console.warn('HSStaticMethods autoInit warning:', e);
|
|
}
|
|
} else if (attempts < 50) {
|
|
setTimeout(() => initPreline(attempts + 1), 50);
|
|
}
|
|
};
|
|
initPreline();
|
|
});
|
|
},
|
|
|
|
init() {
|
|
this.$watch('currentUser.company_id', (value) => {
|
|
this.syncSelect('modal-account-company', value);
|
|
if (this.filteredRoles.length > 0 && !this.filteredRoles.find(r => r.name === this.currentUser.role)) {
|
|
this.currentUser.role = this.filteredRoles[0].name;
|
|
}
|
|
this.updateRoleSelect();
|
|
});
|
|
|
|
this.$watch('currentUser.status', (value) => {
|
|
this.syncSelect('modal-account-status', value);
|
|
});
|
|
|
|
this.$nextTick(() => {
|
|
this.updateRoleSelect();
|
|
});
|
|
}
|
|
}));
|
|
});
|
|
</script>
|
|
@endsection |