[FEAT] 重構機台日誌 UI 與增加多語系支援,並整合 IoT API 核心架構

- 機台日誌:對齊 Luxury UI 規範,實作整合式佈局與分頁組件。
- 多語系:完成機台日誌繁、英、日三語系翻譯與動態處理。
- UI 規範:更新 SKILL.md 定義「標準列表 Bible」。
- 後端:完善 TenantScoped 隔離邏輯,修復儀表板死循環與 User Model 缺失。
- IoT:擴展機台、會員 Model 並建立交易、商品、狀態等核心表結構。
- 基礎設施:設置台北時區與 Docker 環境變數同步。
This commit is contained in:
2026-03-16 17:29:15 +08:00
parent 1851e91c86
commit 3ce88ed342
54 changed files with 2015 additions and 227 deletions

View File

@@ -29,29 +29,32 @@
}
}">
<!-- Header -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
<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-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage administrative and tenant accounts') }}</p>
<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>
<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 -->
<!-- Accounts Content (Integrated Card) -->
<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">
<!-- 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-3 pl-12 pr-6 block w-full md:w-80 luxury-input" placeholder="{{ __('Search users...') }}">
<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>
@@ -63,42 +66,37 @@
@endforeach
</select>
@endif
</div>
<div class="flex items-center gap-3">
<!-- 移除了冗餘的 Filter 按鈕,下拉選單具備自動提交功能 -->
</div>
</form>
<div class="overflow-x-auto mt-8">
<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">{{ __('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 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/50">
<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-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">
<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-sm font-black">{{ substr($user->name, 0, 1) }}</span>
<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-[0.15em]">{{ $user->username }} @if($user->email) {{ $user->email }} @endif</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>
@@ -107,37 +105,38 @@
@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>
<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-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">
<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-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-wider uppercase">
<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-[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">
<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_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">
<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('{{ addslashes(__('Are you sure you want to delete this account?')) }}')">
<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-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">
<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>
@@ -147,7 +146,10 @@
@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>
<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

View File

@@ -1,29 +1,26 @@
@extends('layouts.admin')
@section('header')
<div class="flex justify-between items-center">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('所有機台日誌') }}
</h2>
</div>
@endsection
@section('title', __('Machine Logs'))
@section('content')
<div class="py-12">
<div class="sm:px-6 lg:px-8 space-y-6">
<!-- 篩選器 -->
<div class="luxury-card rounded-2xl p-6 animate-luxury-in">
<div class="flex items-center gap-x-2 mb-4">
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400">
條件篩選
</p>
</div>
<form method="GET" action="{{ route('admin.machines.logs') }}" class="flex flex-wrap gap-4 items-end">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">機台</label>
<select name="machine_id" class="block w-48 rounded-md border-slate-300 shadow-sm focus:border-cyan-500 focus:ring focus:ring-cyan-500/20 text-sm dark:bg-slate-800 dark:border-slate-700 dark:text-white dark:focus:border-cyan-500">
<option value="">全部機台</option>
<div class="space-y-10 pb-20">
<!-- 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">{{ __('Machine Logs') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Monitor events and system activity across your vending fleet.') }}</p>
</div>
</div>
<!-- Machine Logs Content (Integrated Card - Same as Roles) -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<!-- Toolbar (Integrated Filters) -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
<form method="GET" action="{{ route('admin.machines.logs') }}" class="flex flex-wrap items-center gap-4 group">
<div class="space-y-1">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{{ __('Machine') }}</label>
<select name="machine_id" class="luxury-select text-xs h-9 py-0" onchange="this.form.submit()">
<option value="">{{ __('All Machines') }}</option>
@foreach($machines as $machine)
<option value="{{ $machine->id }}" {{ request('machine_id') == $machine->id ? 'selected' : '' }}>
{{ $machine->name }}
@@ -31,97 +28,102 @@
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">層級</label>
<select name="level" class="block w-32 rounded-md border-slate-300 shadow-sm focus:border-cyan-500 focus:ring focus:ring-cyan-500/20 text-sm dark:bg-slate-800 dark:border-slate-700 dark:text-white dark:focus:border-cyan-500">
<option value="">全部層級</option>
<option value="info" {{ request('level') == 'info' ? 'selected' : '' }}>Info</option>
<option value="warning" {{ request('level') == 'warning' ? 'selected' : '' }}>Warning</option>
<option value="error" {{ request('level') == 'error' ? 'selected' : '' }}>Error</option>
<div class="space-y-1">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{{ __('Level') }}</label>
<select name="level" class="luxury-select text-xs h-9 py-0" onchange="this.form.submit()">
<option value="">{{ __('All Levels') }}</option>
<option value="info" {{ request('level') == 'info' ? 'selected' : '' }}>{{ __('Info') }}</option>
<option value="warning" {{ request('level') == 'warning' ? 'selected' : '' }}>{{ __('Warning') }}</option>
<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="per_page" 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([20, 50, 100, 200] as $size)
<option value="{{ $size }}" {{ request('per_page', 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>
<span>篩選</span>
<div class="flex items-end gap-2 mt-5">
<button type="submit" class="p-2 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 transition-colors border border-slate-200 dark:border-slate-700">
<svg class="h-4 w-4" 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"/><path d="m21 21-4.3-4.3"/></svg>
</button>
<a href="{{ route('admin.machines.logs') }}" class="btn-luxury-ghost">
重設
<a href="{{ route('admin.machines.logs') }}" class="p-2 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 transition-colors border border-slate-200 dark:border-slate-700">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</a>
</div>
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
</form>
</div>
<!-- 日誌清單 -->
<div class="luxury-card rounded-2xl p-6 animate-luxury-in overflow-hidden" style="animation-delay: 100ms">
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-bold text-slate-800 dark:text-white">系統日誌清單</h2>
<span class="text-xs text-slate-400">所有時間為系統時區</span>
</div>
<div class="overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-[#0f172a]">
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700/50 font-mono text-xs">
<thead class="bg-slate-50 dark:bg-slate-800/80">
<tr>
<th class="px-4 py-3 text-left font-semibold text-slate-600 dark:text-slate-300">時間</th>
<th class="px-4 py-3 text-left font-semibold text-slate-600 dark:text-slate-300">機台</th>
<th class="px-4 py-3 text-left font-semibold text-slate-600 dark:text-slate-300">層級</th>
<th class="px-4 py-3 text-left font-semibold text-slate-600 dark:text-slate-300">訊息</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-700/50 bg-white dark:bg-transparent">
@forelse ($logs as $log)
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
<td class="px-4 py-3 text-slate-500 dark:text-slate-400 whitespace-nowrap">{{ $log->created_at->format('Y-m-d H:i:s') }}</td>
<td class="px-4 py-3 text-slate-700 dark:text-slate-300 whitespace-nowrap">
<a href="{{ route('admin.machines.show', $log->machine_id) }}" class="hover:text-cyan-600 dark:hover:text-cyan-400 underline decoration-slate-300 dark:decoration-slate-600 underline-offset-2 transition-colors">
{{ $log->machine->name ?? '未知機台' }}
</a>
</td>
<td class="px-4 py-3 whitespace-nowrap">
@php
$levelClasses = [
'info' => 'text-cyan-600 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-500/20 border-cyan-200 dark:border-cyan-500/30',
'warning' => 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/20 border-amber-200 dark:border-amber-500/30 font-semibold',
'error' => 'text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-500/20 border-rose-200 dark:border-rose-500/30 font-bold',
];
@endphp
<span class="px-2 py-0.5 rounded border {{ $levelClasses[$log->level] ?? 'text-slate-500 bg-slate-100 border-slate-200 dark:text-slate-300 dark:bg-slate-800 dark:border-slate-700' }}">
{{ strtoupper($log->level) }}
<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">{{ __('Timestamp') }}</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">{{ __('Machine') }}</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">{{ __('Level') }}</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">{{ __('Message Content') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse ($logs as $log)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6 transition-colors">
<div class="text-[13px] font-bold font-display tracking-widest text-slate-600 dark:text-slate-300">
{{ $log->created_at->format('Y-m-d') }}
</div>
<div class="text-[11px] font-bold text-slate-400 dark:text-slate-500 tracking-wider mt-0.5 uppercase">
{{ $log->created_at->format('H:i:s') }}
</div>
</td>
<td class="px-6 py-6 transition-colors">
<a href="{{ route('admin.machines.show', $log->machine_id) }}" class="inline-flex items-center gap-2 group/link">
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover/link:text-cyan-500 transition-colors">
{{ $log->machine->name ?? __('Unknown') }}
</span>
</td>
<td class="px-4 py-3 text-slate-700 dark:text-slate-200 max-w-xl break-words">
<svg class="w-3.5 h-3.5 text-slate-300 dark:text-slate-600 opacity-0 group-hover/link:opacity-100 transition-all -translate-x-2 group-hover/link:translate-x-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg>
</a>
</td>
<td class="px-6 py-6 transition-colors">
@php
$badgeStyles = [
'info' => 'bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border-cyan-500/20',
'warning' => 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20',
'error' => 'bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-500/20',
];
$currentStyle = $badgeStyles[$log->level] ?? 'bg-slate-500/10 text-slate-600 border-slate-500/20';
@endphp
<span class="inline-flex items-center px-3 py-1 rounded-lg border text-[11px] font-black uppercase tracking-wider {{ $currentStyle }}">
<span class="w-1.5 h-1.5 rounded-full bg-current mr-2 animate-pulse"></span>
{{ __(ucfirst($log->level)) }}
</span>
</td>
<td class="px-6 py-6 transition-colors">
<p class="text-[14px] font-medium text-slate-700 dark:text-slate-200 leading-relaxed max-w-xl">
{{ $log->message }}
@if($log->context)
<div class="text-[10px] text-slate-500 dark:text-slate-400 mt-2 max-h-24 overflow-y-auto bg-slate-100 dark:bg-[#0f172a] p-2 rounded-lg border border-slate-200 dark:border-slate-800/50 shadow-inner">
{{ json_encode($log->context, JSON_UNESCAPED_UNICODE) }}
</div>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-4 py-12 text-center text-slate-500 dark:text-slate-400 italic">暫無相關日誌</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($logs->hasPages())
<div class="mt-6">
{{ $logs->links('vendor.pagination.luxury') }}
</div>
@endif
</p>
@if($log->context)
<div class="mt-3 p-4 rounded-xl bg-slate-50/50 dark:bg-[#0f172a]/50 border border-slate-100 dark:border-slate-800/50 group-hover:bg-white dark:group-hover:bg-[#0f172a] transition-colors">
<pre class="text-[10px] font-bold text-slate-400 dark:text-slate-500 whitespace-pre-wrap break-all">{{ json_encode($log->context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) }}</pre>
</div>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-6 py-24 text-center">
<div class="flex flex-col items-center">
<div class="p-4 rounded-full bg-slate-50 dark:bg-slate-800/50 mb-4 border border-slate-100 dark:border-slate-800/50">
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 7v10c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V7c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2z"/><path d="M12 11l4-4"/><path d="M8 15l4-4"/></svg>
</div>
<span class="text-sm font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('No matching logs found') }}</span>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
{{ $logs->links('vendor.pagination.luxury') }}
</div>
</div>
</div>
@endsection
@endsection

View File

@@ -29,9 +29,10 @@
</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">
<!-- Roles Content (Integrated Card) -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<!-- Toolbar -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
<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="2" stroke-linecap="round" stroke-linejoin="round">
@@ -43,30 +44,27 @@
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
</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">
<table class="w-full text-left border-separate border-spacing-y-0">
<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">{{ __('Permissions') }}</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 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">{{ __('Role Name') }}</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">{{ __('Type') }}</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">{{ __('Permissions') }}</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">{{ __('Users') }}</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">
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@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">
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-xl flex items-center justify-center bg-slate-50 dark:bg-slate-800 text-slate-400 group-hover:bg-cyan-500/10 group-hover:text-cyan-500 transition-all duration-300">
<div class="w-10 h-10 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500/10 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-all duration-500">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
</div>
<span class="text-sm font-bold text-slate-700 dark:text-slate-200">{{ $role->name }}</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">{{ $role->name }}</span>
@if($role->is_system)
<span class="p-1.5 bg-cyan-50 dark:bg-cyan-900/30 text-cyan-600 dark:text-cyan-400 rounded-lg tooltip" title="{{ __('System Role') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
@@ -74,42 +72,42 @@
@endif
</div>
</td>
<td class="px-6 py-5">
<td class="px-6 py-6">
@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">
<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">
{{ __('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">
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-wider uppercase">
{{ __('Custom') }}
</span>
@endif
</td>
<td class="px-6 py-5">
<td class="px-6 py-6">
<div class="flex flex-wrap gap-1 max-w-xs">
@forelse($role->permissions->take(5) as $permission)
<span class="px-2 py-0.5 text-[10px] bg-slate-100 dark:bg-slate-800 text-slate-500 rounded uppercase font-bold">{{ __(str_replace('menu.', '', $permission->name)) }}</span>
@forelse($role->permissions->take(6) as $permission)
<span class="px-2 py-0.5 text-[10px] bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 rounded border border-slate-200 dark:border-slate-700 uppercase font-bold tracking-tight">{{ __(str_replace('menu.', '', $permission->name)) }}</span>
@empty
<span class="text-xs text-slate-400 italic">{{ __('No permissions') }}</span>
<span class="text-[11px] font-bold text-slate-400 italic tracking-tight">{{ __('No permissions') }}</span>
@endforelse
@if($role->permissions->count() > 5)
<span class="px-2 py-0.5 text-[10px] bg-slate-100 dark:bg-slate-800 text-slate-400 rounded uppercase font-bold">+{{ $role->permissions->count() - 5 }}</span>
@if($role->permissions->count() > 6)
<span class="px-2 py-0.5 text-[10px] bg-slate-100 dark:bg-slate-800 text-slate-400 rounded border border-slate-200 dark:border-slate-700 uppercase font-bold tracking-tight">+{{ $role->permissions->count() - 6 }}</span>
@endif
</div>
</td>
<td class="px-6 py-5 text-center">
<td class="px-6 py-6 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 text-slate-400">
<button @click="openModal(true, '{{ $role->id }}', '{{ $role->name }}', {{ $role->permissions->pluck('name') }})" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 hover:text-cyan-500 hover:bg-cyan-500/10 transition-all border border-transparent hover:border-cyan-500/20 shadow-sm tooltip" title="{{ __('Edit') }}">
<td class="px-6 py-6 text-right">
<div class="flex items-center justify-end gap-2">
<button @click="openModal(true, '{{ $role->id }}', '{{ $role->name }}', {{ $role->permissions->pluck('name') }})" 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 tooltip" title="{{ __('Edit') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
</button>
@if(!$role->is_system)
<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 text-slate-400">
@csrf
@method('DELETE')
<button type="submit" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 hover:text-rose-500 hover:bg-rose-500/10 transition-all border border-transparent hover:border-rose-500/20 shadow-sm tooltip" title="{{ __('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 tooltip" title="{{ __('Delete') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
</button>
</form>

View File

@@ -80,7 +80,7 @@
// 3. 處理最後一個動作/頁面
if ($lastSegment !== 'index') {
$pageLabel = match($lastSegment) {
'edit' => __('Edit'),
'edit' => str_starts_with($routeName, 'profile') ? null : __('Edit'),
'create' => __('Create'),
'show' => __('Detail'),
'logs' => __('Machine Logs'),