[FEAT] 優化帳號管理授權顯示邏輯與 UI 樣式一致性
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 59s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 59s
This commit is contained in:
@@ -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` 隔離邏輯正確套用
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
12
lang/en.json
12
lang/en.json
@@ -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..."
|
||||
}
|
||||
@@ -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.": "儲存時發生錯誤。"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
50
resources/views/components/searchable-select.blade.php
Normal file
50
resources/views/components/searchable-select.blade.php
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user