[FEAT] 優化部署流程:加入 RoleSeeder 與 AdminUserSeeder,並實作權限系統基礎架構與多租戶隔離機制
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 48s

This commit is contained in:
2026-03-13 17:35:22 +08:00
parent 39d25ed1d4
commit 56daf8940b
41 changed files with 3052 additions and 358 deletions

View File

@@ -171,4 +171,34 @@
@apply text-slate-500 hover:bg-slate-100 hover:text-slate-900;
@apply dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-cyan-400;
}
/* Luxury Form Elements */
.luxury-input, .luxury-select {
@apply py-3 px-5 block w-full border-slate-200 rounded-xl text-sm font-bold text-slate-700 placeholder-slate-400 transition-all duration-200 outline-none border;
@apply dark:bg-slate-900/40 dark:border-slate-700 dark:text-slate-200 dark:placeholder-slate-500;
backdrop-filter: blur(4px);
}
.luxury-input:focus, .luxury-select:focus {
@apply ring-4 ring-cyan-500/10 border-cyan-500;
@apply dark:ring-cyan-500/20 dark:border-cyan-400/50;
}
/* Date Input Calendar Icon Optimization */
.luxury-input[type="date"]::-webkit-calendar-picker-indicator {
@apply cursor-pointer transition-opacity hover:opacity-100;
opacity: 0.6;
}
.dark .luxury-input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(1);
}
.luxury-select {
@apply appearance-none pr-10;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2364748b' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.85rem center;
background-repeat: no-repeat;
background-size: 1.25em 1.25em;
}
}

View File

@@ -0,0 +1,397 @@
@extends('layouts.admin')
@section('content')
<div class="space-y-6" x-data="{
showModal: false,
editing: false,
currentCompany: {
id: '',
name: '',
code: '',
tax_id: '',
contact_name: '',
contact_phone: '',
contact_email: '',
valid_until: '',
status: 1,
note: ''
},
openCreateModal() {
this.editing = false;
this.currentCompany = { id: '', name: '', code: '', tax_id: '', contact_name: '', contact_phone: '', contact_email: '', valid_until: '', status: 1, note: '' };
this.showModal = true;
},
openEditModal(company) {
this.editing = true;
this.currentCompany = { ...company };
this.showModal = true;
}
}">
<!-- 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">{{ __('Customer Management') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage all tenant accounts and validity') }}</p>
</div>
<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 Customer') }}</span>
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="luxury-card p-6 rounded-2xl animate-luxury-in">
<p class="text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Total Customers') }}</p>
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $companies->total() }}</p>
</div>
<div class="luxury-card p-6 rounded-2xl animate-luxury-in" style="animation-delay: 100ms">
<p class="text-sm font-black text-emerald-500 uppercase tracking-widest">{{ __('Active') }}</p>
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $companies->where('status',
1)->count() }}</p>
</div>
<div class="luxury-card p-6 rounded-2xl animate-luxury-in" style="animation-delay: 200ms">
<p class="text-sm font-black text-rose-500 uppercase tracking-widest">{{ __('Expired / Disabled') }}</p>
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $companies->where('status',
0)->count() }}</p>
</div>
</div>
<!-- Table Section -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 300ms">
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-6">
<form action="{{ route('admin.permission.companies.index') }}" method="GET" class="relative group">
<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-3 pl-12 pr-6 block w-64 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none"
placeholder="{{ __('Search customers...') }}">
</form>
<div class="flex items-center gap-4">
<form action="{{ route('admin.permission.companies.index') }}" method="GET" class="flex items-center gap-2">
@if(request('search'))<input type="hidden" name="search" value="{{ request('search') }}">@endif
@if(request('status'))<input type="hidden" name="status" value="{{ request('status') }}">@endif
<select name="limit" onchange="this.form.submit()" class="luxury-select text-[11px] py-2 px-3">
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
<option value="100" {{ request('limit') == 100 ? 'selected' : '' }}>100</option>
</select>
</form>
<div
class="flex items-center p-1 bg-slate-100/50 dark:bg-slate-900/50 backdrop-blur-md rounded-2xl border border-slate-200/50 dark:border-slate-700/50">
<a href="{{ route('admin.permission.companies.index') }}"
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ !request()->filled('status') ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-lg shadow-cyan-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300' }}">
{{ __('All') }}
</a>
<a href="{{ route('admin.permission.companies.index', ['status' => 1]) }}"
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('status') === '1' ? 'bg-white dark:bg-emerald-500/10 text-emerald-500 shadow-lg shadow-emerald-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-emerald-500/80' }}">
{{ __('Active') }}
</a>
<a href="{{ route('admin.permission.companies.index', ['status' => 0]) }}"
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('status') === '0' ? 'bg-white dark:bg-rose-500/10 text-rose-500 shadow-lg shadow-rose-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-rose-500/80' }}">
{{ __('Disabled') }}
</a>
</div>
</div>
</div>
<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/30">
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">
{{ __('Customer Info') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Status') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Accounts / Machines') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Valid Until') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest 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/50">
@forelse($companies as $company)
<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">
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2">
<path
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</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">{{
$company->name }}</span>
<span
class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-0.5 tracking-[0.15em]">{{
$company->code }}</span>
</div>
</div>
</td>
<td class="px-6 py-6 text-center">
@if($company->status)
<span
class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-wider uppercase">
{{ __('Active') }}
</span>
@else
<span
class="inline-flex items-center px-3 py-1 rounded-full text-[11px] 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-center">
<div class="flex items-center justify-center gap-x-3">
<div class="flex flex-col items-center">
<span class="text-base font-extrabold text-slate-800 dark:text-white">{{ $company->users_count }}</span>
<span class="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Users') }}</span>
</div>
<div class="w-px h-6 bg-slate-100 dark:bg-slate-800"></div>
<div class="flex flex-col items-center">
<span class="text-base font-extrabold text-slate-800 dark:text-white">{{ $company->machines_count }}</span>
<span class="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Machines') }}</span>
</div>
</div>
</td>
<td class="px-6 py-6 text-center">
<span
class="text-[13px] font-bold font-display tracking-widest {{ $company->valid_until && $company->valid_until->isPast() ? 'text-rose-500' : 'text-slate-600 dark:text-slate-300' }}">
{{ $company->valid_until ? $company->valid_until->format('Y/m/d') : __('Permanent') }}
</span>
</td>
<td class="px-6 py-6 text-right">
<div class="flex items-center justify-end gap-x-2">
<button @click="openEditModal({{ json_encode($company) }})"
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">
<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.companies.destroy', $company->id) }}"
method="POST"
onsubmit="return confirm('{{ addslashes(__('Are you sure to delete this customer?')) }}')"
class="inline">
@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">
<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="m14.74 9-.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 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-6 py-24 text-center">
<div class="flex flex-col items-center">
<div
class="w-16 h-16 rounded-2xl bg-slate-50 dark:bg-slate-900 flex items-center justify-center text-slate-300 mb-4">
<svg class="size-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<p class="text-slate-400 font-bold">{{ __('No customers found') }}</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
{{ $companies->links('vendor.pagination.luxury') }}
</div>
</div>
<!-- Create/Edit Modal (Simplified with standard Blade form for reliability) -->
<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 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 Customer') }}' : '{{ __('Add Customer') }}'"></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 ? '{{ route('admin.permission.companies.index') }}/' + currentCompany.id : '{{ route('admin.permission.companies.store') }}'"
method="POST" class="space-y-6">
@csrf
<template x-if="editing">
@method('PUT')
</template>
<!-- Profile Section -->
<div class="space-y-6">
<div class="flex items-center gap-3">
<div class="h-6 w-1 bg-cyan-500 rounded-full"></div>
<h4 class="text-xs font-black text-slate-800 dark:text-white uppercase tracking-widest">{{
__('Company Information') }}</h4>
</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">{{
__('Company Name') }}</label>
<input type="text" name="name" x-model="currentCompany.name" required
class="luxury-input w-full" placeholder="{{ __('e.g. Taiwan Star') }}">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Company Code') }}</label>
<input type="text" name="code" x-model="currentCompany.code" required
class="luxury-input w-full" placeholder="{{ __('e.g. TWSTAR') }}">
</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">{{
__('Tax ID (Optional)') }}</label>
<input type="text" name="tax_id" x-model="currentCompany.tax_id"
class="luxury-input w-full">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Contract Until (Optional)') }}</label>
<input type="date" name="valid_until" x-model="currentCompany.valid_until"
class="luxury-input w-full">
</div>
</div>
</div>
<!-- Admin Account Section (Account Creation) - Only show when creating -->
<div x-show="!editing" class="space-y-6 pt-6 border-t border-slate-100 dark:border-slate-800">
<div class="flex items-center gap-3">
<div class="h-6 w-1 bg-emerald-500 rounded-full"></div>
<h4 class="text-xs font-black text-slate-800 dark:text-white uppercase tracking-widest">
{{ __('Initial Admin Account') }}
<span class="text-[10px] text-slate-400 font-normal normal-case tracking-normal ml-2">({{ __('Optional') }})</span>
</h4>
</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">{{
__('Username') }}</label>
<input type="text" name="admin_username" class="luxury-input w-full"
placeholder="{{ __('Enter login ID') }}">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Password') }}</label>
<input type="password" name="admin_password" class="luxury-input w-full"
placeholder="{{ __('Min 8 characters') }}">
</div>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Admin Name') }}</label>
<input type="text" name="admin_name" class="luxury-input w-full" placeholder="{{ __('Admin display name') }}">
</div>
</div>
<!-- Contact Section -->
<div class="space-y-6 pt-6 border-t border-slate-100 dark:border-slate-800">
<div class="flex items-center gap-3">
<div class="h-6 w-1 bg-amber-500 rounded-full"></div>
<h4 class="text-xs font-black text-slate-800 dark:text-white uppercase tracking-widest">{{
__('Contact & Details') }}</h4>
</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">{{
__('Contact Name') }}</label>
<input type="text" name="contact_name" x-model="currentCompany.contact_name"
class="luxury-input w-full">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Contact Phone') }}</label>
<input type="text" name="contact_phone" x-model="currentCompany.contact_phone"
class="luxury-input w-full">
</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">{{
__('Status') }}</label>
<select name="status" x-model="currentCompany.status" class="luxury-select">
<option value="1">{{ __('Active') }}</option>
<option value="0">{{ __('Disabled') }}</option>
</select>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Notes') }}</label>
<textarea name="note" x-model="currentCompany.note" rows="2"
class="luxury-input w-full h-[46px] min-h-[46px] py-3"></textarea>
</div>
</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>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -122,13 +122,23 @@
<div class="flex items-center gap-x-3">
<h2 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Status List') }}</h2>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-lg text-xs 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-tighter">
{{ __('Total items', ['count' => count($latestActivities)]) }}
{{ __('Total items', ['count' => $machines->total()]) }}
</span>
</div>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1">{{ __('Real-time monitoring across all machines') }}</p>
</div>
<div class="flex items-center gap-x-4">
<form action="{{ route('admin.dashboard') }}" method="GET" class="flex flex-wrap items-center gap-4">
<div class="flex items-center gap-x-2">
<span class="text-[11px] font-black text-slate-400 uppercase tracking-widest hidden sm:inline">{{ __('Show') }}:</span>
<select name="limit" onchange="this.form.submit()" class="luxury-select py-3 min-w-[100px]">
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
<option value="100" {{ request('limit') == 100 ? 'selected' : '' }}>100</option>
</select>
</div>
<div class="relative group">
<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">
@@ -136,25 +146,25 @@
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" class="py-3 pl-12 pr-6 block w-64 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" placeholder="{{ __('Quick search...') }}">
<input type="text" name="search" value="{{ request('search') }}" class="py-3 pl-12 pr-6 block w-64 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" placeholder="{{ __('Quick search...') }}">
</div>
</div>
</form>
</div>
<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/30">
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Machine Info') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Running Status') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Today Cumulative Sales') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Current Stock') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Last Communication') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Alert Summary') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Machine Info') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Running Status') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Today Cumulative Sales') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Current Stock') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Last Signal') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Alert Summary') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/50">
@forelse($latestActivities as $activity)
@forelse($machines as $machine)
<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-5">
@@ -162,18 +172,24 @@
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/></svg>
</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">{{ $activity->machine->name ?? __('Machine Info') }}</span>
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-1 uppercase tracking-[0.15em]">(SN: {{ $activity->machine->serial_no ?? 'N/A' }})</span>
<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">{{ $machine->name }}</span>
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-1 uppercase tracking-[0.15em]">(SN: {{ $machine->serial_no }})</span>
</div>
</div>
</td>
<td class="px-6 py-6 text-center">
<span class="inline-flex items-center px-4 py-1.5 rounded-full text-[10px] font-black bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-300 border border-slate-200 dark:border-slate-700 tracking-widest uppercase">
{{ $activity->machine->status === 'online' ? __('Online') : __('Offline') }}
</span>
@if($machine->status === 'online')
<span class="inline-flex items-center px-4 py-1.5 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">
{{ __('Online') }}
</span>
@else
<span class="inline-flex items-center px-4 py-1.5 rounded-full text-[11px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">
{{ __('Offline') }}
</span>
@endif
</td>
<td class="px-6 py-6 text-center">
<span class="text-xl font-black text-slate-900 dark:text-slate-100">$ 0</span>
<span class="text-base font-extrabold text-slate-900 dark:text-slate-100">$ 0</span>
</td>
<td class="px-6 py-6">
<div class="flex flex-col items-center gap-y-2.5">
@@ -184,7 +200,9 @@
</div>
</td>
<td class="px-6 py-6 text-center">
<span class="text-sm text-slate-600 dark:text-slate-200 font-bold font-display tracking-tight bg-slate-50 dark:bg-slate-800 px-3 py-1 rounded-lg">{{ $activity->created_at->format('Y/m/d H:i') }}</span>
<span class="text-[13px] text-slate-600 dark:text-slate-200 font-bold font-display tracking-widest bg-slate-50 dark:bg-slate-800/50 px-4 py-1.5 rounded-lg border border-slate-100 dark:border-slate-700/50">
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->format('Y/m/d H:i') : '---' }}
</span>
</td>
<td class="px-6 py-6 text-right">
<span class="text-[11px] font-bold text-slate-400/30 dark:text-slate-500 uppercase tracking-widest group-hover:text-slate-400 transition-colors">{{ __('No alert summary') }}</span>
@@ -199,27 +217,8 @@
</table>
</div>
<!-- Table Footer: Pagination -->
<div class="mt-8 flex items-center justify-between border-t border-slate-100 dark:border-slate-800 pt-6">
<p class="text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">
{{ __('Showing :from to :to of :total items', ['from' => 1, 'to' => count($latestActivities), 'total' => count($latestActivities)]) }}
</p>
<div class="flex items-center gap-x-2">
<button class="inline-flex items-center gap-x-2 px-4 py-2 rounded-xl text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 19l-7-7 7-7"/></svg>
<span>{{ __('Previous') }}</span>
</button>
<div class="flex items-center gap-x-1">
<button class="w-8 h-8 rounded-lg text-xs font-black bg-cyan-500 text-white shadow-lg shadow-cyan-500/30">1</button>
<button class="w-8 h-8 rounded-lg text-xs font-black text-slate-500 hover:text-cyan-500 transition-colors">2</button>
<button class="w-8 h-8 rounded-lg text-xs font-black text-slate-500 hover:text-cyan-500 transition-colors">3</button>
</div>
<button class="inline-flex items-center gap-x-2 px-4 py-2 rounded-xl text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all">
<span>{{ __('Next') }}</span>
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7"/></svg>
</button>
</div>
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
{{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
</div>
</div>
</div>

View File

@@ -0,0 +1,259 @@
@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 };
this.showModal = true;
}
}">
<!-- 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">{{ __('Account Management') }}</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>
<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>
<!-- Filters & Search -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<form action="{{ route('admin.permission.accounts') }}" method="GET" class="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div class="flex flex-col md:flex-row items-start md:items-center gap-4">
<div class="relative group">
<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-3 pl-12 pr-6 block w-full md:w-80 luxury-input" placeholder="{{ __('Search users...') }}">
</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
<select name="limit" onchange="this.form.submit()" class="luxury-select w-full md:w-auto text-[11px]">
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
<option value="100" {{ request('limit') == 100 ? 'selected' : '' }}>100</option>
</select>
</div>
<div class="flex items-center gap-3">
<!-- 移除了冗餘的 Filter 按鈕,下拉選單具備自動提交功能 -->
</div>
</form>
<div class="overflow-x-auto mt-8">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('User Info') }}</th>
@if(auth()->user()->isSystemAdmin())
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Belongs To') }}</th>
@endif
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Role') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest 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/50">
@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-full 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">
@if($user->avatar)
<img src="{{ Storage::url($user->avatar) }}" class="w-full h-full object-cover">
@else
<span class="text-sm 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-[0.15em]">{{ $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="text-xs font-black 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-0.5 rounded-lg text-[11px] 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-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-wider uppercase">
{{ __('Active') }}
</span>
@else
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] 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_encode($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">
<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('{{ addslashes(__('Are you sure you want to delete this account?')) }}')">
@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">
<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">
<p class="text-slate-400 font-bold">{{ __('No users found') }}</p>
</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">&#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 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">
<option value="user">{{ __('User') }}</option>
<option value="admin">{{ __('Admin') }}</option>
<option value="super-admin">{{ __('Super Admin') }}</option>
</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

View File

@@ -13,10 +13,22 @@
<div class="p-6 text-gray-900">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-medium">機台列表</h3>
<div class="flex space-x-2">
<a href="{{ route('admin.machines.index') }}" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md text-sm transition">全部</a>
<a href="{{ route('admin.machines.index', ['status' => 'online']) }}" class="px-4 py-2 bg-green-100 text-green-700 hover:bg-green-200 rounded-md text-sm transition">線上</a>
<a href="{{ route('admin.machines.index', ['status' => 'error']) }}" class="px-4 py-2 bg-red-100 text-red-700 hover:bg-red-200 rounded-md text-sm transition">異常</a>
<div class="flex items-center gap-x-4">
<form method="GET" action="{{ route('admin.machines.index') }}" class="flex items-center gap-x-2">
@if(request('status'))
<input type="hidden" name="status" value="{{ request('status') }}">
@endif
<select name="limit" onchange="this.form.submit()" class="h-9 text-[11px] font-black bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700 rounded-lg focus:ring-cyan-500/20 focus:border-cyan-500 transition-all">
@foreach([10, 25, 50, 100] as $size)
<option value="{{ $size }}" {{ request('limit') == $size ? 'selected' : '' }}>{{ $size }} {{ __('Items') }}</option>
@endforeach
</select>
</form>
<div class="flex space-x-2">
<a href="{{ route('admin.machines.index') }}" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md text-sm transition">全部</a>
<a href="{{ route('admin.machines.index', ['status' => 'online']) }}" class="px-4 py-2 bg-green-100 text-green-700 hover:bg-green-200 rounded-md text-sm transition">線上</a>
<a href="{{ route('admin.machines.index', ['status' => 'error']) }}" class="px-4 py-2 bg-red-100 text-red-700 hover:bg-red-200 rounded-md text-sm transition">異常</a>
</div>
</div>
</div>
@@ -71,7 +83,7 @@
</div>
<div class="mt-6">
{{ $machines->links() }}
{{ $machines->links('vendor.pagination.luxury') }}
</div>
</div>
</div>

View File

@@ -40,6 +40,14 @@
<option value="error" {{ request('level') == 'error' ? 'selected' : '' }}>Error</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">筆數</label>
<select name="limit" class="h-9 text-[11px] font-black bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700 rounded-lg focus:ring-cyan-500/20 focus:border-cyan-500 transition-all">
@foreach([10, 20, 50, 100] as $size)
<option value="{{ $size }}" {{ request('limit', 20) == $size ? 'selected' : '' }}>{{ $size }} </option>
@endforeach
</select>
</div>
<div class="flex items-center gap-2">
<button type="submit" class="btn-luxury-primary">
<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="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
@@ -110,7 +118,7 @@
@if($logs->hasPages())
<div class="mt-6">
{{ $logs->appends(request()->query())->links() }}
{{ $logs->links('vendor.pagination.luxury') }}
</div>
@endif
</div>

View File

@@ -0,0 +1,163 @@
@extends('layouts.admin')
@section('content')
<div class="space-y-6" x-data="{
showModal: false,
isEdit: false,
roleId: '',
roleName: '',
modalTitle: '{{ __('Create Role') }}',
openModal(edit = false, id = '', name = '') {
this.isEdit = edit;
this.roleId = id;
this.roleName = name;
this.modalTitle = edit ? '{{ __('Edit Role') }}' : '{{ __('Create Role') }}';
this.showModal = true;
}
}">
<!-- Header -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Roles') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Define and manage security roles for the system.') }}</p>
</div>
<button @click="openModal()" class="btn-luxury-primary">
<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="M5 12h14"/><path d="M12 5v14"/></svg>
<span>{{ __('Add Role') }}</span>
</button>
</div>
<!-- Toolbar -->
<div class="luxury-card rounded-3xl p-6 mb-6 animate-luxury-in" style="animation-delay: 100ms">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
<form action="{{ route('admin.permission.roles') }}" method="GET" class="relative group">
<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 md:w-80 luxury-input" placeholder="{{ __('Search roles...') }}">
</form>
<form action="{{ route('admin.permission.roles') }}" method="GET" class="flex items-center gap-3">
@if(request('search'))<input type="hidden" name="search" value="{{ request('search') }}">@endif
<label class="text-[11px] font-black text-slate-400 uppercase tracking-widest">{{ __('Show') }}</label>
<select name="limit" onchange="this.form.submit()" class="luxury-select py-1.5 px-3 text-[11px] min-w-[70px]">
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
</select>
</form>
</div>
</div>
<!-- Roles List -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 200ms">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="border-b border-slate-100 dark:border-slate-700">
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Role Name') }}</th>
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Type') }}</th>
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest text-center">{{ __('Users') }}</th>
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest text-right">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800">
@forelse($roles as $role)
<tr class="hover:bg-slate-50/80 dark:hover:bg-slate-800/50 transition-colors group">
<td class="px-6 py-5">
<div class="flex items-center gap-3">
<span class="text-sm font-bold text-slate-700 dark:text-slate-200">{{ $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.5" 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-5">
@if($role->is_system)
<span class="px-2.5 py-1 text-[11px] font-black uppercase tracking-tight bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400 rounded-full">
{{ __('System') }}
</span>
@else
<span class="px-2.5 py-1 text-[11px] font-black uppercase tracking-tight bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 rounded-full">
{{ __('Custom') }}
</span>
@endif
</td>
<td class="px-6 py-5 text-center">
<span class="text-sm font-black text-slate-600 dark:text-slate-400">{{ $role->users_count }}</span>
</td>
<td class="px-6 py-5 text-right">
<div class="flex items-center justify-end gap-2">
@if(!$role->is_system)
<button @click="openModal(true, '{{ $role->id }}', '{{ $role->name }}')" 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 xmlns="http://www.w3.org/2000/svg" class="w-4.5 h-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
</button>
<form action="{{ route('admin.permission.roles.destroy', $role->id) }}" method="POST" @submit.prevent="if(confirm('{{ __('Are you sure you want to delete this role?') }}')) $el.submit()" class="inline">
@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.5 h-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>
@else
<span class="px-3 py-1 text-[11px] font-black uppercase tracking-widest text-slate-400 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200/50 dark:border-slate-700/50 italic">{{ __('Protected') }}</span>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="4" 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">
{{ $roles->links('vendor.pagination.luxury') }}
</div>
</div>
<!-- Modal -->
<template x-if="showModal">
<div class="fixed inset-0 z-[60] flex items-center justify-center p-4">
<div @click="showModal = false" class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"></div>
<div class="relative w-full max-w-lg bg-white dark:bg-slate-900 rounded-2xl shadow-2xl overflow-hidden animate-luxury-in">
<div class="flex items-center justify-between p-6 border-b border-slate-100 dark:border-slate-800">
<h3 class="text-xl font-black text-slate-800 dark:text-white" x-text="modalTitle"></h3>
<button @click="showModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<form :action="isEdit ? '{{ route('admin.permission.roles') }}/' + roleId : '{{ route('admin.permission.roles.store') }}'" method="POST" class="p-6 space-y-6">
@csrf
<template x-if="isEdit"><input type="hidden" name="_method" value="PUT"></template>
<div class="space-y-2">
<label class="text-sm font-black text-slate-700 dark:text-slate-300">{{ __('Role Name') }}</label>
<input type="text" name="name" x-model="roleName" required class="luxury-input w-full" :placeholder="'{{ __('Enter role name') }}'">
</div>
<div class="flex items-center justify-end gap-3 mt-8">
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-6 py-2.5 rounded-xl font-bold">{{ __('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-8 py-2.5 rounded-xl font-bold">{{ __('Save Changes') }}</button>
</div>
</form>
</div>
</div>
</template>
</div>
@endsection

View File

@@ -52,113 +52,136 @@
$links[] = $foundModule;
}
// 2. 處理具體頁面
// 2. 處理中間層級與具體頁面
$segments = explode('.', $routeName);
$lastSegment = end($segments);
// 嘗試翻譯最後一個片段作為頁面名稱
$pageLabel = match($lastSegment) {
'edit' => __('Profile'), // 專門處理 profile.edit
'index' => null, // 通常 index 代表列表,如果在大模組下則不重複顯示
'logs' => __('Machine Logs'),
'permissions' => __('Machine Permissions'),
'utilization' => __('Utilization Rate'),
'expiry' => __('Expiry Management'),
'maintenance' => __('Maintenance Records'),
'ui-elements' => __('UI Elements'),
'helper' => __('Helper'),
'questionnaire' => __('Questionnaire'),
'games' => __('Games'),
'timer' => __('Timer'),
'personal' => __('Warehouse List (Individual)'),
'stock-management' => __('Stock Management'),
'transfers' => __('Transfers'),
'purchases' => __('Purchases'),
'replenishments' => __('Replenishments'),
'replenishment-records' => __('Replenishment Records'),
'machine-stock' => __('Machine Stock'),
'staff-stock' => __('Staff Stock'),
'returns' => __('Returns'),
'pickup-codes' => __('Pickup Codes'),
'orders' => __('Orders'),
'promotions' => __('Promotions'),
'pass-codes' => __('Pass Codes'),
'store-gifts' => __('Store Gifts'),
'change-stock' => __('Change Stock'),
'machine-reports' => __('Machine Reports'),
'product-reports' => __('Product Reports'),
'survey-analysis' => __('Survey Analysis'),
'products' => __('Product Management'),
'advertisements' => __('Advertisement Management'),
'admin-products' => __('Admin Sellable Products'),
'accounts' => __('Account Management'),
'sub-accounts' => __('Sub Accounts'),
'sub-account-roles' => __('Sub Account Roles'),
'points' => __('Point Settings'),
'badges' => __('Badge Settings'),
'restart' => __('Machine Restart'),
'restart-card-reader' => __('Card Reader Restart'),
'checkout' => __('Remote Checkout'),
'lock' => __('Remote Lock'),
'change' => __('Remote Change'),
'dispense' => __('Remote Dispense'),
'official-account' => __('Line Official Account'),
'coupons' => __('Line Coupons'),
'stores' => __('Store Management'),
'time-slots' => __('Time Slots'),
'venues' => __('Venue Management'),
'reservations' => __('Reservations'),
'clear-stock' => __('Clear Stock'),
'apk-versions' => __('APK Versions'),
'discord-notifications' => __('Discord Notifications'),
'app-features' => __('APP Features'),
'roles' => __('Roles'),
'others' => __('Others'),
'ai-prediction' => __('AI Prediction'),
'create' => __('Create'),
'show' => __('Show'),
'members' => __('Member List'), // 處理 admin.members.index 這種情況
default => null,
};
// 如果匹配不到,嘗試處理一些特殊的 index 標籤
if (!$pageLabel && $lastSegment === 'index') {
$pageLabel = match($segments[count($segments)-2] ?? '') {
// 如果路由有四段以上 (如 admin.permission.companies.index),則處理中間一段
if (count($segments) >= 4) {
$midSegment = $segments[2];
$midLabel = match($midSegment) {
'companies' => __('Customer Management'),
'members' => __('Member List'),
'machines' => __('Machine List'),
'warehouses' => __('Warehouse List'),
'sales' => __('Sales Records'),
default => null,
};
if ($midLabel) {
$links[] = [
'label' => $midLabel,
'url' => '#', // 如果有需要可以導向 index 路由
'active' => $lastSegment === 'index'
];
}
}
if ($pageLabel) {
$links[] = [
'label' => $pageLabel,
'url' => route($routeName),
'active' => true
];
// 3. 處理最後一個動作/頁面
if ($lastSegment !== 'index') {
$pageLabel = match($lastSegment) {
'edit' => __('Edit'),
'create' => __('Create'),
'show' => __('Detail'),
'logs' => __('Machine Logs'),
'permissions' => __('Machine Permissions'),
'utilization' => __('Utilization Rate'),
'expiry' => __('Expiry Management'),
'maintenance' => __('Maintenance Records'),
'ui-elements' => __('UI Elements'),
'helper' => __('Helper'),
'questionnaire' => __('Questionnaire'),
'games' => __('Games'),
'timer' => __('Timer'),
'personal' => __('Warehouse List (Individual)'),
'stock-management' => __('Stock Management'),
'transfers' => __('Transfers'),
'purchases' => __('Purchases'),
'replenishments' => __('Replenishments'),
'replenishment-records' => __('Replenishment Records'),
'machine-stock' => __('Machine Stock'),
'staff-stock' => __('Staff Stock'),
'returns' => __('Returns'),
'pickup-codes' => __('Pickup Codes'),
'orders' => __('Orders'),
'promotions' => __('Promotions'),
'pass-codes' => __('Pass Codes'),
'store-gifts' => __('Store Gifts'),
'change-stock' => __('Change Stock'),
'machine-reports' => __('Machine Reports'),
'product-reports' => __('Product Reports'),
'survey-analysis' => __('Survey Analysis'),
'products' => __('Product Management'),
'advertisements' => __('Advertisement Management'),
'admin-products' => __('Admin Sellable Products'),
'accounts' => __('Account Management'),
'sub-accounts' => __('Sub Accounts'),
'sub-account-roles' => __('Sub Account Roles'),
'points' => __('Point Settings'),
'badges' => __('Badge Settings'),
'restart' => __('Machine Restart'),
'restart-card-reader' => __('Card Reader Restart'),
'checkout' => __('Remote Checkout'),
'lock' => __('Remote Lock'),
'change' => __('Remote Change'),
'dispense' => __('Remote Dispense'),
'official-account' => __('Line Official Account'),
'coupons' => __('Line Coupons'),
'stores' => __('Store Management'),
'time-slots' => __('Time Slots'),
'venues' => __('Venue Management'),
'reservations' => __('Reservations'),
'clear-stock' => __('Clear Stock'),
'apk-versions' => __('APK Versions'),
'discord-notifications' => __('Discord Notifications'),
'app-features' => __('APP Features'),
'roles' => __('Roles'),
'others' => __('Others'),
'ai-prediction' => __('AI Prediction'),
'data-config' => __('Data Configuration Permissions'),
'sales' => __('Sales Permissions'),
'machines' => __('Machine Management Permissions'),
'warehouses' => __('Warehouse Permissions'),
'analysis' => __('Analysis Permissions'),
'audit' => __('Audit Permissions'),
'remote' => __('Remote Permissions'),
'line' => __('Line Permissions'),
default => null,
};
if ($pageLabel) {
$links[] = [
'label' => $pageLabel,
'url' => route($routeName),
'active' => true
];
}
}
// 確保最後一個 link 是 active 的
if (!empty($links)) {
$links[count($links) - 1]['active'] = true;
// 如果倒數第二個也是同個頁面(例如 Dashboard > Dashboard),則移除重複
// 檢查是否有相鄰重複的 Label (例如 Dashboard > Dashboard)
if (count($links) > 1 && $links[count($links)-1]['label'] === $links[count($links)-2]['label']) {
array_pop($links);
$links[count($links)-1]['active'] = true;
}
}
}
}
@endphp
<nav {{ $attributes->merge(['class' => 'flex', 'aria-label' => 'Breadcrumb']) }}>
<ol class="flex items-center whitespace-nowrap min-w-0 w-full">
@foreach($links as $link)
<li class="flex items-center text-sm {{ $link['active'] ? 'font-semibold text-slate-800 dark:text-slate-200' : 'text-slate-500 dark:text-slate-400' }} {{ !$loop->last ? 'shrink-0' : 'truncate' }}">
@if(!$link['active'] && $link['url'] !== '#')
<a class="hover:text-cyan-600 transition-colors" href="{{ $link['url'] }}">
@php
$isActive = $link['active'] ?? false;
$url = $link['url'] ?? '#';
@endphp
<li class="flex items-center text-sm {{ $isActive ? 'font-semibold text-slate-800 dark:text-slate-200' : 'text-slate-500 dark:text-slate-400' }} {{ !$loop->last ? 'shrink-0' : 'truncate' }}">
@if(!$isActive && $url !== '#')
<a class="hover:text-cyan-600 transition-colors" href="{{ $url }}">
{{ $link['label'] }}
</a>
@else

View File

@@ -84,19 +84,19 @@
<li x-data="{ open: localStorage.getItem('menu_warehouses') === 'true' || {{ request()->routeIs('admin.warehouses.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_warehouses', open)" class="luxury-nav-item w-full text-start group">
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.warehouses.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>
倉庫管理
{{ __('Warehouse Management') }}
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse>
<ul class="luxury-submenu">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.index') }}">倉庫列表()</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.personal') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.personal') }}">倉庫列表()</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.stock-management') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.stock-management') }}">庫存管理單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.transfers') }}">調撥單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.purchases') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.purchases') }}">採購單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishments') }}">機台補貨單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishment-records') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishment-records') }}">機台補貨紀錄</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.machine-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.machine-stock') }}">機台庫存</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.index') }}">{{ __('Warehouse List (All)') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.personal') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.personal') }}">{{ __('Warehouse List (Individual)') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.stock-management') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.stock-management') }}">{{ __('Stock Management') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.transfers') }}">{{ __('Transfers') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.purchases') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.purchases') }}">{{ __('Purchases') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishments') }}">{{ __('Replenishments') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishment-records') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishment-records') }}">{{ __('Replenishment Records') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.machine-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.machine-stock') }}">{{ __('Machine Stock') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.staff-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.staff-stock') }}">{{ __('Staff Stock') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.returns') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.returns') }}">{{ __('Returns') }}</a></li>
</ul>
@@ -167,7 +167,6 @@
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.products') }}">{{ __('Product Management') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.advertisements') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.advertisements') }}">{{ __('Advertisement Management') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.admin-products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.admin-products') }}">{{ __('Admin Sellable Products') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.accounts') }}">{{ __('Account Management') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-accounts') }}">{{ __('Sub Accounts') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-account-roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-account-roles') }}">{{ __('Sub Account Roles') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.points') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.points') }}">{{ __('Point Settings') }}</a></li>
@@ -236,6 +235,7 @@
</div>
</li>
@role('super-admin')
{{-- 14. 特殊權限 --}}
<li x-data="{ open: localStorage.getItem('menu_special_permission') === 'true' || {{ request()->routeIs('admin.special-permission.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_special_permission', open)" class="luxury-nav-item w-full text-start group">
@@ -251,7 +251,9 @@
</ul>
</div>
</li>
@endrole
@role('super-admin')
{{-- 15. 權限設定 --}}
<li x-data="{ open: localStorage.getItem('menu_permissions') === 'true' || {{ request()->routeIs('admin.permission.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_permissions', open)" class="luxury-nav-item w-full text-start group">
@@ -261,6 +263,9 @@
</button>
<div x-show="open" x-collapse>
<ul class="luxury-submenu">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.companies.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.companies.index') }}">{{ __('Customer Management') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.accounts') }}">{{ __('Account Management') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.roles') }}">{{ __('Roles') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.app-features') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.app-features') }}">{{ __('APP Features') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.data-config') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.data-config') }}">{{ __('Data Configuration') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.sales') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.sales') }}">{{ __('Sales') }}</a></li>
@@ -270,9 +275,9 @@
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.audit') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.audit') }}">{{ __('Audit Management') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.remote') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.remote') }}">{{ __('Remote Management') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.line') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.members') }}">{{ __('Line Management') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.roles') }}">{{ __('Roles') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.others') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.others') }}">{{ __('Others') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.ai-prediction') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.ai-prediction') }}">{{ __('AI Prediction') }}</a></li>
</ul>
</div>
</li>
@endrole

View File

@@ -0,0 +1,59 @@
@if ($paginator->hasPages())
<div class="flex flex-col sm:flex-row items-center justify-between w-full gap-4">
{{-- Total Items Info --}}
<div class="order-2 sm:order-1">
<p class="text-[10px] sm:text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest text-center sm:text-left">
<span class="hidden xs:inline">{{ __('Showing') }}</span>
<span class="text-slate-600 dark:text-slate-300">{{ $paginator->firstItem() }}</span>
{{ __('to') }}
<span class="text-slate-600 dark:text-slate-300">{{ $paginator->lastItem() }}</span>
<span class="hidden xs:inline">{{ __('of') }}</span>
<span class="inline xs:hidden">/</span>
<span class="text-slate-600 dark:text-slate-300">{{ $paginator->total() }}</span>
{{ __('items') }}
</p>
</div>
<div class="order-1 sm:order-2 flex items-center gap-x-1.5 sm:gap-x-2">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<span class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-slate-50 dark:bg-slate-800 text-slate-300 dark:text-slate-600 border border-slate-100 dark:border-slate-800 cursor-not-allowed">
<svg class="size-3.5 sm:size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"/></svg>
<span class="hidden xxs:inline">{{ __('Previous') }}</span>
</span>
@else
<a href="{{ $paginator->previousPageUrl() }}" class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all shadow-sm group">
<svg class="size-3.5 sm:size-4 text-cyan-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"/></svg>
<span class="hidden xxs:inline">{{ __('Previous') }}</span>
</a>
@endif
{{-- Unified Quick Jump Selection (Desktop & Mobile) --}}
<div class="relative group">
<select onchange="window.location.href = this.value" class="h-9 pl-4 pr-10 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-[11px] sm:text-xs font-black text-slate-600 dark:text-slate-300 appearance-none focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 outline-none transition-all cursor-pointer shadow-sm hover:border-slate-300 dark:hover:border-slate-600">
@for ($i = 1; $i <= $paginator->lastPage(); $i++)
<option value="{{ $paginator->url($i) }}" {{ $i == $paginator->currentPage() ? 'selected' : '' }}>
{{ $i }} / {{ $paginator->lastPage() }}
</option>
@endfor
</select>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none opacity-40 group-hover:opacity-100 transition-opacity">
<svg class="size-3.5 text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M19 9l-7 7-7-7"/></svg>
</div>
</div>
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}" class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all shadow-sm group">
<span class="hidden xxs:inline">{{ __('Next') }}</span>
<svg class="size-3.5 sm:size-4 text-cyan-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 5l7 7-7 7"/></svg>
</a>
@else
<span class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-slate-50 dark:bg-slate-800 text-slate-300 dark:text-slate-600 border border-slate-100 dark:border-slate-800 cursor-not-allowed">
<span class="hidden xxs:inline">{{ __('Next') }}</span>
<svg class="size-3.5 sm:size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 5l7 7-7 7"/></svg>
</span>
@endif
</div>
</div>
@endif