Files
star-cloud/resources/views/admin/data-config/accounts.blade.php
sky121113 c875ab7d29
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 46s
[FEAT] 移除「商品狀態」冗餘模組、優化麵包屑導航與完善帳號角色過濾邏輯
2026-03-27 16:53:43 +08:00

929 lines
57 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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">
{{ __('Manage administrative and tenant accounts') }}
</p>
</div>
<div class="flex items-center gap-3">
@if($tab === 'accounts')
<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>
<!-- Tabs Navigation -->
<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">
<a href="{{ route($baseRoute, ['tab' => 'accounts']) }}"
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all {{ $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' }}">
{{ __('Account Management') }}
</a>
<a href="{{ route($baseRoute, ['tab' => 'permissions']) }}"
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all {{ $tab === 'permissions' ? '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' }}">
{{ __('Authorized Machines') }}
</a>
</div>
<!-- Accounts Content (Integrated Card) -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in mt-6">
<!-- 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="{{ $tab }}">
<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...') }}">
</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) }}">
</form>
<div class="overflow-x-auto">
@if($tab === 'accounts')
<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>
@else
<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>
@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">
{{ __('Authorized Machines') }}</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">
{{ __('Action') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@foreach ($users as $account)
<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($account->avatar)
<img src="{{ Storage::url($account->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">{{
$account->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">{{ $account->username }}</span></span>
</div>
</div>
</td>
@if(auth()->user()->isSystemAdmin())
<td class="px-6 py-6">
@if($account->company)
<span
class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{
$account->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 min-w-[240px]">
<div class="flex flex-wrap gap-1.5 overflow-hidden items-center"
id="machines-container-{{ $account->id }}">
@if(!$account->company_id)
<span
class="px-3 py-1 text-[10px] bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 rounded-lg border border-cyan-500/20 uppercase font-black tracking-[0.2em] shadow-sm">
{{ __('Full Access') }}
</span>
@else
@php $assigned = $account->machines; @endphp
@if($assigned->isNotEmpty())
@foreach($assigned->take(3) as $machine)
<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 shadow-sm">
{{ $machine->name }}
</span>
@endforeach
@if($assigned->count() > 3)
<span
class="px-2 py-0.5 text-xs bg-cyan-50 dark:bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 rounded border border-cyan-100 dark:border-cyan-500/20 uppercase font-bold tracking-widest shadow-sm cursor-help transition-all hover:bg-cyan-100 dark:hover:bg-cyan-500/20"
title="{{ $assigned->pluck('name')->implode(', ') }}">
+{{ $assigned->count() - 3 }}
</span>
@endif
@else
<span class="text-[10px] font-bold text-slate-400 italic">{{ __('No machines assigned')
}}</span>
@endif
@endif
</div>
</td>
<td class="px-6 py-6 text-right">
@if(!$account->company_id)
<span
class="text-[10px] font-black text-slate-300 dark:text-slate-600 uppercase tracking-[0.15em] px-2">
{{ __('System Default') }}
</span>
@else
<button @click="openMachineModal({{ $account->id }}, '{{ $account->name }}')"
class="btn-luxury-primary !px-4 !py-2 !text-[11px] !shadow-sm uppercase tracking-widest">
<svg class="w-3.5 h-3.5 mr-1.5 inline-block" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4">
</path>
</svg>
<span class="align-middle">{{ __('Assign') }}</span>
</button>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
@endif
</div>
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
{{ $users->appends(['tab' => $tab])->links('vendor.pagination.luxury') }}
</div>
</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">&#8203;</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()">
{{-- 選項由組件根據 placeholder 自動生成 value=' ' 的項目 --}}
@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">
<!-- updateRoleSelect() 動態渲染 -->
</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>
<input type="password" name="password" :required="!editing"
class="luxury-input @error('password') border-rose-500 @enderror"
placeholder="••••••••">
</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>
<!-- Machine Assignment Modal -->
<template x-teleport="body">
<div x-show="showMachineModal"
class="fixed inset-0 z-[100] flex items-center justify-center overflow-y-auto px-4 py-8" x-cloak>
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity" x-show="showMachineModal"
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"
@click="showMachineModal = false"></div>
<div class="luxury-card !bg-white dark:!bg-slate-900 w-full max-w-2xl mx-auto rounded-[2.5rem] shadow-2xl relative overflow-hidden animate-luxury-in border border-slate-100 dark:border-slate-800"
@click.stop x-show="showMachineModal" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95">
<div
class="px-10 py-8 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center relative z-10">
<div>
<h3 class="text-2xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{
__('Assign Machines') }}</h3>
<p class="text-[11px] font-black text-cyan-500 uppercase tracking-[0.2em] mt-1"
x-text="selectedUserName"></p>
</div>
</div>
<div class="p-8 max-h-[60vh] overflow-y-auto custom-scrollbar">
<template x-if="loading">
<div class="flex flex-col items-center justify-center py-20">
<div
class="w-12 h-12 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
</div>
<p class="mt-4 text-xs font-bold text-slate-400 uppercase tracking-widest">{{ __('Loading
machines...') }}</p>
</div>
</template>
<template x-if="!loading && machines.length === 0">
<div class="text-center py-20 text-slate-400 font-bold uppercase tracking-widest">{{ __('No
machines available') }}</div>
</template>
<template x-if="!loading && machines.length > 0">
<div class="space-y-6">
<div class="flex items-center justify-between px-2">
<label class="flex items-center gap-3 cursor-pointer group">
<input type="checkbox" @change="toggleAll" :checked="isAllSelected"
class="size-5 rounded-lg border-2 border-slate-300 dark:border-white/20 text-cyan-600 focus:ring-cyan-500 bg-transparent">
<span
class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest group-hover:text-cyan-500 transition-colors">{{
__('Select All') }}</span>
</label>
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest"
x-text="`${assignedIds.length} / ${machines.length} {{ __('Selected') }}`"></span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<template x-for="machine in machines" :key="machine.id">
<label
class="group relative flex items-center p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border-2 border-transparent hover:border-cyan-500/30 transition-all cursor-pointer has-[:checked]:border-cyan-500 has-[:checked]:bg-cyan-500/[0.03]">
<input type="checkbox" :value="machine.id.toString()" x-model="assignedIds"
class="size-5 rounded-lg border-2 border-slate-300 dark:border-white/20 text-cyan-600 focus:ring-cyan-500 bg-transparent">
<div class="ml-4 flex-1">
<div class="text-sm font-black text-slate-700 dark:text-slate-200 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors"
x-text="machine.name"></div>
<div class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-widest mt-0.5"
x-text="machine.serial_no"></div>
</div>
</label>
</template>
</div>
</div>
</template>
</div>
<div
class="px-10 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 border-t border-slate-100 dark:border-slate-800">
<button @click="showMachineModal = false" class="btn-luxury-ghost">{{ __('Cancel') }}</button>
<button @click="savePermissions" class="btn-luxury-primary px-8" :disabled="saving"
:class="saving ? 'opacity-50 cursor-not-allowed' : ''">
<template x-if="!saving"><span>{{ __('Save Changes') }}</span></template>
<template x-if="saving">
<div class="flex items-center gap-2">
<div class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin">
</div><span>{{ __('Saving...') }}</span>
</div>
</template>
</button>
</div>
</div>
</div>
</template>
<!-- Global Delete Confirm Modal -->
<x-delete-confirm-modal
:message="__('Are you sure you want to delete this account? This action cannot be undone.')" />
<!-- Status Change Confirm Modal -->
<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>
</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), // Added logic for editing when errors exist on an existing user
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
},
roleSelectConfig: initData.roleSelectConfig,
isDeleteConfirmOpen: false,
deleteFormAction: '',
isStatusConfirmOpen: false,
toggleFormAction: '',
statusToggleSource: 'list',
confirmDelete(action) {
this.deleteFormAction = action;
this.isDeleteConfirmOpen = true;
},
tab: initData.tab,
showMachineModal: false,
selectedUserId: '',
selectedUserName: '',
machines: [],
assignedIds: [],
loading: false,
saving: false,
get isAllSelected() {
return this.machines.length > 0 && this.assignedIds.length === this.machines.length;
},
async openMachineModal(userId, userName) {
this.selectedUserId = userId;
this.selectedUserName = userName;
this.showMachineModal = true;
this.loading = true;
this.assignedIds = [];
try {
const url = `{{ route('admin.machines.permissions.accounts.get', 'USER_ID') }}`.replace('USER_ID', userId);
const response = await fetch(url);
const data = await response.json();
this.machines = data.machines;
this.assignedIds = data.assigned_ids.map(id => id.toString());
} catch (error) {
console.error('Error fetching data:', error);
window.Alpine.store('toast').show('{{ __("Failed to fetch machine data.") }}', 'error');
} finally {
this.loading = false;
}
},
toggleAll() {
if (this.isAllSelected) {
this.assignedIds = [];
} else {
this.assignedIds = this.machines.map(m => m.id.toString());
}
},
async savePermissions() {
this.saving = true;
try {
const url = `{{ route('admin.machines.permissions.accounts.sync', 'USER_ID') }}`.replace('USER_ID', this.selectedUserId);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ machine_ids: this.assignedIds })
});
const data = await response.json();
if (data.success) {
this.showMachineModal = false;
window.Alpine.store('toast').show(data.message, 'success');
const container = document.getElementById(`machines-container-${this.selectedUserId}`);
if (container) {
const assigned = data.assigned_machines;
if (assigned.length > 0) {
const visible = assigned.slice(0, 3);
const extraCount = assigned.length - 3;
const allNames = assigned.map(m => m.name).join(', ');
let html = visible.map(m => `<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 shadow-sm">${m.name}</span>`).join('');
if (extraCount > 0) {
html += `<span class="px-2 py-0.5 text-xs bg-cyan-50 dark:bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 rounded border border-cyan-100 dark:border-cyan-500/20 uppercase font-bold tracking-widest shadow-sm cursor-help transition-all hover:bg-cyan-100 dark:hover:bg-cyan-500/20" title="${allNames}">+${extraCount}</span>`;
}
container.innerHTML = html;
} else {
container.innerHTML = `<span class="text-[10px] font-bold text-slate-400 italic">{{ __('No machines assigned') }}</span>`;
}
}
} else {
window.Alpine.store('toast').show(data.error || '{{ __("Failed to save permissions.") }}', 'error');
}
} catch (error) {
console.error('Error saving permissions:', error);
window.Alpine.store('toast').show('{{ __("An error occurred while saving.") }}', 'error');
} finally {
this.saving = false;
}
},
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() === '') {
// 系統管理層級:僅顯示全域角色 (company_id 為空)
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 {
// 租戶層級 fallback顯示全域角色但明確排除 super-admin
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 = '';
let roles = [];
if (!initialCompanyId || initialCompanyId.toString().trim() === '') {
roles = this.allRoles.filter(r => !r.company_id || r.company_id.toString().trim() === '');
} else {
let companyRoles = this.allRoles.filter(r => r.company_id == initialCompanyId);
// 這裡也要同步排除 super-admin
roles = companyRoles.length > 0 ? companyRoles : this.allRoles.filter(r => (!r.company_id || r.company_id.toString().trim() === '') && r.name !== 'super-admin');
}
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;
// 🛡️ 終極防護:自動過濾配置中的換行符號,防止自動排版工具折行導致 Preline 崩潰
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