- 機台日誌:對齊 Luxury UI 規範,實作整合式佈局與分頁組件。 - 多語系:完成機台日誌繁、英、日三語系翻譯與動態處理。 - UI 規範:更新 SKILL.md 定義「標準列表 Bible」。 - 後端:完善 TenantScoped 隔離邏輯,修復儀表板死循環與 User Model 缺失。 - IoT:擴展機台、會員 Model 並建立交易、商品、狀態等核心表結構。 - 基礎設施:設置台北時區與 Docker 環境變數同步。
260 lines
19 KiB
PHP
260 lines
19 KiB
PHP
@extends('layouts.admin')
|
|
|
|
@section('content')
|
|
<div class="space-y-6" x-data="{
|
|
showModal: false,
|
|
editing: false,
|
|
currentUser: {
|
|
id: '',
|
|
name: '',
|
|
username: '',
|
|
email: '',
|
|
phone: '',
|
|
company_id: '',
|
|
role: 'user',
|
|
status: 1
|
|
},
|
|
openCreateModal() {
|
|
this.editing = false;
|
|
this.currentUser = { id: '', name: '', username: '', email: '', phone: '', company_id: '', role: 'user', status: 1 };
|
|
this.showModal = true;
|
|
},
|
|
openEditModal(user) {
|
|
this.editing = true;
|
|
this.currentUser = {
|
|
...user,
|
|
role: user.roles && user.roles.length > 0 ? user.roles[0].name : 'user'
|
|
};
|
|
this.showModal = true;
|
|
}
|
|
}">
|
|
<!-- Header -->
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
|
<div>
|
|
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Account Management') }}</h1>
|
|
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 mt-1 uppercase tracking-[0.2em]">{{ __('Manage administrative and tenant accounts') }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Accounts Content (Integrated Card) -->
|
|
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
|
<!-- Filters & Search -->
|
|
<form action="{{ route('admin.permission.accounts') }}" method="GET" class="mb-10">
|
|
<div class="flex flex-col md:flex-row items-start md:items-center gap-4 w-full md:w-auto">
|
|
<div class="relative group w-full md:w-80">
|
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
|
<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="3" 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 luxury-input" placeholder="{{ __('Search users...') }}">
|
|
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
|
</div>
|
|
|
|
@if(auth()->user()->isSystemAdmin())
|
|
<select name="company_id" onchange="this.form.submit()" class="luxury-select w-full md:w-auto min-w-[200px]">
|
|
<option value="">{{ __('All Companies') }}</option>
|
|
@foreach($companies as $company)
|
|
<option value="{{ $company->id }}" {{ request('company_id') == $company->id ? 'selected' : '' }}>{{ $company->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
@endif
|
|
</div>
|
|
</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-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('User Info') }}</th>
|
|
@if(auth()->user()->isSystemAdmin())
|
|
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Belongs To') }}</th>
|
|
@endif
|
|
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Role') }}</th>
|
|
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
|
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] 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-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 overflow-hidden group-hover:bg-cyan-500/10 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-all duration-500">
|
|
@if($user->avatar)
|
|
<img src="{{ Storage::url($user->avatar) }}" class="w-full h-full object-cover">
|
|
@else
|
|
<span class="text-xs font-black">{{ substr($user->name, 0, 1) }}</span>
|
|
@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-[11px] font-bold text-slate-400 dark:text-slate-500 mt-0.5 tracking-tight">{{ $user->username }} @if($user->email) • {{ $user->email }} @endif</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
@if(auth()->user()->isSystemAdmin())
|
|
<td class="px-6 py-6">
|
|
@if($user->company)
|
|
<span class="text-xs font-bold text-slate-600 dark:text-slate-300 tracking-tight">{{ $user->company->name }}</span>
|
|
@else
|
|
<span class="px-2.5 py-1 rounded-lg text-[10px] font-black 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-[10px] font-black bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 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-3 py-1 rounded-full text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-wider uppercase">
|
|
<span class="size-1.5 rounded-full bg-emerald-500 mr-2 animate-pulse"></span>
|
|
{{ __('Active') }}
|
|
</span>
|
|
@else
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 border border-slate-200 dark:border-slate-700 tracking-wider uppercase">
|
|
{{ __('Disabled') }}
|
|
</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-6 text-right">
|
|
<div class="flex justify-end items-center gap-2">
|
|
<button @click='openEditModal(@json($user))' class="p-2 rounded-xl bg-slate-50/50 dark:bg-slate-900/30 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/10 transition-all border border-transparent hover:border-cyan-500/20 shadow-sm">
|
|
<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('admin.permission.accounts.destroy', $user->id) }}" method="POST" onsubmit="return confirm('{{ __('Are you sure you want to delete this account?') }}')">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="submit" class="p-2 rounded-xl bg-slate-50/50 dark:bg-slate-900/30 text-slate-400 hover:text-rose-500 hover:bg-rose-500/10 transition-all border border-transparent hover:border-rose-500/20 shadow-sm">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="size-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>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="{{ auth()->user()->isSystemAdmin() ? 5 : 4 }}" 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 dark:border-slate-800 pt-6">
|
|
{{ $users->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">​</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 overflow-hidden 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">
|
|
|
|
<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 :action="editing ? '{{ url('admin/permission/accounts') }}/' + currentUser.id : '{{ route('admin.permission.accounts.store') }}'" method="POST" class="space-y-6">
|
|
@csrf
|
|
<template x-if="editing">
|
|
<input type="hidden" name="_method" value="PUT">
|
|
</template>
|
|
|
|
<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') }}</label>
|
|
<input type="text" name="name" x-model="currentUser.name" required class="luxury-input" 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') }}</label>
|
|
<input type="text" name="username" x-model="currentUser.username" required class="luxury-input" 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" 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">
|
|
</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">{{ __('Role') }}</label>
|
|
<select name="role" x-model="currentUser.role" class="luxury-select">
|
|
@foreach($roles as $role)
|
|
<option value="{{ $role->name }}">{{ __($role->name) }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Status') }}</label>
|
|
<select name="status" x-model="currentUser.status" class="luxury-select">
|
|
<option value="1">{{ __('Active') }}</option>
|
|
<option value="0">{{ __('Disabled') }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
@if(auth()->user()->isSystemAdmin())
|
|
<div class="space-y-2">
|
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Company') }}</label>
|
|
<select name="company_id" x-model="currentUser.company_id" class="luxury-select">
|
|
<option value="">{{ __('SYSTEM') }}</option>
|
|
@foreach($companies as $company)
|
|
<option value="{{ $company->id }}">{{ $company->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
@endif
|
|
|
|
<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>
|
|
</label>
|
|
<input type="password" name="password" :required="!editing" class="luxury-input" 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>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@endsection
|