[FEAT] 優化帳號管理授權顯示邏輯與 UI 樣式一致性
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 59s

This commit is contained in:
2026-03-23 17:16:26 +08:00
parent 72812f9b0b
commit 38770b080b
26 changed files with 1265 additions and 444 deletions

View File

@@ -15,7 +15,7 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
| 觸發詞 / 情境 | 對應 Skill | 路徑 |
|---|---|---|
| 機台通訊, IoT, 日誌上報, Log Ingestion, 異步隊列, Queue, Heartbeat, 心跳發報 | **IoT 通訊與高併發處理規範** | `.agents/skills/iot-communication/SKILL.md` |
| 介面, UI, 佈局, CSS, Tailwind, 奢華, 深色模式, Light Mode, Dark Mode, Blade, 樣式, 間距, 陰影, 動畫 | **極簡奢華風 UI 實作規範** | `.agents/skills/ui-minimal-luxury/SKILL.md` |
| 介面, UI, 設計, 佈局, CSS, Tailwind, 奢華, 深色模式, Light Mode, Dark Mode, Blade, 樣式, 間距, 陰影, 動畫, 畫面, 頁面 | **極簡奢華風 UI 實作規範** | `.agents/skills/ui-minimal-luxury/SKILL.md` |
| 查詢、撈資料、Query、Controller、下拉選單、Eloquent、N+1、`->get()`、select、交易、Transaction、Bulk、分頁、索引 | **資料庫與 ORM 最佳實踐規範** | `/home/mama/.gemini/antigravity/global_skills/database-best-practices/SKILL.md` |
| RBAC, 權限, 角色, 租戶, Tenant, Company, Access Control, 多租戶, 權限控管 | **多租戶與權限架構實作規範** | `.agents/rules/rbac-rules.md` |
@@ -41,4 +41,4 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
### 🔴 新增或修改 API 與 Controller 撈取資料庫邏輯時
必須讀取:
1. **database-best-practices** — 確認查詢優化、交易安全、批量寫入與索引規範
2. **rbac-rules** — 確保 `company_id` 隔離邏輯正確套用
2. **rbac-rules** — 確保 `company_id` 隔離邏輯正確套用

View File

@@ -178,6 +178,25 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
<option value="1">啟用</option>
<option value="0">禁用</option>
</select>
### 搜尋式下拉選單 (Searchable Select) - 【進階推薦】
- **組件**: `<x-searchable-select />`
- **適用場景**: 選項大於 10 筆或具備層級關聯的篩選器(如:所屬單位、機台編號)。
- **奢華特徵**:
- **動態旋轉箭頭**: 透過 `::after` 偽元素實作,選單展開時箭頭執行 `300ms` 的 180 度旋轉動畫。
- **即時過濾**: 輸入關鍵字即時隱藏不匹配項。
- **選取標示**: 選取的項目右側帶有青色 (`Cyan`) 的勾選小圖標。
- **全部選項修復 (Space Fix)**: 若用於篩選(如公司篩選),組件內部已實作「空格佔位符」機制。若選單中的「全部」選項在選取後消失,請確保該選項的值為單個空格 (`value=" "`)。這能繞過 Preline 對空標記的隱藏邏輯,並同步觸發 Laravel 的 `blank()` 判定。
```html
<x-searchable-select
name="company_id"
:options="$companies"
:selected="request('company_id')"
:placeholder="__('All Companies')"
onchange="this.form.submit()"
/>
```
```
## 8. 編輯與詳情頁規範 (Detail & Edit Views)

View File

@@ -45,7 +45,7 @@ class MachineSettingController extends AdminController
// 3. 基礎下拉資料 (用於新增/編輯機台的彈窗)
$models = MachineModel::select('id', 'name')->get();
$paymentConfigs = PaymentConfig::select('id', 'name')->get();
$companies = \App\Models\System\Company::select('id', 'name')->get();
$companies = \App\Models\System\Company::select('id', 'name', 'code')->get();
return view('admin.basic-settings.machines.index', compact(
'machines',
@@ -101,7 +101,7 @@ class MachineSettingController extends AdminController
{
$models = MachineModel::select('id', 'name')->get();
$paymentConfigs = PaymentConfig::select('id', 'name')->get();
$companies = \App\Models\System\Company::select('id', 'name')->get();
$companies = \App\Models\System\Company::select('id', 'name', 'code')->get();
return view('admin.basic-settings.machines.edit', compact('machine', 'models', 'paymentConfigs', 'companies'));
}
@@ -137,6 +137,13 @@ class MachineSettingController extends AdminController
'payment_config_id' => 'nullable|exists:payment_configs,id',
'location' => 'nullable|string|max:255',
]);
// 僅限系統管理員可修改公司
if (auth()->user()->isSystemAdmin()) {
$companyRule = ['company_id' => 'nullable|exists:companies,id'];
$companyData = $request->validate($companyRule);
$validated = array_merge($validated, $companyData);
}
Log::info('Machine Update Validated Data', ['data' => $validated]);
} catch (\Illuminate\Validation\ValidationException $e) {

View File

@@ -38,7 +38,7 @@ class PaymentConfigController extends AdminController
*/
public function create(): View
{
$companies = \App\Models\System\Company::select('id', 'name')->get();
$companies = \App\Models\System\Company::select('id', 'name', 'code')->get();
return view('admin.basic-settings.payment-configs.create', compact('companies'));
}

View File

@@ -67,12 +67,68 @@ class MachineController extends AdminController
return view('admin.machines.logs', compact('logs', 'machines'));
}
/**
* 機台權限設定 (開發中)
* AJAX: 取得特定帳號的機台分配狀態
*/
public function permissions(Request $request): View
public function getAccountMachines(\App\Models\System\User $user)
{
return view('admin.machines.index', ['machines' => Machine::paginate(1)]); // Placeholder
$currentUser = auth()->user();
// 安全檢查:只能操作自己公司的帳號(除非是系統管理員)
if (!$currentUser->isSystemAdmin() && $user->company_id !== $currentUser->company_id) {
return response()->json(['error' => 'Unauthorized'], 403);
}
// 取得該公司所有機台 (限定 company_id 以實作資料隔離)
$machines = Machine::where('company_id', $user->company_id)
->get(['id', 'name', 'serial_no']);
$assignedIds = $user->machines()->pluck('machines.id')->toArray();
return response()->json([
'user' => $user,
'machines' => $machines,
'assigned_ids' => $assignedIds
]);
}
/**
* AJAX: 儲存特定帳號的機台分配
*/
public function syncAccountMachines(Request $request, \App\Models\System\User $user)
{
$currentUser = auth()->user();
// 安全檢查
if (!$currentUser->isSystemAdmin() && $user->company_id !== $currentUser->company_id) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$request->validate([
'machine_ids' => 'nullable|array',
'machine_ids.*' => 'exists:machines,id'
]);
// 加固驗證:確保所有機台 ID 都屬於該使用者的公司
if ($request->has('machine_ids')) {
$machineIds = array_unique($request->machine_ids);
$validCount = Machine::where('company_id', $user->company_id)
->whereIn('id', $machineIds)
->count();
if ($validCount !== count($machineIds)) {
return response()->json(['error' => 'Invalid machine IDs provided.'], 422);
}
}
$user->machines()->sync($request->machine_ids ?? []);
return response()->json([
'success' => true,
'message' => __('Permissions updated successfully.'),
'assigned_machines' => $user->machines()->select('machines.id', 'machines.name', 'machines.serial_no')->get()
]);
}
/**

View File

@@ -248,7 +248,7 @@ class PermissionController extends Controller
// 帳號管理
public function accounts(Request $request)
{
$query = \App\Models\System\User::query()->with(['company', 'roles']);
$query = \App\Models\System\User::query()->with(['company', 'roles', 'machines']);
// 租戶隔離:如果不是系統管理員,則只看自己公司的成員
if (!auth()->user()->isSystemAdmin()) {

View File

@@ -12,6 +12,26 @@ class Machine extends Model
use HasFactory, TenantScoped;
use \Illuminate\Database\Eloquent\SoftDeletes;
protected static function booted()
{
// 權限隔離:一般帳號登入時只能看到自己被分配的機台
static::addGlobalScope('machine_access', function (\Illuminate\Database\Eloquent\Builder $builder) {
$user = auth()->user();
// 如果是在 Console、或是系統管理員、或是租戶的「管理員」角色則不限制 (可看該公司所有機台)
if (app()->runningInConsole() || !$user || $user->isSystemAdmin() || $user->hasRole('管理員') || $user->hasRole('super-admin')) {
return;
}
// 一般租戶帳號:限制只能看自己擁有的機台
$builder->whereExists(function ($query) use ($user) {
$query->select(\Illuminate\Support\Facades\DB::raw(1))
->from('machine_user')
->whereColumn('machine_user.machine_id', 'machines.id')
->where('machine_user.user_id', $user->id);
});
});
}
protected $fillable = [
'company_id',
'name',
@@ -101,4 +121,8 @@ class Machine extends Model
return $this->belongsTo(\App\Models\System\User::class, 'updater_id');
}
public function users()
{
return $this->belongsToMany(\App\Models\System\User::class);
}
}

View File

@@ -69,6 +69,14 @@ class User extends Authenticatable
return $this->belongsTo(Company::class);
}
/**
* Get the machines assigned to the user.
*/
public function machines()
{
return $this->belongsToMany(\App\Models\Machine\Machine::class);
}
/**
* Check if the user is a system administrator.
*/

View File

@@ -17,6 +17,7 @@ class MachineFactory extends Factory
'status' => fake()->randomElement(['online', 'offline', 'error']),
'temperature' => fake()->randomFloat(2, 2, 10),
'firmware_version' => 'v' . fake()->randomElement(['1.0.0', '1.1.2', '2.0.1']),
'serial_no' => 'SN-' . strtoupper(fake()->unique()->bothify('??###?')),
'last_heartbeat_at' => fake()->dateTimeBetween('-1 day', 'now'),
];
}

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('machine_user', function (Blueprint $table) {
$table->id();
$table->foreignId('machine_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamps();
$table->unique(['machine_id', 'user_id']); // Ensure uniqueness
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('machine_user');
}
};

View File

@@ -505,5 +505,15 @@
"Role Identification": "Role Identification",
"LEVEL TYPE": "LEVEL TYPE",
"Affiliated Unit": "Affiliated Unit",
"System Official": "System Official"
"System Official": "System Official",
"Full Access": "Full Access",
"System Default": "System Default",
"Authorized Machines": "Authorized Machines",
"Assign": "Assign",
"No machines available": "No machines available",
"Selected": "Selected",
"Failed to fetch machine data.": "Failed to fetch machine data.",
"Failed to save permissions.": "Failed to save permissions.",
"An error occurred while saving.": "An error occurred while saving.",
"Loading machines...": "Loading machines..."
}

View File

@@ -20,6 +20,17 @@
"admin": "管理員",
"Admin display name": "管理員顯示名稱",
"Admin Name": "管理員姓名",
"Account Name": "帳號姓名",
"Account": "帳號",
"Account:": "帳號:",
"Manage Account Access": "管理帳號存取",
"Assigned Machines": "授權機台",
"No machines assigned": "未分配機台",
"Available Machines": "可供分配的機台",
"Loading machines...": "正在載入機台...",
"No machines available in this company.": "此客戶目前沒有可供分配的機台。",
"Saving...": "儲存中...",
"Save Permissions": "儲存權限",
"Admin Sellable Products": "管理者可賣",
"Administrator": "管理員",
"Advertisement Management": "廣告管理",
@@ -230,7 +241,7 @@
"Machine Model Settings": "機台型號設定",
"Machine model updated successfully.": "機台型號已成功更新。",
"Machine Name": "機台名稱",
"Machine Permissions": "機台權限",
"Machine Permissions": "授權機台",
"Machine Reports": "機台報表",
"Machine Restart": "機台重啟",
"Machine Settings": "機台設定",
@@ -524,5 +535,26 @@
"Role Identification": "角色識別資訊",
"LEVEL TYPE": "層級類型",
"Affiliated Unit": "所屬單位",
"System Official": "系統層"
"System Official": "系統層",
"Other Permissions": "其他權限",
"General permissions not linked to a specific menu.": "未連結到特定選單的一般權限。",
"Assign Machines": "分配機台",
"data-config.sub-accounts": "子帳號管理",
"data-config.sub-account-roles": "子帳號角色",
"basic.machines": "機台設定",
"basic.payment-configs": "客戶金流設定",
"permissions.companies": "客戶管理",
"permissions.accounts": "帳號管理",
"permissions.roles": "角色權限管理",
"Edit Sub Account Role": "編輯子帳號角色",
"New Sub Account Role": "新增子帳號角色",
"Full Access": "全機台授權",
"System Default": "系統預設",
"Authorized Machines": "授權機台",
"Assign": "分配所屬機台",
"No machines available": "目前沒有可供分配的機台",
"Selected": "已選擇",
"Failed to fetch machine data.": "無法取得機台資料。",
"Failed to save permissions.": "無法儲存權限設定。",
"An error occurred while saving.": "儲存時發生錯誤。"
}

View File

@@ -265,4 +265,48 @@
background-repeat: no-repeat;
background-size: 1.25em 1.25em;
}
/* Preline Searchable Select Customizations */
.hs-select-toggle.luxury-select-toggle {
@apply luxury-input pr-10 cursor-pointer text-start flex items-center justify-between relative;
}
.hs-select-toggle.luxury-select-toggle::after {
content: "";
@apply absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 transition-transform duration-300 pointer-events-none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m6 9 6 6 6-6'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-size: contain;
}
/* 當選單開啟時旋轉箭頭 */
.hs-select.active .hs-select-toggle::after,
.hs-select-toggle.active::after,
.hs-select-toggle[aria-expanded="true"]::after {
@apply rotate-180;
}
.hs-select-menu {
@apply mt-2 max-h-72 p-2 space-y-0.5 z-50 bg-white border border-slate-200 rounded-2xl shadow-2xl overflow-y-auto;
@apply dark:bg-slate-900 dark:border-slate-800;
backdrop-filter: blur(12px);
}
.hs-select-option {
@apply py-2.5 px-4 w-full text-sm font-bold text-slate-700 cursor-pointer hover:bg-slate-50 rounded-xl transition-colors;
@apply dark:text-slate-200 dark:hover:bg-slate-800;
}
.hs-selected.hs-select-option {
@apply bg-cyan-50 text-cyan-600;
@apply dark:bg-cyan-500/10 dark:text-cyan-400;
}
.hs-select-search-wrapper {
@apply p-2 sticky top-0 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md z-10 mb-1 border-b border-slate-100 dark:border-slate-800;
}
.hs-select-search-input {
@apply luxury-input py-2 px-3 text-xs border-slate-100 dark:border-slate-800;
}
}

View File

@@ -62,7 +62,7 @@
<!-- Left: Basic info & Hardware -->
<div class="lg:col-span-2 space-y-6">
<!-- Basic Information -->
<div class="luxury-card rounded-3xl p-7 animate-luxury-in">
<div class="luxury-card rounded-3xl p-7 animate-luxury-in relative z-20">
<div class="flex items-center gap-3 mb-8">
<div class="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -87,17 +87,39 @@
</div>
<div>
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Machine Model') }}</label>
<select name="machine_model_id" class="luxury-select w-full" required>
<x-searchable-select
name="machine_model_id"
:selected="old('machine_model_id', $machine->machine_model_id)"
required
>
@foreach($models as $model)
<option value="{{ $model->id }}" {{ old('machine_model_id', $machine->machine_model_id) == $model->id ? 'selected' : '' }}>{{ $model->name }}</option>
@endforeach
</select>
</x-searchable-select>
</div>
@if(auth()->user()->isSystemAdmin())
<div>
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Company') }}</label>
<x-searchable-select
name="company_id"
:selected="old('company_id', $machine->company_id)"
:placeholder="__('No Company')"
>
@foreach($companies as $company)
<option value="{{ $company->id }}" {{ old('company_id', $machine->company_id) == $company->id ? 'selected' : '' }}
data-title="{{ $company->name }}{{ $company->code ? ' (' . $company->code . ')' : '' }}">
{{ $company->name }}{{ $company->code ? ' (' . $company->code . ')' : '' }}
</option>
@endforeach
</x-searchable-select>
@error('company_id') <p class="mt-1 text-xs text-rose-500 font-bold uppercase tracking-wider">{{ $message }}</p> @enderror
</div>
@endif
</div>
</div>
<!-- Operational Parameters -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 50ms">
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative z-10" style="animation-delay: 50ms">
<div class="flex items-center gap-3 mb-8">
<div class="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center text-amber-500">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -176,7 +198,7 @@
<!-- Right: System & Payment -->
<div class="space-y-8">
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 200ms">
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative z-20" style="animation-delay: 200ms">
<div class="flex items-center gap-3 mb-8">
<div class="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -189,26 +211,34 @@
<div class="space-y-6">
<div>
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Payment Config') }}</label>
<select name="payment_config_id" class="luxury-select w-full">
<option value="">{{ __('Not Used') }}</option>
<x-searchable-select
name="payment_config_id"
:selected="old('payment_config_id', $machine->payment_config_id)"
:placeholder="__('Not Used')"
:hasSearch="false"
>
@foreach($paymentConfigs as $config)
<option value="{{ $config->id }}" {{ $machine->payment_config_id == $config->id ? 'selected' : '' }}>{{ $config->name }}</option>
@endforeach
</select>
</x-searchable-select>
</div>
<div>
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Invoice Status') }}</label>
<select name="invoice_status" class="luxury-select w-full">
<x-searchable-select
name="invoice_status"
:selected="old('invoice_status', $machine->invoice_status)"
:hasSearch="false"
>
<option value="0" {{ $machine->invoice_status == 0 ? 'selected' : '' }}>{{ __('No Invoice') }}</option>
<option value="1" {{ $machine->invoice_status == 1 ? 'selected' : '' }}>{{ __('Default Donate') }}</option>
<option value="2" {{ $machine->invoice_status == 2 ? 'selected' : '' }}>{{ __('Default Not Donate') }}</option>
</select>
</x-searchable-select>
</div>
</div>
</div>
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 300ms">
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative z-10" style="animation-delay: 300ms">
<div class="flex items-center gap-3 mb-8">
<div class="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-500">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

@@ -391,16 +391,17 @@
<input type="text" name="serial_no" required class="luxury-input w-full"
placeholder="{{ __('Enter serial number') }}">
</div>
<div>
<div class="relative z-20">
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{
__('Owner') }}</label>
<select name="company_id" required class="luxury-select w-full">
<option value="">{{ __('Select Owner') }}</option>
<x-searchable-select name="company_id" required :placeholder="__('Select Owner')">
@foreach($companies as $company)
<option value="{{ $company->id }}">{{ $company->name }}</option>
<option value="{{ $company->id }}" data-title="{{ $company->name }}{{ $company->code ? ' (' . $company->code . ')' : '' }}">
{{ $company->name }}{{ $company->code ? ' (' . $company->code . ')' : '' }}
</option>
@endforeach
</select>
</x-searchable-select>
</div>
<div>
<label
@@ -409,16 +410,15 @@
<input type="text" name="location" class="luxury-input w-full"
placeholder="{{ __('Enter machine location') }}">
</div>
<div>
<div class="relative z-10">
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{
__('Model') }}</label>
<select name="machine_model_id" required class="luxury-select w-full">
<option value="">{{ __('Select Model') }}</option>
<x-searchable-select name="machine_model_id" required :placeholder="__('Select Model')">
@foreach($models as $model)
<option value="{{ $model->id }}">{{ $model->name }}</option>
@endforeach
</select>
</x-searchable-select>
</div>
<div>
<label

View File

@@ -43,7 +43,7 @@
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- Left Column: Primary Info -->
<div class="lg:col-span-12 space-y-6">
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative z-20">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Configuration Name') }} <span class="text-rose-500">*</span></label>
@@ -51,12 +51,16 @@
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Belongs To Company') }} <span class="text-rose-500">*</span></label>
<select name="company_id" required class="luxury-select w-full">
<option value="">{{ __('Select Company') }}</option>
<x-searchable-select
name="company_id"
required
:selected="old('company_id')"
:placeholder="__('Select Company')"
>
@foreach($companies as $company)
<option value="{{ $company->id }}">{{ $company->name }}</option>
<option value="{{ $company->id }}" data-title="{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}">{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}</option>
@endforeach
</select>
</x-searchable-select>
</div>
</div>
</div>

View File

@@ -44,7 +44,7 @@
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- Left Column: Primary Info -->
<div class="lg:col-span-12 space-y-6">
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative z-20">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Configuration Name') }} <span class="text-rose-500">*</span></label>
@@ -52,12 +52,16 @@
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Belongs To Company') }} <span class="text-rose-500">*</span></label>
<select name="company_id" required class="luxury-select w-full">
<option value="">{{ __('Select Company') }}</option>
<x-searchable-select
name="company_id"
required
:selected="old('company_id', $paymentConfig->company_id)"
:placeholder="__('Select Company')"
>
@foreach($companies as $company)
<option value="{{ $company->id }}" {{ $paymentConfig->company_id == $company->id ? 'selected' : '' }}>{{ $company->name }}</option>
<option value="{{ $company->id }}" data-title="{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}">{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}</option>
@endforeach
</select>
</x-searchable-select>
</div>
</div>
</div>

View File

@@ -236,7 +236,7 @@
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">
class="inline-block px-8 py-10 overflow-visible 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"
@@ -296,7 +296,7 @@
</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 x-show="!editing" class="space-y-6 pt-6 border-t border-slate-100 dark:border-slate-800 relative z-20">
<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">
@@ -324,21 +324,24 @@
<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 class="space-y-2">
<div class="space-y-2 relative z-[30]">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Initial Role') }}</label>
<select name="admin_role" class="luxury-select w-full">
<x-searchable-select
name="admin_role"
:hasSearch="false"
>
@foreach($template_roles as $role)
<option value="{{ $role->name }}" {{ $role->name == '客戶管理員角色模板' ? 'selected' : '' }}>
{{ $role->name }}
</option>
@endforeach
</select>
</x-searchable-select>
</div>
</div>
</div>
<!-- Contact Section -->
<div class="space-y-6 pt-6 border-t border-slate-100 dark:border-slate-800">
<div class="space-y-6 pt-6 border-t border-slate-100 dark:border-slate-800 relative z-10">
<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">{{
@@ -361,13 +364,25 @@
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<div class="space-y-2 relative z-[20]">
<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">
<x-searchable-select
name="status"
x-model="currentCompany.status"
:hasSearch="false"
x-init="$watch('currentCompany.status', (value) => {
$nextTick(() => {
const inst = HSSelect.getInstance($el);
if (inst) {
inst.setValue(String(value));
}
});
})"
>
<option value="1">{{ __('Active') }}</option>
<option value="0">{{ __('Disabled') }}</option>
</select>
</x-searchable-select>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{

File diff suppressed because it is too large Load Diff

View File

@@ -1,71 +0,0 @@
@extends('layouts.admin')
@section('content')
<div class="container mx-auto px-6 py-8">
<h3 class="text-gray-900 dark:text-gray-300 text-3xl font-medium">{{ __('Edit Machine') }}</h3>
<div class="mt-8">
<form action="{{ route('admin.machines.update', $machine) }}" method="POST"
class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
@csrf
@method('PUT')
<div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{ __('Machine
Name') }}</label>
<input type="text" name="name" id="name" value="{{ old('name', $machine->name) }}"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
required>
@error('name') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="location" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{
__('Location') }}</label>
<input type="text" name="location" id="location" value="{{ old('location', $machine->location) }}"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
@error('location') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{ __('Status')
}}</label>
<select name="status" id="status"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="offline" {{ $machine->status == 'offline' ? 'selected' : '' }}>{{ __('Offline') }}
</option>
<option value="online" {{ $machine->status == 'online' ? 'selected' : '' }}>{{ __('Connecting...')
}}</option>
<option value="error" {{ $machine->status == 'error' ? 'selected' : '' }}>{{ __('Error') }}</option>
</select>
@error('status') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="temperature" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{
__('Temperature') }} (°C)</label>
<input type="number" step="0.1" name="temperature" id="temperature"
value="{{ old('temperature', $machine->temperature) }}"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
@error('temperature') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="firmware_version" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{
__('Firmware Version') }}</label>
<input type="text" name="firmware_version" id="firmware_version"
value="{{ old('firmware_version', $machine->firmware_version) }}"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
@error('firmware_version') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div class="flex justify-end">
<a href="{{ route('admin.machines.index') }}"
class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded mr-2">{{
__('Cancel') }}</a>
<button type="submit" class="btn-luxury-primary">{{ __('Update') }}</button>
</div>
</form>
</div>
</div>
@endsection

View File

@@ -3,6 +3,16 @@
@section('content')
<div class="space-y-6" x-data="{
selectedPermissions: {{ json_encode($role->permissions->pluck('name')->toArray()) }},
@php
$availablePermissions = $all_permissions->flatten();
if (!$role->is_system) {
$availablePermissions = $availablePermissions->filter(function($p) {
return !str_starts_with($p->name, 'menu.basic') &&
!str_starts_with($p->name, 'menu.permissions');
});
}
@endphp
availableCount: {{ $availablePermissions->count() }},
activeCategory: '',
toggleCategory(category, permissions) {
const allSelected = permissions.every(p => this.selectedPermissions.includes(p));
@@ -20,6 +30,32 @@
const selectedCount = permissions.filter(p => this.selectedPermissions.includes(p)).length;
return selectedCount > 0 && selectedCount < permissions.length;
},
toggleParent(parentName, childrenNames) {
const isSelected = this.selectedPermissions.includes(parentName);
if (isSelected) {
// 取消父項目與所有子項目
this.selectedPermissions = this.selectedPermissions.filter(p => p !== parentName && !childrenNames.includes(p));
} else {
// 勾選父項目
if (!this.selectedPermissions.includes(parentName)) {
this.selectedPermissions.push(parentName);
}
// 勾選所有尚未勾選的子項目
childrenNames.forEach(p => {
if (!this.selectedPermissions.includes(p)) this.selectedPermissions.push(p);
});
}
},
isParentSelected(parentName, childrenNames) {
return this.selectedPermissions.includes(parentName) &&
childrenNames.every(p => this.selectedPermissions.includes(p));
},
isParentPartial(parentName, childrenNames) {
const hasParent = this.selectedPermissions.includes(parentName);
const selectedChildrenCount = childrenNames.filter(p => this.selectedPermissions.includes(p)).length;
return (hasParent && selectedChildrenCount < childrenNames.length) ||
(!hasParent && selectedChildrenCount > 0);
},
scrollTo(id) {
const el = document.getElementById(id);
if (el) {
@@ -140,7 +176,7 @@
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Total Selected') }}</span>
<div class="flex items-baseline gap-1">
<span class="text-3xl font-display font-black text-cyan-500" x-text="selectedPermissions.length">0</span>
<span class="text-xs font-bold text-slate-400">/ {{ $all_permissions->flatten()->count() }}</span>
<span class="text-xs font-bold text-slate-400">/ <span x-text="availableCount"></span></span>
</div>
</div>
<div class="w-12 h-12 rounded-2xl bg-emerald-500/5 flex items-center justify-center text-emerald-500">
@@ -156,6 +192,13 @@
<div class="flex-1 w-full space-y-12">
@foreach($all_permissions as $group => $permissions)
@php
// 如果非系統角色,過濾掉敏感權限
if (!$role->is_system && $group === 'menu') {
$permissions = $permissions->filter(function($p) {
return !str_starts_with($p->name, 'menu.basic') &&
!str_starts_with($p->name, 'menu.permissions');
});
}
$groupId = 'group-' . $group;
$groupPermissions = $permissions->pluck('name')->toArray();
@endphp
@@ -218,9 +261,11 @@
<div class="flex items-center pr-1">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="permissions[]" value="{{ $parent->name }}"
x-model="selectedPermissions"
:checked="selectedPermissions.includes('{{ $parent->name }}')"
@change="toggleParent('{{ $parent->name }}', {{ json_encode($children->pluck('name')->toArray()) }})"
class="sr-only peer">
<div class="w-11 h-6 bg-slate-200 dark:bg-slate-700 rounded-full peer peer-focus:outline-none peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500 transition-all duration-200 shadow-inner"></div>
<div class="w-11 h-6 bg-slate-200 dark:bg-slate-700 rounded-full peer peer-focus:outline-none peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500 transition-all duration-200 shadow-inner"
:class="isParentPartial('{{ $parent->name }}', {{ json_encode($children->pluck('name')->toArray()) }}) ? 'ring-2 ring-cyan-500/50' : ''"></div>
</label>
</div>
</div>
@@ -237,9 +282,9 @@
<div class="relative flex items-center flex-shrink-0">
<input type="checkbox"
name="permissions[]"
value="{{ $child->id }}"
class="w-4 h-4 rounded border-2 border-slate-300 dark:border-slate-700 text-cyan-500 focus:ring-cyan-500/20 transition-all cursor-pointer accent-cyan-500"
{{ $role->hasPermissionTo($child->name) ? 'checked' : '' }}>
value="{{ $child->name }}"
x-model="selectedPermissions"
class="w-4 h-4 rounded border-2 border-slate-300 dark:border-slate-700 text-cyan-500 focus:ring-cyan-500/20 transition-all cursor-pointer accent-cyan-500">
</div>
</label>
@endforeach

View File

@@ -66,13 +66,16 @@
@if(auth()->user()->isSystemAdmin())
<div class="relative">
<select name="company_id" onchange="this.form.submit()" class="py-2.5 pl-4 pr-10 block w-full md:w-60 luxury-input">
<option value="">{{ __('All Affiliations') }}</option>
<option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }}>{{ __('System Level') }}</option>
@foreach($companies as $company)
<option value="{{ $company->id }}" {{ request('company_id') == $company->id ? 'selected' : '' }}>{{ $company->name }}</option>
@endforeach
</select>
<x-searchable-select
name="company_id"
:options="$companies"
:selected="request('company_id')"
placeholder="{{ __('All Affiliations') }}"
class="w-full md:w-auto min-w-[280px]"
onchange="this.form.submit()"
>
<option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }} data-title="{{ __('System Level') }}">{{ __('System Level') }}</option>
</x-searchable-select>
</div>
@endif
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
@@ -108,11 +111,11 @@
</td>
<td class="px-6 py-6">
@if($role->is_system)
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-800 uppercase tracking-wider">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 uppercase tracking-widest">
{{ __('System Level') }}
</span>
@else
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 tracking-wider">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">
{{ $role->company->name ?? __('Company Level') }}
</span>
@endif

View File

@@ -0,0 +1,50 @@
@props([
'name' => null,
'options' => [],
'selected' => null,
'placeholder' => null,
'id' => null,
'hasSearch' => true,
])
@php
$id = $id ?? $name ?? 'select-' . uniqid();
$options = is_iterable($options) ? $options : [];
// Skill Standard: Use " " for empty/all options to bypass Preline hiding while staying 'blank'
$isEmptySelected = (is_null($selected) || (string)$selected === '' || (string)$selected === ' ');
$config = [
"hasSearch" => (bool)$hasSearch,
"searchPlaceholder" => $placeholder ?: __('Search...'),
"isHidePlaceholder" => false,
"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500",
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
"dropdownClasses" => "hs-select-menu w-full bg-white/95 dark:bg-slate-900/95 backdrop-blur-xl border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[100] animate-luxury-in",
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between transition-all duration-300",
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></span></div>'
];
@endphp
<div {{ $attributes->merge(['class' => 'relative w-full'])->only('class') }}>
<select name="{{ $name }}" id="{{ $id }}" data-hs-select='{!! json_encode($config) !!}' class="hidden" {{ $attributes->except(['options', 'selected', 'placeholder', 'id', 'name', 'class', 'hasSearch']) }}>
@if($placeholder)
<option value=" " {{ $isEmptySelected ? 'selected' : '' }} data-title="{{ $placeholder }}">
{{ $placeholder }}
</option>
@endif
{{ $slot }}
@foreach($options as $v => $l)
@php
$val = is_object($l) ? ($l->id ?? $l->value) : $v;
$text = is_object($l) ? ($l->name ?? $l->label ?? $l->title) : $l;
@endphp
<option value="{{ $val }}" {{ (string)$selected === (string)$val ? 'selected' : '' }} data-title="{{ $text }}">
{{ $text }}
</option>
@endforeach
</select>
</div>

View File

@@ -7,46 +7,46 @@
}
@endphp
<div class="fixed top-8 left-1/2 -translate-x-1/2 z-[99999] w-full max-w-sm px-4 space-y-3 pointer-events-none">
@if(session('success'))
<div x-data="{ show: true }"
x-show="show"
x-init="setTimeout(() => show = false, 3000)"
<div x-data="{
toasts: [],
add(message, type = 'success') {
const id = Date.now();
this.toasts.push({ id, message, type });
setTimeout(() => {
this.toasts = this.toasts.filter(t => t.id !== id);
}, type === 'success' ? 3000 : 5000);
}
}"
@toast.window="add($event.detail.message, $event.detail.type)"
x-init="
window.Alpine.store('toast', {
show(message, type = 'success') {
window.dispatchEvent(new CustomEvent('toast', { detail: { message, type } }));
}
});
@if(session('success')) add('{{ session('success') }}', 'success'); @endif
@if(session('error')) add('{{ session('error') }}', 'error'); @endif
@foreach($allErrors as $error) add('{{ addslashes($error) }}', 'error'); @endforeach
"
class="fixed top-8 left-1/2 -translate-x-1/2 z-[99999] w-full max-w-sm px-4 space-y-3 pointer-events-none">
<template x-for="toast in toasts" :key="toast.id">
<div x-show="true"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="opacity-0 transform -translate-y-4 scale-95"
x-transition:enter-end="opacity-100 transform translate-y-0 scale-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 transform translate-y-0 scale-100"
x-transition:leave-end="opacity-0 transform -translate-y-4 scale-95"
class="p-4 bg-white dark:bg-slate-900 border border-emerald-500/30 text-emerald-600 dark:text-emerald-400 rounded-2xl font-bold shadow-[0_20px_50px_rgba(16,185,129,0.15)] flex items-center gap-3 pointer-events-auto backdrop-blur-xl bg-opacity-90 dark:bg-opacity-90 animate-luxury-in">
<div class="size-8 bg-emerald-500/20 rounded-xl flex items-center justify-center flex-shrink-0 text-emerald-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg>
:class="toast.type === 'success'
? 'border-emerald-500/30 text-emerald-600 dark:text-emerald-400 shadow-[0_20px_50px_rgba(16,185,129,0.15)]'
: 'border-rose-500/30 text-rose-600 dark:text-rose-400 shadow-[0_20px_50px_rgba(244,63,94,0.15)]'"
class="p-4 bg-white dark:bg-slate-900 border rounded-2xl font-bold flex items-center gap-3 pointer-events-auto backdrop-blur-xl bg-opacity-90 dark:bg-opacity-90 animate-luxury-in">
<div :class="toast.type === 'success' ? 'bg-emerald-500/20 text-emerald-500' : 'bg-rose-500/20 text-rose-500'"
class="size-8 rounded-xl flex items-center justify-center flex-shrink-0">
<svg x-show="toast.type === 'success'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg>
<svg x-show="toast.type === 'error'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
</div>
<span>{{ session('success') }}</span>
<span x-text="toast.message"></span>
</div>
@endif
@if(session('error') || count($allErrors) > 0)
<div x-data="{ show: true }"
x-show="show"
x-init="setTimeout(() => show = false, 5000)"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="opacity-0 transform -translate-y-4 scale-95"
x-transition:enter-end="opacity-100 transform translate-y-0 scale-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 transform translate-y-0 scale-100"
x-transition:leave-end="opacity-0 transform -translate-y-4 scale-95"
class="p-4 bg-white dark:bg-slate-900 border border-rose-500/30 text-rose-600 dark:text-rose-400 rounded-2xl font-bold shadow-[0_20px_50px_rgba(244,63,94,0.15)] flex items-center gap-3 pointer-events-auto backdrop-blur-xl bg-opacity-90 dark:bg-opacity-90 animate-luxury-in">
<div class="size-8 bg-rose-500/20 rounded-xl flex items-center justify-center flex-shrink-0 text-rose-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
</div>
<span>
@if(session('error'))
{{ session('error') }}
@else
{{ $allErrors[0] ?? __('Please check the form for errors.') }}
@endif
</span>
</div>
@endif
</template>
</div>

View File

@@ -58,7 +58,7 @@
<ul class="luxury-submenu" data-sidebar-sub>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.logs') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.logs') }}">{{ __('Machine Logs') }}</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.machines.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.index') }}">{{ __('Machine List') }}</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.machines.permissions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.permissions') }}">{{ __('Machine Permissions') }}</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.machines.utilization') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.utilization') }}">{{ __('Utilization Rate') }}</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.machines.expiry') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.expiry') }}">{{ __('Expiry 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.machines.maintenance') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.maintenance') }}">{{ __('Maintenance Records') }}</a></li>

View File

@@ -39,7 +39,9 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
// 3. 機台管理
Route::prefix('machines')->name('machines.')->group(function () {
Route::get('/logs', [App\Http\Controllers\Admin\MachineController::class , 'logs'])->name('logs');
Route::get('/permissions', [App\Http\Controllers\Admin\MachineController::class , 'permissions'])->name('permissions');
// Route::get('/permissions', [App\Http\Controllers\Admin\MachineController::class , 'permissions'])->name('permissions'); // Merged into Sub-account Management
Route::get('/permissions/accounts/{user}', [App\Http\Controllers\Admin\MachineController::class, 'getAccountMachines'])->name('permissions.accounts.get');
Route::post('/permissions/accounts/{user}', [App\Http\Controllers\Admin\MachineController::class, 'syncAccountMachines'])->name('permissions.accounts.sync');
Route::get('/utilization', [App\Http\Controllers\Admin\MachineController::class , 'utilization'])->name('utilization');
Route::get('/expiry', [App\Http\Controllers\Admin\MachineController::class , 'expiry'])->name('expiry');
Route::get('/maintenance', [App\Http\Controllers\Admin\MachineController::class , 'maintenance'])->name('maintenance');