[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 | 路徑 |
|
| 觸發詞 / 情境 | 對應 Skill | 路徑 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 機台通訊, IoT, 日誌上報, Log Ingestion, 異步隊列, Queue, Heartbeat, 心跳發報 | **IoT 通訊與高併發處理規範** | `.agents/skills/iot-communication/SKILL.md` |
|
| 機台通訊, 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` |
|
| 查詢、撈資料、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` |
|
| RBAC, 權限, 角色, 租戶, Tenant, Company, Access Control, 多租戶, 權限控管 | **多租戶與權限架構實作規範** | `.agents/rules/rbac-rules.md` |
|
||||||
|
|
||||||
@@ -41,4 +41,4 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
|
|||||||
### 🔴 新增或修改 API 與 Controller 撈取資料庫邏輯時
|
### 🔴 新增或修改 API 與 Controller 撈取資料庫邏輯時
|
||||||
必須讀取:
|
必須讀取:
|
||||||
1. **database-best-practices** — 確認查詢優化、交易安全、批量寫入與索引規範
|
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="1">啟用</option>
|
||||||
<option value="0">禁用</option>
|
<option value="0">禁用</option>
|
||||||
</select>
|
</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)
|
## 8. 編輯與詳情頁規範 (Detail & Edit Views)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class MachineSettingController extends AdminController
|
|||||||
// 3. 基礎下拉資料 (用於新增/編輯機台的彈窗)
|
// 3. 基礎下拉資料 (用於新增/編輯機台的彈窗)
|
||||||
$models = MachineModel::select('id', 'name')->get();
|
$models = MachineModel::select('id', 'name')->get();
|
||||||
$paymentConfigs = PaymentConfig::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(
|
return view('admin.basic-settings.machines.index', compact(
|
||||||
'machines',
|
'machines',
|
||||||
@@ -101,7 +101,7 @@ class MachineSettingController extends AdminController
|
|||||||
{
|
{
|
||||||
$models = MachineModel::select('id', 'name')->get();
|
$models = MachineModel::select('id', 'name')->get();
|
||||||
$paymentConfigs = PaymentConfig::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'));
|
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',
|
'payment_config_id' => 'nullable|exists:payment_configs,id',
|
||||||
'location' => 'nullable|string|max:255',
|
'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]);
|
Log::info('Machine Update Validated Data', ['data' => $validated]);
|
||||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class PaymentConfigController extends AdminController
|
|||||||
*/
|
*/
|
||||||
public function create(): View
|
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'));
|
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'));
|
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)
|
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()) {
|
if (!auth()->user()->isSystemAdmin()) {
|
||||||
|
|||||||
@@ -12,6 +12,26 @@ class Machine extends Model
|
|||||||
use HasFactory, TenantScoped;
|
use HasFactory, TenantScoped;
|
||||||
use \Illuminate\Database\Eloquent\SoftDeletes;
|
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 = [
|
protected $fillable = [
|
||||||
'company_id',
|
'company_id',
|
||||||
'name',
|
'name',
|
||||||
@@ -101,4 +121,8 @@ class Machine extends Model
|
|||||||
return $this->belongsTo(\App\Models\System\User::class, 'updater_id');
|
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);
|
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.
|
* Check if the user is a system administrator.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class MachineFactory extends Factory
|
|||||||
'status' => fake()->randomElement(['online', 'offline', 'error']),
|
'status' => fake()->randomElement(['online', 'offline', 'error']),
|
||||||
'temperature' => fake()->randomFloat(2, 2, 10),
|
'temperature' => fake()->randomFloat(2, 2, 10),
|
||||||
'firmware_version' => 'v' . fake()->randomElement(['1.0.0', '1.1.2', '2.0.1']),
|
'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'),
|
'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",
|
"Role Identification": "Role Identification",
|
||||||
"LEVEL TYPE": "LEVEL TYPE",
|
"LEVEL TYPE": "LEVEL TYPE",
|
||||||
"Affiliated Unit": "Affiliated Unit",
|
"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": "管理員",
|
||||||
"Admin display name": "管理員顯示名稱",
|
"Admin display name": "管理員顯示名稱",
|
||||||
"Admin 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": "管理者可賣",
|
"Admin Sellable Products": "管理者可賣",
|
||||||
"Administrator": "管理員",
|
"Administrator": "管理員",
|
||||||
"Advertisement Management": "廣告管理",
|
"Advertisement Management": "廣告管理",
|
||||||
@@ -230,7 +241,7 @@
|
|||||||
"Machine Model Settings": "機台型號設定",
|
"Machine Model Settings": "機台型號設定",
|
||||||
"Machine model updated successfully.": "機台型號已成功更新。",
|
"Machine model updated successfully.": "機台型號已成功更新。",
|
||||||
"Machine Name": "機台名稱",
|
"Machine Name": "機台名稱",
|
||||||
"Machine Permissions": "機台權限",
|
"Machine Permissions": "授權機台",
|
||||||
"Machine Reports": "機台報表",
|
"Machine Reports": "機台報表",
|
||||||
"Machine Restart": "機台重啟",
|
"Machine Restart": "機台重啟",
|
||||||
"Machine Settings": "機台設定",
|
"Machine Settings": "機台設定",
|
||||||
@@ -524,5 +535,26 @@
|
|||||||
"Role Identification": "角色識別資訊",
|
"Role Identification": "角色識別資訊",
|
||||||
"LEVEL TYPE": "層級類型",
|
"LEVEL TYPE": "層級類型",
|
||||||
"Affiliated Unit": "所屬單位",
|
"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-repeat: no-repeat;
|
||||||
background-size: 1.25em 1.25em;
|
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 -->
|
<!-- Left: Basic info & Hardware -->
|
||||||
<div class="lg:col-span-2 space-y-6">
|
<div class="lg:col-span-2 space-y-6">
|
||||||
<!-- Basic Information -->
|
<!-- 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="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">
|
<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">
|
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -87,17 +87,39 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Machine Model') }}</label>
|
<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)
|
@foreach($models as $model)
|
||||||
<option value="{{ $model->id }}" {{ old('machine_model_id', $machine->machine_model_id) == $model->id ? 'selected' : '' }}>{{ $model->name }}</option>
|
<option value="{{ $model->id }}" {{ old('machine_model_id', $machine->machine_model_id) == $model->id ? 'selected' : '' }}>{{ $model->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</x-searchable-select>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Operational Parameters -->
|
<!-- 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="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">
|
<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">
|
<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 -->
|
<!-- Right: System & Payment -->
|
||||||
<div class="space-y-8">
|
<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="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">
|
<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">
|
<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 class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Payment Config') }}</label>
|
<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">
|
<x-searchable-select
|
||||||
<option value="">{{ __('Not Used') }}</option>
|
name="payment_config_id"
|
||||||
|
:selected="old('payment_config_id', $machine->payment_config_id)"
|
||||||
|
:placeholder="__('Not Used')"
|
||||||
|
:hasSearch="false"
|
||||||
|
>
|
||||||
@foreach($paymentConfigs as $config)
|
@foreach($paymentConfigs as $config)
|
||||||
<option value="{{ $config->id }}" {{ $machine->payment_config_id == $config->id ? 'selected' : '' }}>{{ $config->name }}</option>
|
<option value="{{ $config->id }}" {{ $machine->payment_config_id == $config->id ? 'selected' : '' }}>{{ $config->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Invoice Status') }}</label>
|
<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="0" {{ $machine->invoice_status == 0 ? 'selected' : '' }}>{{ __('No Invoice') }}</option>
|
||||||
<option value="1" {{ $machine->invoice_status == 1 ? 'selected' : '' }}>{{ __('Default Donate') }}</option>
|
<option value="1" {{ $machine->invoice_status == 1 ? 'selected' : '' }}>{{ __('Default Donate') }}</option>
|
||||||
<option value="2" {{ $machine->invoice_status == 2 ? 'selected' : '' }}>{{ __('Default Not Donate') }}</option>
|
<option value="2" {{ $machine->invoice_status == 2 ? 'selected' : '' }}>{{ __('Default Not Donate') }}</option>
|
||||||
</select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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="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">
|
<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">
|
<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"
|
<input type="text" name="serial_no" required class="luxury-input w-full"
|
||||||
placeholder="{{ __('Enter serial number') }}">
|
placeholder="{{ __('Enter serial number') }}">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="relative z-20">
|
||||||
<label
|
<label
|
||||||
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{
|
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{
|
||||||
__('Owner') }}</label>
|
__('Owner') }}</label>
|
||||||
<select name="company_id" required class="luxury-select w-full">
|
<x-searchable-select name="company_id" required :placeholder="__('Select Owner')">
|
||||||
<option value="">{{ __('Select Owner') }}</option>
|
|
||||||
@foreach($companies as $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
|
@endforeach
|
||||||
</select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
@@ -409,16 +410,15 @@
|
|||||||
<input type="text" name="location" class="luxury-input w-full"
|
<input type="text" name="location" class="luxury-input w-full"
|
||||||
placeholder="{{ __('Enter machine location') }}">
|
placeholder="{{ __('Enter machine location') }}">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="relative z-10">
|
||||||
<label
|
<label
|
||||||
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{
|
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{
|
||||||
__('Model') }}</label>
|
__('Model') }}</label>
|
||||||
<select name="machine_model_id" required class="luxury-select w-full">
|
<x-searchable-select name="machine_model_id" required :placeholder="__('Select Model')">
|
||||||
<option value="">{{ __('Select Model') }}</option>
|
|
||||||
@foreach($models as $model)
|
@foreach($models as $model)
|
||||||
<option value="{{ $model->id }}">{{ $model->name }}</option>
|
<option value="{{ $model->id }}">{{ $model->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||||
<!-- Left Column: Primary Info -->
|
<!-- Left Column: Primary Info -->
|
||||||
<div class="lg:col-span-12 space-y-6">
|
<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 class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<div>
|
<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>
|
<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>
|
||||||
<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>
|
<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">
|
<x-searchable-select
|
||||||
<option value="">{{ __('Select Company') }}</option>
|
name="company_id"
|
||||||
|
required
|
||||||
|
:selected="old('company_id')"
|
||||||
|
:placeholder="__('Select Company')"
|
||||||
|
>
|
||||||
@foreach($companies as $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
|
@endforeach
|
||||||
</select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||||
<!-- Left Column: Primary Info -->
|
<!-- Left Column: Primary Info -->
|
||||||
<div class="lg:col-span-12 space-y-6">
|
<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 class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<div>
|
<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>
|
<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>
|
||||||
<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>
|
<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">
|
<x-searchable-select
|
||||||
<option value="">{{ __('Select Company') }}</option>
|
name="company_id"
|
||||||
|
required
|
||||||
|
:selected="old('company_id', $paymentConfig->company_id)"
|
||||||
|
:placeholder="__('Select Company')"
|
||||||
|
>
|
||||||
@foreach($companies as $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
|
@endforeach
|
||||||
</select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -236,7 +236,7 @@
|
|||||||
x-transition:leave="ease-in duration-200"
|
x-transition:leave="ease-in duration-200"
|
||||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
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"
|
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">
|
<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"
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight"
|
||||||
@@ -296,7 +296,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Admin Account Section (Account Creation) - Only show when creating -->
|
<!-- 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="flex items-center gap-3">
|
||||||
<div class="h-6 w-1 bg-emerald-500 rounded-full"></div>
|
<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">
|
<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>
|
<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') }}">
|
<input type="text" name="admin_name" class="luxury-input w-full" placeholder="{{ __('Admin display name') }}">
|
||||||
</div>
|
</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>
|
<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)
|
@foreach($template_roles as $role)
|
||||||
<option value="{{ $role->name }}" {{ $role->name == '客戶管理員角色模板' ? 'selected' : '' }}>
|
<option value="{{ $role->name }}" {{ $role->name == '客戶管理員角色模板' ? 'selected' : '' }}>
|
||||||
{{ $role->name }}
|
{{ $role->name }}
|
||||||
</option>
|
</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact Section -->
|
<!-- 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="flex items-center gap-3">
|
||||||
<div class="h-6 w-1 bg-amber-500 rounded-full"></div>
|
<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">{{
|
<h4 class="text-xs font-black text-slate-800 dark:text-white uppercase tracking-widest">{{
|
||||||
@@ -361,13 +364,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<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">{{
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||||
__('Status') }}</label>
|
__('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="1">{{ __('Active') }}</option>
|
||||||
<option value="0">{{ __('Disabled') }}</option>
|
<option value="0">{{ __('Disabled') }}</option>
|
||||||
</select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
<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')
|
@section('content')
|
||||||
<div class="space-y-6" x-data="{
|
<div class="space-y-6" x-data="{
|
||||||
selectedPermissions: {{ json_encode($role->permissions->pluck('name')->toArray()) }},
|
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: '',
|
activeCategory: '',
|
||||||
toggleCategory(category, permissions) {
|
toggleCategory(category, permissions) {
|
||||||
const allSelected = permissions.every(p => this.selectedPermissions.includes(p));
|
const allSelected = permissions.every(p => this.selectedPermissions.includes(p));
|
||||||
@@ -20,6 +30,32 @@
|
|||||||
const selectedCount = permissions.filter(p => this.selectedPermissions.includes(p)).length;
|
const selectedCount = permissions.filter(p => this.selectedPermissions.includes(p)).length;
|
||||||
return selectedCount > 0 && selectedCount < permissions.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) {
|
scrollTo(id) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) {
|
if (el) {
|
||||||
@@ -140,7 +176,7 @@
|
|||||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Total Selected') }}</span>
|
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Total Selected') }}</span>
|
||||||
<div class="flex items-baseline gap-1">
|
<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-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>
|
</div>
|
||||||
<div class="w-12 h-12 rounded-2xl bg-emerald-500/5 flex items-center justify-center text-emerald-500">
|
<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">
|
<div class="flex-1 w-full space-y-12">
|
||||||
@foreach($all_permissions as $group => $permissions)
|
@foreach($all_permissions as $group => $permissions)
|
||||||
@php
|
@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;
|
$groupId = 'group-' . $group;
|
||||||
$groupPermissions = $permissions->pluck('name')->toArray();
|
$groupPermissions = $permissions->pluck('name')->toArray();
|
||||||
@endphp
|
@endphp
|
||||||
@@ -218,9 +261,11 @@
|
|||||||
<div class="flex items-center pr-1">
|
<div class="flex items-center pr-1">
|
||||||
<label class="relative inline-flex items-center cursor-pointer">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<input type="checkbox" name="permissions[]" value="{{ $parent->name }}"
|
<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">
|
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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,9 +282,9 @@
|
|||||||
<div class="relative flex items-center flex-shrink-0">
|
<div class="relative flex items-center flex-shrink-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
name="permissions[]"
|
name="permissions[]"
|
||||||
value="{{ $child->id }}"
|
value="{{ $child->name }}"
|
||||||
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"
|
x-model="selectedPermissions"
|
||||||
{{ $role->hasPermissionTo($child->name) ? 'checked' : '' }}>
|
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>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|||||||
@@ -66,13 +66,16 @@
|
|||||||
|
|
||||||
@if(auth()->user()->isSystemAdmin())
|
@if(auth()->user()->isSystemAdmin())
|
||||||
<div class="relative">
|
<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">
|
<x-searchable-select
|
||||||
<option value="">{{ __('All Affiliations') }}</option>
|
name="company_id"
|
||||||
<option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }}>{{ __('System Level') }}</option>
|
:options="$companies"
|
||||||
@foreach($companies as $company)
|
:selected="request('company_id')"
|
||||||
<option value="{{ $company->id }}" {{ request('company_id') == $company->id ? 'selected' : '' }}>{{ $company->name }}</option>
|
placeholder="{{ __('All Affiliations') }}"
|
||||||
@endforeach
|
class="w-full md:w-auto min-w-[280px]"
|
||||||
</select>
|
onchange="this.form.submit()"
|
||||||
|
>
|
||||||
|
<option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }} data-title="{{ __('System Level') }}">{{ __('System Level') }}</option>
|
||||||
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||||
@@ -108,11 +111,11 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6">
|
<td class="px-6 py-6">
|
||||||
@if($role->is_system)
|
@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') }}
|
{{ __('System Level') }}
|
||||||
</span>
|
</span>
|
||||||
@else
|
@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') }}
|
{{ $role->company->name ?? __('Company Level') }}
|
||||||
</span>
|
</span>
|
||||||
@endif
|
@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
|
@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">
|
<div x-data="{
|
||||||
@if(session('success'))
|
toasts: [],
|
||||||
<div x-data="{ show: true }"
|
add(message, type = 'success') {
|
||||||
x-show="show"
|
const id = Date.now();
|
||||||
x-init="setTimeout(() => show = false, 3000)"
|
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="transition ease-out duration-500"
|
||||||
x-transition:enter-start="opacity-0 transform -translate-y-4 scale-95"
|
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:enter-end="opacity-100 transform translate-y-0 scale-100"
|
||||||
x-transition:leave="transition ease-in duration-300"
|
x-transition:leave="transition ease-in duration-300"
|
||||||
x-transition:leave-start="opacity-100 transform translate-y-0 scale-100"
|
x-transition:leave-start="opacity-100 transform translate-y-0 scale-100"
|
||||||
x-transition:leave-end="opacity-0 transform -translate-y-4 scale-95"
|
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">
|
:class="toast.type === 'success'
|
||||||
<div class="size-8 bg-emerald-500/20 rounded-xl flex items-center justify-center flex-shrink-0 text-emerald-500">
|
? 'border-emerald-500/30 text-emerald-600 dark:text-emerald-400 shadow-[0_20px_50px_rgba(16,185,129,0.15)]'
|
||||||
<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>
|
: '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>
|
</div>
|
||||||
<span>{{ session('success') }}</span>
|
<span x-text="toast.message"></span>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
</template>
|
||||||
|
|
||||||
@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
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<ul class="luxury-submenu" data-sidebar-sub>
|
<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.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.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.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.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>
|
<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. 機台管理
|
// 3. 機台管理
|
||||||
Route::prefix('machines')->name('machines.')->group(function () {
|
Route::prefix('machines')->name('machines.')->group(function () {
|
||||||
Route::get('/logs', [App\Http\Controllers\Admin\MachineController::class , 'logs'])->name('logs');
|
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('/utilization', [App\Http\Controllers\Admin\MachineController::class , 'utilization'])->name('utilization');
|
||||||
Route::get('/expiry', [App\Http\Controllers\Admin\MachineController::class , 'expiry'])->name('expiry');
|
Route::get('/expiry', [App\Http\Controllers\Admin\MachineController::class , 'expiry'])->name('expiry');
|
||||||
Route::get('/maintenance', [App\Http\Controllers\Admin\MachineController::class , 'maintenance'])->name('maintenance');
|
Route::get('/maintenance', [App\Http\Controllers\Admin\MachineController::class , 'maintenance'])->name('maintenance');
|
||||||
|
|||||||
Reference in New Issue
Block a user