Compare commits
4 Commits
44ef355c54
...
d14eda7d69
| Author | SHA1 | Date | |
|---|---|---|---|
| d14eda7d69 | |||
| 9bbfaa39e6 | |||
| 2c9dc793d7 | |||
| f3b2c3e018 |
@@ -14,7 +14,6 @@ use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Models\System\User;
|
||||
|
||||
class MachineSettingController extends AdminController
|
||||
{
|
||||
@@ -46,16 +45,26 @@ class MachineSettingController extends AdminController
|
||||
}
|
||||
$models_list = $modelQuery->latest()->paginate($per_page)->withQueryString();
|
||||
|
||||
// 3. 處理使用者清單 (Accounts Tab - 授權帳號)
|
||||
$userQuery = User::query()->with('machines')->whereNotNull('company_id'); // 僅列出租戶帳號以供分配
|
||||
if ($tab === 'accounts' && $search) {
|
||||
$userQuery->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('username', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
});
|
||||
// 3. 處理機台權限 (Permissions Tab) - 僅顯示 is_admin 帳號
|
||||
$users_list = null;
|
||||
if ($tab === 'permissions') {
|
||||
$userQuery = \App\Models\System\User::query()
|
||||
->where('is_admin', true)
|
||||
->with(['company', 'machines']);
|
||||
|
||||
if ($search) {
|
||||
$userQuery->where(function($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('username', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$userQuery->where('company_id', $request->company_id);
|
||||
}
|
||||
|
||||
$users_list = $userQuery->latest()->paginate($per_page)->withQueryString();
|
||||
}
|
||||
$users_list = $userQuery->latest()->paginate($per_page)->withQueryString();
|
||||
|
||||
// 4. 基礎下拉資料 (用於新增/編輯機台的彈窗)
|
||||
$models = MachineModel::select('id', 'name')->get();
|
||||
@@ -222,66 +231,5 @@ class MachineSettingController extends AdminController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: 取得特定帳號的機台分配狀態 (從 MachineController 遷移)
|
||||
*/
|
||||
public function getAccountMachines(User $user): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$currentUser = auth()->user();
|
||||
|
||||
// 安全檢查:只能操作自己公司的帳號(除非是系統管理員)
|
||||
if (!$currentUser->isSystemAdmin() && $user->company_id !== $currentUser->company_id) {
|
||||
return response()->json(['error' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
// 取得該使用者所屬公司之所有機台
|
||||
$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: 儲存特定帳號的機台分配 (從 MachineController 遷移)
|
||||
*/
|
||||
public function syncAccountMachines(Request $request, User $user): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Machine;
|
||||
|
||||
use App\Http\Controllers\Admin\AdminController;
|
||||
use App\Models\System\Company;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\System\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MachinePermissionController extends AdminController
|
||||
{
|
||||
/**
|
||||
* 顯示機台權限管理列表
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$per_page = $request->input('per_page', 10);
|
||||
$search = $request->input('search');
|
||||
$company_id = $request->input('company_id');
|
||||
|
||||
$currentUser = auth()->user();
|
||||
|
||||
// 僅列出租戶中具有「is_admin」標記的角色帳號以供分配
|
||||
$userQuery = User::query()
|
||||
->with(['machines' => function($query) {
|
||||
$query->withoutGlobalScope('machine_access')
|
||||
->select('machines.id', 'machines.name', 'machines.serial_no');
|
||||
}])
|
||||
->whereNotNull('company_id');
|
||||
|
||||
// 非系統管理員僅能看到同公司的帳號 (因 User Model 排除 TenantScoped 全域過濾,需手動注入)
|
||||
if (!$currentUser->isSystemAdmin()) {
|
||||
$userQuery->where('company_id', $currentUser->company_id);
|
||||
} elseif ($company_id) {
|
||||
// 系統管理員的篩選邏輯
|
||||
$userQuery->where('company_id', $company_id);
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$userQuery->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('username', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$users_list = $userQuery->latest()->paginate($per_page)->withQueryString();
|
||||
$companies = $currentUser->isSystemAdmin() ? Company::all() : collect();
|
||||
|
||||
return view('admin.machines.permissions', compact('users_list', 'companies'));
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: 取得特定帳號的機台分配狀態
|
||||
*/
|
||||
public function getAccountMachines(User $user): JsonResponse
|
||||
{
|
||||
$currentUser = auth()->user();
|
||||
|
||||
// 安全檢查:只能操作自己公司的帳號(除非是系統管理員)
|
||||
if (!$currentUser->isSystemAdmin() && $user->company_id !== $currentUser->company_id) {
|
||||
return response()->json(['error' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
// 取得該使用者所屬公司之所有機台 (忽略個別帳號的 machine_access 限制,以公司為單位顯示)
|
||||
$machines = Machine::withoutGlobalScope('machine_access')
|
||||
->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, User $user): JsonResponse
|
||||
{
|
||||
$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 都屬於該使用者的公司 (使用 withoutGlobalScope 避免管理員自身權限影響驗證邏輯)
|
||||
if ($request->has('machine_ids')) {
|
||||
$machineIds = array_unique($request->machine_ids);
|
||||
$validCount = Machine::withoutGlobalScope('machine_access')
|
||||
->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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class MaintenanceController extends Controller
|
||||
$this->authorize('viewAny', MaintenanceRecord::class);
|
||||
|
||||
$query = MaintenanceRecord::with(['machine', 'user', 'company'])
|
||||
->whereHas('machine') // 確保僅顯示該帳號「看得見」的機台紀錄,避開因權限隔離導致的 null 報錯
|
||||
->latest('maintenance_at');
|
||||
|
||||
// 搜尋邏輯
|
||||
|
||||
@@ -195,11 +195,13 @@ class PermissionController extends Controller
|
||||
|
||||
$is_system = auth()->user()->isSystemAdmin() ? $request->boolean('is_system') : $role->is_system;
|
||||
|
||||
$role->update([
|
||||
$updateData = [
|
||||
'name' => $validated['name'],
|
||||
'is_system' => $is_system,
|
||||
'company_id' => $is_system ? null : $role->company_id,
|
||||
]);
|
||||
];
|
||||
|
||||
$role->update($updateData);
|
||||
|
||||
$perms = $validated['permissions'] ?? [];
|
||||
|
||||
@@ -363,6 +365,7 @@ class PermissionController extends Controller
|
||||
'status' => $validated['status'],
|
||||
'company_id' => $company_id,
|
||||
'phone' => $validated['phone'] ?? null,
|
||||
'is_admin' => (auth()->user()->isSystemAdmin() && !empty($validated['company_id'])),
|
||||
]);
|
||||
|
||||
$user->assignRole($role);
|
||||
@@ -430,6 +433,18 @@ class PermissionController extends Controller
|
||||
'phone' => $validated['phone'] ?? null,
|
||||
];
|
||||
|
||||
// 只有系統管理員在編輯租戶帳號時,且該帳號原本不是管理員,才可能觸發標記(視需求而定)
|
||||
// 這裡我們維持 storeAccount 的邏輯:如果是系統管理員幫公司「開站」或「首配」,才自動標記
|
||||
// 為求嚴謹,我們檢查該公司是否已經有 is_admin,如果沒有,當前這個人可以是第一個
|
||||
if (auth()->user()->isSystemAdmin() && !empty($validated['company_id']) && !$user->is_admin) {
|
||||
$hasAdmin = \App\Models\System\User::where('company_id', $validated['company_id'])
|
||||
->where('is_admin', true)
|
||||
->exists();
|
||||
if (!$hasAdmin) {
|
||||
$updateData['is_admin'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (auth()->user()->isSystemAdmin()) {
|
||||
// 防止超級管理員不小心把自己綁定到租客公司或降級
|
||||
if ($user->id === auth()->id()) {
|
||||
@@ -459,6 +474,7 @@ class PermissionController extends Controller
|
||||
'guard_name' => 'web',
|
||||
'company_id' => $target_company_id,
|
||||
'is_system' => false,
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$newRole->syncPermissions($roleObj->getPermissionNames());
|
||||
$roleObj = $newRole;
|
||||
|
||||
@@ -86,7 +86,14 @@ class ProductController extends Controller
|
||||
public function edit($id)
|
||||
{
|
||||
$user = auth()->user();
|
||||
$product = Product::with(['translations', 'company'])->findOrFail($id);
|
||||
// 繞過 TenantScoped 載入翻譯,確保系統管理員能看到租戶公司的翻譯資料
|
||||
$product = Product::with(['company'])->findOrFail($id);
|
||||
$product->setRelation('translations',
|
||||
Translation::withoutGlobalScopes()
|
||||
->where('group', 'product')
|
||||
->where('key', $product->name_dictionary_key)
|
||||
->get()
|
||||
);
|
||||
$categories = ProductCategory::all();
|
||||
$companies = $user->isSystemAdmin() ? Company::all() : collect();
|
||||
|
||||
@@ -131,10 +138,10 @@ class ProductController extends Controller
|
||||
? $request->company_id
|
||||
: auth()->user()->company_id;
|
||||
|
||||
// Store translations
|
||||
// 儲存多語系翻譯(繞過 TenantScoped,避免系統管理員操作租戶資料時被過濾)
|
||||
foreach ($request->names as $locale => $name) {
|
||||
if (empty($name)) continue;
|
||||
Translation::create([
|
||||
Translation::withoutGlobalScopes()->create([
|
||||
'group' => 'product',
|
||||
'key' => $dictKey,
|
||||
'locale' => $locale,
|
||||
@@ -219,10 +226,10 @@ class ProductController extends Controller
|
||||
$dictKey = $product->name_dictionary_key ?: \Illuminate\Support\Str::uuid()->toString();
|
||||
$company_id = $product->company_id;
|
||||
|
||||
// Update or Create translations
|
||||
// 更新或建立多語系翻譯(繞過 TenantScoped,避免系統管理員操作租戶資料時被過濾)
|
||||
foreach ($request->names as $locale => $name) {
|
||||
if (empty($name)) {
|
||||
Translation::where([
|
||||
Translation::withoutGlobalScopes()->where([
|
||||
'group' => 'product',
|
||||
'key' => $dictKey,
|
||||
'locale' => $locale
|
||||
@@ -230,7 +237,7 @@ class ProductController extends Controller
|
||||
continue;
|
||||
}
|
||||
|
||||
Translation::updateOrCreate(
|
||||
Translation::withoutGlobalScopes()->updateOrCreate(
|
||||
[
|
||||
'group' => 'product',
|
||||
'key' => $dictKey,
|
||||
@@ -246,6 +253,7 @@ class ProductController extends Controller
|
||||
$data = [
|
||||
'category_id' => $request->category_id,
|
||||
'name' => $request->names['zh_TW'] ?? ($product->name ?? 'Untitled'),
|
||||
'name_dictionary_key' => $dictKey,
|
||||
'barcode' => $request->barcode,
|
||||
'spec' => $request->spec,
|
||||
'manufacturer' => $request->manufacturer,
|
||||
@@ -316,9 +324,9 @@ class ProductController extends Controller
|
||||
try {
|
||||
$product = Product::findOrFail($id);
|
||||
|
||||
// Delete translations associated with this product
|
||||
// 刪除與此商品關聯的翻譯資料(繞過 TenantScoped)
|
||||
if ($product->name_dictionary_key) {
|
||||
Translation::where('key', $product->name_dictionary_key)->delete();
|
||||
Translation::withoutGlobalScopes()->where('key', $product->name_dictionary_key)->delete();
|
||||
}
|
||||
|
||||
// Delete image
|
||||
|
||||
@@ -48,6 +48,33 @@ class Product extends Model
|
||||
return $this->belongsTo(ProductCategory::class, 'category_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 自動附加到 JSON/陣列輸出的屬性(供 Alpine.js 等前端使用)
|
||||
*/
|
||||
protected $appends = ['localized_name'];
|
||||
|
||||
/**
|
||||
* 取得當前語系的商品名稱。
|
||||
* 回退順序:當前語系 → zh_TW → name 欄位
|
||||
*/
|
||||
public function getLocalizedNameAttribute(): string
|
||||
{
|
||||
if ($this->relationLoaded('translations') && $this->translations->isNotEmpty()) {
|
||||
$locale = app()->getLocale();
|
||||
// 先找當前語系
|
||||
$translation = $this->translations->firstWhere('locale', $locale);
|
||||
if ($translation) {
|
||||
return $translation->value;
|
||||
}
|
||||
// 回退至 zh_TW
|
||||
$fallback = $this->translations->firstWhere('locale', 'zh_TW');
|
||||
if ($fallback) {
|
||||
return $fallback->value;
|
||||
}
|
||||
}
|
||||
return $this->name ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translations for the product name.
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,7 @@ class User extends Authenticatable
|
||||
'avatar',
|
||||
'role',
|
||||
'status',
|
||||
'is_admin',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -51,6 +52,7 @@ class User extends Authenticatable
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'is_admin' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->boolean('is_admin')->default(false)->after('is_system');
|
||||
});
|
||||
|
||||
// 資料遷移:將所有租戶中名稱為「管理員」的角色標示為 is_admin = true
|
||||
// 這樣既有的授權篩選才不會斷掉
|
||||
DB::table('roles')
|
||||
->whereNotNull('company_id')
|
||||
->where('name', '管理員')
|
||||
->update(['is_admin' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->dropColumn('is_admin');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 1. 從 roles 移除 is_admin
|
||||
if (Schema::hasColumn('roles', 'is_admin')) {
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->dropColumn('is_admin');
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 在 users 新增 is_admin
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('is_admin')->default(false)->after('status');
|
||||
});
|
||||
|
||||
// 3. 資料遷移:針對現有租戶,將每一家公司最先建立的帳號(或是目前名稱為管理員角色的人)標記為 is_admin = true
|
||||
// 取得所有租戶公司 ID
|
||||
$companyIds = DB::table('companies')->pluck('id');
|
||||
|
||||
foreach ($companyIds as $companyId) {
|
||||
// 優先找該公司 ID 最小的 user (通常是第一個建立的)
|
||||
$userId = DB::table('users')
|
||||
->where('company_id', $companyId)
|
||||
->orderBy('id', 'asc')
|
||||
->value('id');
|
||||
|
||||
if ($userId) {
|
||||
DB::table('users')->where('id', $userId)->update(['is_admin' => true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('is_admin');
|
||||
});
|
||||
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->boolean('is_admin')->default(false)->after('is_system');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 1. 先將所有已刪除帳號的 is_admin 全部歸零,確保不會標記在「看不到的人」身上
|
||||
DB::table('users')->whereNotNull('deleted_at')->update(['is_admin' => false]);
|
||||
|
||||
// 2. 針對每一家公司,重新撈取「目前還存活 (deleted_at is null)」的最早建立帳號
|
||||
$companyIds = DB::table('companies')->pluck('id');
|
||||
|
||||
foreach ($companyIds as $companyId) {
|
||||
// 找該公司中,目前 ID 最小且「尚未被刪除」的 User
|
||||
$userId = DB::table('users')
|
||||
->where('company_id', $companyId)
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('id', 'asc')
|
||||
->value('id');
|
||||
|
||||
if ($userId) {
|
||||
// 將該帳號設為管理員,並確保該公司其它生存帳號如果是 true 的先清掉 (一對一標記)
|
||||
DB::table('users')
|
||||
->where('company_id', $companyId)
|
||||
->where('id', '!=', $userId)
|
||||
->update(['is_admin' => false]);
|
||||
|
||||
DB::table('users')->where('id', $userId)->update(['is_admin' => true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// 基本上這是資料修正,回復也不太有意義
|
||||
}
|
||||
};
|
||||
@@ -22,6 +22,7 @@ class RoleSeeder extends Seeder
|
||||
'menu.members',
|
||||
'menu.machines',
|
||||
'menu.machines.list',
|
||||
'menu.machines.permissions',
|
||||
'menu.machines.utilization',
|
||||
'menu.machines.maintenance',
|
||||
'menu.app',
|
||||
@@ -68,6 +69,7 @@ class RoleSeeder extends Seeder
|
||||
'menu.members',
|
||||
'menu.machines',
|
||||
'menu.machines.list',
|
||||
'menu.machines.permissions',
|
||||
'menu.machines.utilization',
|
||||
'menu.machines.maintenance',
|
||||
'menu.app',
|
||||
|
||||
30
lang/en.json
30
lang/en.json
@@ -73,10 +73,14 @@
|
||||
"Assign Machines": "Assign Machines",
|
||||
"Assigned Machines": "Assigned Machines",
|
||||
"Audit Management": "Audit Management",
|
||||
"Authorization updated successfully": "Authorization updated successfully",
|
||||
"Authorize": "Authorize",
|
||||
"Authorize Btn": "Authorize",
|
||||
"Authorized Accounts": "Authorized Accounts",
|
||||
"Authorized Accounts Tab": "Authorized Accounts",
|
||||
"Authorized Machines": "Authorized Machines",
|
||||
"Authorized Machines Management": "Authorized Machines Management",
|
||||
"Authorized Status": "Authorized",
|
||||
"Availability": "可用性 (Availability)",
|
||||
"Available Machines": "可供分配的機台",
|
||||
"Avatar updated successfully.": "Avatar updated successfully.",
|
||||
@@ -183,7 +187,6 @@
|
||||
"Delete Product Confirmation": "Delete Product Confirmation",
|
||||
"Deposit Bonus": "Deposit Bonus",
|
||||
"Describe the repair or maintenance status...": "Describe the repair or maintenance status...",
|
||||
"Deselect All": "取消全選",
|
||||
"Detail": "Detail",
|
||||
"Device Information": "Device Information",
|
||||
"Device Status Logs": "Device Status Logs",
|
||||
@@ -353,6 +356,7 @@
|
||||
"Manage Expiry": "Manage Expiry",
|
||||
"Manage administrative and tenant accounts": "Manage administrative and tenant accounts",
|
||||
"Manage all tenant accounts and validity": "Manage all tenant accounts and validity",
|
||||
"Manage machine access permissions": "Manage machine access permissions",
|
||||
"Manage your catalog, prices, and multilingual details.": "Manage your catalog, prices, and multilingual details.",
|
||||
"Manage your machine fleet and operational data": "Manage your machine fleet and operational data",
|
||||
"Manage your profile information, security settings, and login history": "Manage your profile information, security settings, and login history",
|
||||
@@ -571,6 +575,7 @@
|
||||
"Scale level and access control": "層級與存取控制",
|
||||
"Scan this code to quickly access the maintenance form for this device.": "Scan this code to quickly access the maintenance form for this device.",
|
||||
"Search configurations...": "Search configurations...",
|
||||
"Search company...": "Search company...",
|
||||
"Search customers...": "Search customers...",
|
||||
"Search machines by name or serial...": "Search machines by name or serial...",
|
||||
"Search machines...": "Search machine name or serial...",
|
||||
@@ -580,20 +585,21 @@
|
||||
"Search serial or machine...": "Search serial or machine...",
|
||||
"Search serial or name...": "Search serial or name...",
|
||||
"Search users...": "Search users...",
|
||||
"Select All": "全選",
|
||||
"Select All": "Select All",
|
||||
"Select Company": "Select Company Name",
|
||||
"Select Machine": "選擇機台",
|
||||
"Select Machine to view metrics": "請選擇機台以查看指標",
|
||||
"Select Machine": "Select Machine",
|
||||
"Select Machine to view metrics": "Please select a machine to view metrics",
|
||||
"Select Model": "Select Model",
|
||||
"Select Owner": "Select Company Name",
|
||||
"Select a machine to deep dive": "請選擇機台以開始深度分析",
|
||||
"Select a machine to deep dive": "Please select a machine to deep dive",
|
||||
"Select an asset from the left to start analysis": "Select an asset from the left to start analysis",
|
||||
"Select date to sync data": "Select date to sync data",
|
||||
"Selected": "Selected",
|
||||
"Selected Date": "查詢日期",
|
||||
"Selected Date": "Search Date",
|
||||
"Selection": "Selection",
|
||||
"Serial & Version": "Serial & Version",
|
||||
"Serial NO": "Serial NO",
|
||||
"Deselect All": "Deselect All",
|
||||
"Serial NO": "SERIAL NO",
|
||||
"Serial No": "Serial No",
|
||||
"Serial Number": "Serial Number",
|
||||
"Show": "Show",
|
||||
@@ -651,6 +657,7 @@
|
||||
"The Super Admin role is immutable.": "The Super Admin role is immutable.",
|
||||
"The Super Admin role name cannot be modified.": "The Super Admin role name cannot be modified.",
|
||||
"The image is too large. Please upload an image smaller than 1MB.": "The image is too large. Please upload an image smaller than 1MB.",
|
||||
"This is a system administrator role. Its name is locked to ensure system stability.": "This is a system administrator role. Its name is locked to ensure system stability.",
|
||||
"This role belongs to another company and cannot be assigned.": "This role belongs to another company and cannot be assigned.",
|
||||
"Time": "Time",
|
||||
"Time Slots": "Time Slots",
|
||||
@@ -675,6 +682,7 @@
|
||||
"Tutorial Page": "Tutorial Page",
|
||||
"Type": "Type",
|
||||
"UI Elements": "UI Elements",
|
||||
"Unauthorized Status": "Unauthorized",
|
||||
"Uncategorized": "Uncategorized",
|
||||
"Unified Operational Timeline": "Unified Operational Timeline",
|
||||
"Units": "Units",
|
||||
@@ -762,6 +770,7 @@
|
||||
"menu.machines": "Machine Management",
|
||||
"menu.machines.list": "Machine List",
|
||||
"menu.machines.maintenance": "Maintenance Records",
|
||||
"menu.machines.permissions": "Machine Permissions",
|
||||
"menu.machines.utilization": "Utilization Rate",
|
||||
"menu.members": "Member Management",
|
||||
"menu.permission": "Permission Settings",
|
||||
@@ -791,10 +800,5 @@
|
||||
"user": "一般用戶",
|
||||
"vs Yesterday": "vs Yesterday",
|
||||
"warehouses": "Warehouse Management",
|
||||
"待填寫": "Pending",
|
||||
"Authorized Accounts Tab": "Authorized Accounts",
|
||||
"Authorize Btn": "Authorize",
|
||||
"Authorization updated successfully": "Authorization updated successfully",
|
||||
"Authorized Status": "Authorized",
|
||||
"Unauthorized Status": "Unauthorized"
|
||||
"待填寫": "Pending"
|
||||
}
|
||||
66
lang/ja.json
66
lang/ja.json
@@ -74,7 +74,12 @@
|
||||
"Assigned Machines": "授權機台",
|
||||
"Audit Management": "監査管理",
|
||||
"Audit Permissions": "監査管理權限",
|
||||
"Authorization updated successfully": "認証が更新されました",
|
||||
"Authorize Btn": "認可",
|
||||
"Authorized Accounts Tab": "認定アカウント",
|
||||
"Authorized Machines": "授權機台",
|
||||
"Authorized Machines Management": "認定機台管理",
|
||||
"Authorized Status": "認可済み",
|
||||
"Availability": "可用性 (Availability)",
|
||||
"Available Machines": "可供分配的機台",
|
||||
"Avatar updated successfully.": "アバターが正常に更新されました。",
|
||||
@@ -163,7 +168,7 @@
|
||||
"Customer enabled successfully.": "顧客が正常に有効化されました。",
|
||||
"Customer updated successfully.": "顧客が正常に更新されました。",
|
||||
"Cycle Efficiency": "サイクル効率",
|
||||
"Daily Revenue": "日次収益",
|
||||
"Daily Revenue": "当日収益",
|
||||
"Danger Zone: Delete Account": "危険区域:アカウントの削除",
|
||||
"Dashboard": "ダッシュボード",
|
||||
"Data Configuration": "データ設定",
|
||||
@@ -179,7 +184,6 @@
|
||||
"Delete Product Confirmation": "商品削除の確認",
|
||||
"Deposit Bonus": "入金ボーナス",
|
||||
"Describe the repair or maintenance status...": "修理またはメンテナンスの状況を説明してください...",
|
||||
"Deselect All": "取消全選",
|
||||
"Detail": "詳細",
|
||||
"Device Information": "デバイス情報",
|
||||
"Device Status Logs": "デバイス状態ログ",
|
||||
@@ -242,7 +246,7 @@
|
||||
"Fill in the device repair or maintenance details": "デバイスの修理またはメンテナンスの詳細を入力してください",
|
||||
"Fill in the product details below": "以下に商品の詳細を入力してください",
|
||||
"Firmware Version": "ファームウェアバージョン",
|
||||
"Fleet Avg OEE": "フリート平均 OEE",
|
||||
"Fleet Avg OEE": "全機台平均OEE",
|
||||
"Fleet Performance": "全機隊效能",
|
||||
"From": "から",
|
||||
"From:": "開始:",
|
||||
@@ -297,7 +301,7 @@
|
||||
"Line Orders": "Line注文",
|
||||
"Line Permissions": "Line管理權限",
|
||||
"Line Products": "Line商品",
|
||||
"Live Fleet Updates": "ライブフリート更新",
|
||||
"Live Fleet Updates": "機台リアルタイム更新",
|
||||
"Loading machines...": "正在載入機台...",
|
||||
"Loading...": "読み込み中...",
|
||||
"Location": "場所",
|
||||
@@ -320,7 +324,7 @@
|
||||
"Machine Model Settings": "機台型號設定",
|
||||
"Machine Name": "機台名",
|
||||
"Machine Permissions": "機台権限",
|
||||
"Machine Registry": "機台登録",
|
||||
"Machine Registry": "機台登録簿",
|
||||
"Machine Reports": "機台レポート",
|
||||
"Machine Restart": "機台再起動",
|
||||
"Machine Settings": "機台設定",
|
||||
@@ -349,6 +353,7 @@
|
||||
"Manage Expiry": "進入效期管理",
|
||||
"Manage administrative and tenant accounts": "管理者およびテナントアカウントを管理します",
|
||||
"Manage all tenant accounts and validity": "すべてのテナントアカウントと有効期限を管理します",
|
||||
"Manage machine access permissions": "機台アクセス權限の管理",
|
||||
"Manage your catalog, prices, and multilingual details.": "カタログ、価格、多言語詳細を管理します。",
|
||||
"Manage your machine fleet and operational data": "機台フリートと運用データの管理",
|
||||
"Manage your profile information, security settings, and login history": "プロフィール情報、セキュリティ設定、ログイン履歴の管理",
|
||||
@@ -406,7 +411,7 @@
|
||||
"No machines available in this company.": "此客戶目前沒有可供分配的機台。",
|
||||
"No maintenance records found": "メンテナンス記録が見つかりません",
|
||||
"No matching logs found": "一致するログが見つかりません",
|
||||
"No matching machines": "一致する機台が見つかりません",
|
||||
"No matching machines": "一致する機台がありません",
|
||||
"No permissions": "権限項目なし",
|
||||
"No roles found.": "ロールが見つかりませんでした。",
|
||||
"No slots found": "未找到貨道資訊",
|
||||
@@ -417,8 +422,8 @@
|
||||
"Not Used Description": "不使用第三方支付介接",
|
||||
"Notes": "備考",
|
||||
"OEE": "OEE",
|
||||
"OEE Efficiency Trend": "OEE 効率トレンド",
|
||||
"OEE Score": "OEE スコア",
|
||||
"OEE Efficiency Trend": "OEE効率トレンド",
|
||||
"OEE Score": "OEE総合スコア",
|
||||
"OEE.Activity": "稼働アクティビティ",
|
||||
"OEE.Errors": "エラー",
|
||||
"OEE.Hours": "時間",
|
||||
@@ -431,12 +436,12 @@
|
||||
"Online": "オンライン",
|
||||
"Online Duration": "累積連線時數",
|
||||
"Online Machines": "オンライン機台",
|
||||
"Online Status": "オンライン状態",
|
||||
"Online Status": "オンラインステータス",
|
||||
"Only system roles can be assigned to platform administrative accounts.": "プラットフォーム管理アカウントにはシステムロールのみ割り当て可能です。",
|
||||
"Operational Parameters": "運用パラメータ",
|
||||
"Operations": "運用設定",
|
||||
"Optimal": "最適",
|
||||
"Optimized Performance": "最適化されたパフォーマンス",
|
||||
"Optimized Performance": "パフォーマンスの最適化",
|
||||
"Optimized for display. Supported formats: JPG, PNG, WebP.": "表示用に最適化されています。対応形式:JPG, PNG, WebP。",
|
||||
"Optional": "任意",
|
||||
"Order Management": "注文管理",
|
||||
@@ -446,7 +451,7 @@
|
||||
"Original:": "元:",
|
||||
"Other Permissions": "其他權限",
|
||||
"Others": "その他",
|
||||
"Output Count": "出力数",
|
||||
"Output Count": "出荷回数",
|
||||
"Owner": "会社名",
|
||||
"PARTNER_KEY": "パートナーキー",
|
||||
"PI_MERCHANT_ID": "Pi 拍錢包 加盟店ID",
|
||||
@@ -514,9 +519,9 @@
|
||||
"Quick search...": "クイック検索...",
|
||||
"Real-time OEE analysis awaits": "リアルタイム OEE 分析待機中",
|
||||
"Real-time Operation Logs (Last 50)": "リアルタイム操作ログ (直近 50 件)",
|
||||
"Real-time fleet efficiency and OEE metrics": "リアルタイムのフリート効率と OEE 指標",
|
||||
"Real-time fleet efficiency and OEE metrics": "全機台リアルタイム効率とOEE指標",
|
||||
"Real-time monitoring across all machines": "全機台のリアルタイム監視",
|
||||
"Real-time performance analytics": "リアルタイム・パフォーマンス分析",
|
||||
"Real-time performance analytics": "リアルタイムパフォーマンス分析",
|
||||
"Real-time status monitoring": "リアルタイムステータス監視",
|
||||
"Receipt Printing": "レシート印刷",
|
||||
"Recent Login": "最近のログイン",
|
||||
@@ -571,6 +576,7 @@
|
||||
"Scale level and access control": "層級與存取控制",
|
||||
"Scan this code to quickly access the maintenance form for this device.": "このコードをスキャンして、このデバイスのメンテナンスフォームに素早くアクセスしてください。",
|
||||
"Search configurations...": "設定を検索...",
|
||||
"Search company...": "会社を検索...",
|
||||
"Search customers...": "顧客を検索...",
|
||||
"Search machines by name or serial...": "名称またはシリアル番号で検索...",
|
||||
"Search machines...": "マシン名またはシリアル番号で検索...",
|
||||
@@ -578,22 +584,23 @@
|
||||
"Search roles...": "ロールを検索...",
|
||||
"Search serial no or name...": "シリアル番号または名前を検索...",
|
||||
"Search serial or machine...": "シリアルまたはマシンを検索...",
|
||||
"Search serial or name...": "シリアル番号または名称で検索...",
|
||||
"Search serial or name...": "機台名またはシリアル番号で検索...",
|
||||
"Search users...": "ユーザーを検索...",
|
||||
"Select All": "全選",
|
||||
"Select All": "すべて選択",
|
||||
"Select Company": "会社名を選択",
|
||||
"Select Machine": "選擇機台",
|
||||
"Select Machine to view metrics": "請選擇機台以查看指標",
|
||||
"Select Machine": "機台を選択",
|
||||
"Select Machine to view metrics": "指標を表示する機台を選択してください",
|
||||
"Select Model": "型番を選択",
|
||||
"Select Owner": "会社名を選択",
|
||||
"Select a machine to deep dive": "請選擇機台以開始深度分析",
|
||||
"Select a machine to deep dive": "詳細分析を開始する機台を選択してください",
|
||||
"Select an asset from the left to start analysis": "分析を開始するには左側のデバイスを選択してください",
|
||||
"Select date to sync data": "データ同期の日付を選択してください",
|
||||
"Selected": "已選擇",
|
||||
"Selected Date": "查詢日期",
|
||||
"Selected": "選択済み",
|
||||
"Selected Date": "検索日",
|
||||
"Selection": "選択済み",
|
||||
"Deselect All": "すべて選択解除",
|
||||
"Serial & Version": "シリアルとバージョン",
|
||||
"Serial NO": "シリアル番号",
|
||||
"Serial NO": "機台シリアル番号",
|
||||
"Serial No": "機台シリアル番号",
|
||||
"Serial Number": "シリアル番号",
|
||||
"Show": "表示",
|
||||
@@ -653,6 +660,7 @@
|
||||
"The Super Admin role is immutable.": "スーパー管理者ロールは変更できません。",
|
||||
"The Super Admin role name cannot be modified.": "スーパー管理者のロール名は変更できません。",
|
||||
"The image is too large. Please upload an image smaller than 1MB.": "画像が大きすぎます。1MB未満の画像をアップロードしてください。",
|
||||
"This is a system administrator role. Its name is locked to ensure system stability.": "これはシステム管理者ロールです。システムの安定性を確保するため、名称は固定されています。",
|
||||
"This role belongs to another company and cannot be assigned.": "このロールは他の会社に属しており、割り当てることはできません。",
|
||||
"Time": "時間",
|
||||
"Time Slots": "タイムスロット",
|
||||
@@ -664,7 +672,7 @@
|
||||
"Total Connected": "接続数合計",
|
||||
"Total Customers": "顧客總數",
|
||||
"Total Daily Sales": "本日累計銷量",
|
||||
"Total Gross Value": "総売上額",
|
||||
"Total Gross Value": "売上高計",
|
||||
"Total Logins": "總ログイン數",
|
||||
"Total Selected": "已選擇總數",
|
||||
"Total Slots": "合計スロット数",
|
||||
@@ -677,9 +685,10 @@
|
||||
"Tutorial Page": "チュートリアル画面",
|
||||
"Type": "タイプ",
|
||||
"UI Elements": "UI要素",
|
||||
"Unauthorized Status": "未認可",
|
||||
"Uncategorized": "未分類",
|
||||
"Unified Operational Timeline": "整合式營運時序圖",
|
||||
"Units": "ユニット",
|
||||
"Units": "台",
|
||||
"Unknown": "不明",
|
||||
"Update": "更新",
|
||||
"Update Authorization": "権限を更新",
|
||||
@@ -697,7 +706,7 @@
|
||||
"Utilization Rate": "稼働率",
|
||||
"Utilization Timeline": "稼動時序",
|
||||
"Utilization, OEE and Operational Intelligence": "稼動率、OEE と運用インテリジェンス",
|
||||
"Utilized Time": "稼働時間",
|
||||
"Utilized Time": "稼動持続時間",
|
||||
"Valid Until": "有効期限",
|
||||
"Validation Error": "検証エラー",
|
||||
"Vending Page": "販売画面",
|
||||
@@ -764,6 +773,7 @@
|
||||
"menu.machines": "機台管理",
|
||||
"menu.machines.list": "機台リスト",
|
||||
"menu.machines.maintenance": "メンテナンス記録",
|
||||
"menu.machines.permissions": "機台権限",
|
||||
"menu.machines.utilization": "稼働率",
|
||||
"menu.members": "会員管理",
|
||||
"menu.permission": "權限設定",
|
||||
@@ -793,11 +803,5 @@
|
||||
"user": "一般用戶",
|
||||
"vs Yesterday": "前日比",
|
||||
"warehouses": "倉庫管理",
|
||||
"待填寫": "待填寫",
|
||||
"Authorized Accounts Tab": "認定アカウント",
|
||||
"Authorize Btn": "認可",
|
||||
"Authorized Machines Management": "認定機台管理",
|
||||
"Authorization updated successfully": "認証が更新されました",
|
||||
"Authorized Status": "認可済み",
|
||||
"Unauthorized Status": "未認可"
|
||||
"待填寫": "待填寫"
|
||||
}
|
||||
@@ -77,10 +77,14 @@
|
||||
"Assigned Machines": "授權機台",
|
||||
"Audit Management": "稽核管理",
|
||||
"Audit Permissions": "稽核管理權限",
|
||||
"Authorization updated successfully": "授權更新成功",
|
||||
"Authorize": "授權",
|
||||
"Authorize Btn": "授權",
|
||||
"Authorized Accounts": "授權帳號",
|
||||
"Authorized Accounts Tab": "授權帳號",
|
||||
"Authorized Machines": "授權機台",
|
||||
"Authorized Machines Management": "授權機台管理",
|
||||
"Authorized Status": "已授權",
|
||||
"Availability": "可用性 (Availability)",
|
||||
"Available Machines": "可供分配的機台",
|
||||
"Avatar updated successfully.": "頭像已成功更新。",
|
||||
@@ -189,7 +193,6 @@
|
||||
"Delete Product Confirmation": "刪除商品確認",
|
||||
"Deposit Bonus": "儲值回饋",
|
||||
"Describe the repair or maintenance status...": "請描述維修或保養狀況...",
|
||||
"Deselect All": "取消全選",
|
||||
"Detail": "詳細",
|
||||
"Device Information": "設備資訊",
|
||||
"Device Status Logs": "設備狀態紀錄",
|
||||
@@ -322,7 +325,7 @@
|
||||
"Machine Details": "機台詳情",
|
||||
"Machine Images": "機台照片",
|
||||
"Machine Info": "機台資訊",
|
||||
"Machine Information": "機台資訊",
|
||||
"Machine Information": "機器資訊",
|
||||
"Machine List": "機台列表",
|
||||
"Machine Login Logs": "機台登入紀錄",
|
||||
"Machine Logs": "機台日誌",
|
||||
@@ -331,7 +334,7 @@
|
||||
"Machine Model": "機台型號",
|
||||
"Machine Model Settings": "機台型號設定",
|
||||
"Machine Name": "機台名稱",
|
||||
"Machine Permissions": "授權機台",
|
||||
"Machine Permissions": "機台權限",
|
||||
"Machine Registry": "機台清冊",
|
||||
"Machine Reports": "機台報表",
|
||||
"Machine Restart": "機台重啟",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Manage Expiry": "進入效期管理",
|
||||
"Manage administrative and tenant accounts": "管理系統管理者與租戶帳號",
|
||||
"Manage all tenant accounts and validity": "管理所有租戶帳號與合約效期",
|
||||
"Manage machine access permissions": "管理機台存取權限",
|
||||
"Manage your catalog, prices, and multilingual details.": "管理您的商品型錄、價格及多語系詳情。",
|
||||
"Manage your machine fleet and operational data": "管理您的機台群組與營運數據",
|
||||
"Manage your profile information, security settings, and login history": "管理您的個人資訊、安全設定與登入紀錄",
|
||||
@@ -420,7 +424,7 @@
|
||||
"No machines available in this company.": "此客戶目前沒有可供分配的機台。",
|
||||
"No maintenance records found": "找不到維修紀錄",
|
||||
"No matching logs found": "找不到符合條件的日誌",
|
||||
"No matching machines": "找不到符合的機台",
|
||||
"No matching machines": "查無匹配機台",
|
||||
"No permissions": "無權限項目",
|
||||
"No roles found.": "找不到角色資料。",
|
||||
"No slots found": "未找到貨道資訊",
|
||||
@@ -431,7 +435,7 @@
|
||||
"Not Used Description": "不使用第三方支付介接",
|
||||
"Notes": "備註",
|
||||
"OEE Efficiency Trend": "OEE 效率趨勢",
|
||||
"OEE Score": "OEE 綜合得分",
|
||||
"OEE Score": "OEE 綜合評分",
|
||||
"OEE.Activity": "營運活動",
|
||||
"OEE.Errors": "異常",
|
||||
"OEE.Hours": "小時",
|
||||
@@ -449,7 +453,7 @@
|
||||
"Operational Parameters": "運作參數",
|
||||
"Operations": "運作設定",
|
||||
"Optimal": "良好",
|
||||
"Optimized Performance": "效能優化",
|
||||
"Optimized Performance": "效能最佳化",
|
||||
"Optimized for display. Supported formats: JPG, PNG, WebP.": "已針對顯示進行優化。支援格式:JPG, PNG, WebP。",
|
||||
"Optional": "選填",
|
||||
"Order Management": "訂單管理",
|
||||
@@ -459,7 +463,7 @@
|
||||
"Original:": "原:",
|
||||
"Other Permissions": "其他權限",
|
||||
"Others": "其他功能",
|
||||
"Output Count": "出貨數量",
|
||||
"Output Count": "出貨次數",
|
||||
"Owner": "公司名稱",
|
||||
"PARTNER_KEY": "PARTNER_KEY",
|
||||
"PI_MERCHANT_ID": "Pi 拍錢包 商店代號",
|
||||
@@ -588,6 +592,7 @@
|
||||
"Scale level and access control": "層級與存取控制",
|
||||
"Scan this code to quickly access the maintenance form for this device.": "掃描此 QR Code 即可快速進入此設備的維修單填寫頁面。",
|
||||
"Search accounts...": "搜尋帳號...",
|
||||
"Search company...": "搜尋公司...",
|
||||
"Search configurations...": "搜尋設定...",
|
||||
"Search customers...": "搜尋客戶...",
|
||||
"Search machines by name or serial...": "搜尋機台名稱或序號...",
|
||||
@@ -597,6 +602,7 @@
|
||||
"Search roles...": "搜尋角色...",
|
||||
"Search serial no or name...": "搜尋序號或機台名稱...",
|
||||
"Search serial or machine...": "搜尋序號或機台名稱...",
|
||||
"Search serial or name...": "搜尋序號或機台名稱...",
|
||||
"Search users...": "搜尋用戶...",
|
||||
"Select All": "全選",
|
||||
"Select Category": "選擇類別",
|
||||
@@ -606,11 +612,14 @@
|
||||
"Select Model": "選擇型號",
|
||||
"Select Owner": "選擇公司名稱",
|
||||
"Select a machine to deep dive": "請選擇機台以開始深度分析",
|
||||
"Select an asset from the left to start analysis": "選擇左側機台以開始分析數據",
|
||||
"Select date to sync data": "選擇日期以同步數據",
|
||||
"Selected": "已選擇",
|
||||
"Selected Date": "查詢日期",
|
||||
"Selection": "已選擇",
|
||||
"Serial & Version": "序號與版本",
|
||||
"Deselect All": "取消全選",
|
||||
"Serial NO": "機台序號",
|
||||
"Serial No": "機台序號",
|
||||
"Serial Number": "機台序號",
|
||||
"Show": "顯示",
|
||||
@@ -673,6 +682,7 @@
|
||||
"The Super Admin role is immutable.": "超級管理員角色不可修改。",
|
||||
"The Super Admin role name cannot be modified.": "超級管理員角色的名稱無法修改。",
|
||||
"The image is too large. Please upload an image smaller than 1MB.": "圖片檔案太大,請上傳小於 1MB 的圖片。",
|
||||
"This is a system administrator role. Its name is locked to ensure system stability.": "這是系統管理員角色,名稱已鎖定以確保系統穩定性。",
|
||||
"This role belongs to another company and cannot be assigned.": "此角色屬於其他公司,無法指派。",
|
||||
"Time": "時間",
|
||||
"Time Slots": "時段組合",
|
||||
@@ -685,7 +695,7 @@
|
||||
"Total Connected": "總計連線數",
|
||||
"Total Customers": "客戶總數",
|
||||
"Total Daily Sales": "本日累計銷量",
|
||||
"Total Gross Value": "銷售總結",
|
||||
"Total Gross Value": "銷售總額",
|
||||
"Total Logins": "總登入次數",
|
||||
"Total Selected": "已選擇總數",
|
||||
"Total Slots": "總貨道數",
|
||||
@@ -699,6 +709,7 @@
|
||||
"Tutorial Page": "教學頁",
|
||||
"Type": "類型",
|
||||
"UI Elements": "UI元素",
|
||||
"Unauthorized Status": "未授權",
|
||||
"Uncategorized": "未分類",
|
||||
"Unified Operational Timeline": "整合式營運時序圖",
|
||||
"Units": "台",
|
||||
@@ -787,6 +798,7 @@
|
||||
"menu.machines": "機台管理",
|
||||
"menu.machines.list": "機台列表",
|
||||
"menu.machines.maintenance": "維修管理單",
|
||||
"menu.machines.permissions": "機台權限",
|
||||
"menu.machines.utilization": "機台嫁動率",
|
||||
"menu.members": "會員管理",
|
||||
"menu.permission": "權限設定",
|
||||
@@ -816,10 +828,5 @@
|
||||
"user": "一般用戶",
|
||||
"vs Yesterday": "較昨日",
|
||||
"warehouses": "倉庫管理",
|
||||
"待填寫": "待填寫",
|
||||
"Authorized Accounts Tab": "授權帳號",
|
||||
"Authorize Btn": "授權",
|
||||
"Authorization updated successfully": "授權更新成功",
|
||||
"Authorized Status": "已授權",
|
||||
"Unauthorized Status": "未授權"
|
||||
"待填寫": "待填寫"
|
||||
}
|
||||
@@ -114,7 +114,7 @@
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Error processing request') }}', type: 'error' } }));
|
||||
});
|
||||
},
|
||||
// Machine Permissions (Migrated from Account Management)
|
||||
// Permission Management
|
||||
showPermissionModal: false,
|
||||
isPermissionsLoading: false,
|
||||
targetUserId: null,
|
||||
@@ -131,7 +131,7 @@
|
||||
this.allMachines = [];
|
||||
this.permissionSearchQuery = '';
|
||||
|
||||
fetch(`/admin/basic-settings/machines/permissions/accounts/${user.id}`)
|
||||
fetch(`/admin/machines/permissions/accounts/${user.id}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.machines) {
|
||||
@@ -154,10 +154,20 @@
|
||||
togglePermission(machineId) {
|
||||
this.permissions = { ...this.permissions, [machineId]: !this.permissions[machineId] };
|
||||
},
|
||||
toggleSelectAll() {
|
||||
const filtered = this.allMachines.filter(m =>
|
||||
!this.permissionSearchQuery ||
|
||||
m.name.toLowerCase().includes(this.permissionSearchQuery.toLowerCase()) ||
|
||||
m.serial_no.toLowerCase().includes(this.permissionSearchQuery.toLowerCase())
|
||||
);
|
||||
if (filtered.length === 0) return;
|
||||
const allSelected = filtered.every(m => this.permissions[m.id]);
|
||||
filtered.forEach(m => this.permissions[m.id] = !allSelected);
|
||||
},
|
||||
savePermissions() {
|
||||
const machineIds = Object.keys(this.permissions).filter(id => this.permissions[id]);
|
||||
|
||||
fetch(`/admin/basic-settings/machines/permissions/accounts/${this.targetUserId}`, {
|
||||
fetch(`/admin/machines/permissions/accounts/${this.targetUserId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -195,7 +205,7 @@
|
||||
</svg>
|
||||
<span>{{ __('Add Machine') }}</span>
|
||||
</button>
|
||||
@else
|
||||
@elseif($tab === 'models')
|
||||
<button @click="showCreateModelModal = true" class="btn-luxury-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
@@ -216,9 +226,9 @@
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all {{ $tab === 'models' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200' }}">
|
||||
{{ __('Models') }}
|
||||
</a>
|
||||
<a href="{{ route('admin.basic-settings.machines.index', ['tab' => 'accounts']) }}"
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all {{ $tab === 'accounts' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200' }}">
|
||||
{{ __('Authorized Accounts Tab') }}
|
||||
<a href="{{ route('admin.basic-settings.machines.index', ['tab' => 'permissions']) }}"
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all {{ $tab === 'permissions' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200' }}">
|
||||
{{ __('Machine Permissions') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -226,20 +236,33 @@
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in mt-6">
|
||||
<!-- Toolbar & Filters -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<form method="GET" action="{{ route('admin.basic-settings.machines.index') }}" class="relative group">
|
||||
<input type="hidden" name="tab" value="{{ $tab }}">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
placeholder="{{ $tab === 'machines' ? __('Search machines...') : ($tab === 'models' ? __('Search models...') : __('Search accounts...')) }}"
|
||||
class="luxury-input py-2.5 pl-12 pr-6 block w-64">
|
||||
</form>
|
||||
<div class="flex items-center gap-4">
|
||||
<form method="GET" action="{{ route('admin.basic-settings.machines.index') }}" class="relative group">
|
||||
<input type="hidden" name="tab" value="{{ $tab }}">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
placeholder="{{ $tab === 'machines' ? __('Search machines...') : ($tab === 'models' ? __('Search models...') : __('Search accounts...')) }}"
|
||||
class="luxury-input py-2.5 pl-12 pr-6 block w-64">
|
||||
</form>
|
||||
|
||||
@if($tab === 'permissions' && auth()->user()->isSystemAdmin())
|
||||
<div class="w-72">
|
||||
<form method="GET" action="{{ route('admin.basic-settings.machines.index') }}">
|
||||
<input type="hidden" name="tab" value="permissions">
|
||||
<input type="hidden" name="search" value="{{ request('search') }}">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
|
||||
:placeholder="__('All Companies')" onchange="this.form.submit()" />
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($tab === 'machines')
|
||||
@@ -388,62 +411,83 @@
|
||||
{{ $machines->appends(['tab' => 'machines'])->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
|
||||
@elseif($tab === 'accounts')
|
||||
<!-- Accounts Table (Machine Selection Interface) -->
|
||||
@elseif($tab === 'permissions')
|
||||
<!-- Permissions Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Account Info') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Affiliation') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-left">
|
||||
{{ __('Company Name') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
|
||||
{{ __('Authorized Machines') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">
|
||||
{{ __('Action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($users_list as $user)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6 font-display">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700">
|
||||
<td class="px-6 py-6 font-display text-left">
|
||||
<div class="flex items-center gap-4 text-left">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $user->name }}</span>
|
||||
<span class="text-xs font-mono font-bold text-slate-500 tracking-widest uppercase">{{ $user->username }}</span>
|
||||
<div class="flex flex-col text-left">
|
||||
<span
|
||||
class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{
|
||||
$user->name }}</span>
|
||||
<span
|
||||
class="text-xs font-mono font-bold text-slate-500 tracking-widest uppercase">{{
|
||||
$user->username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest uppercase">
|
||||
<td class="px-6 py-6 text-left">
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest uppercase">
|
||||
{{ $user->company->name ?? __('System') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex flex-wrap gap-2 justify-center lg:justify-start max-w-[400px] mx-auto lg:mx-0">
|
||||
<td class="px-6 py-4 text-center">
|
||||
<div
|
||||
class="flex flex-wrap gap-2 justify-center lg:justify-start max-w-[420px] mx-auto lg:mx-0 max-h-[140px] overflow-y-auto pr-2 custom-scrollbar py-1 text-left">
|
||||
@forelse($user->machines as $m)
|
||||
<div class="flex flex-col px-4 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-white/5 hover:border-cyan-500/30 transition-all duration-300 shadow-sm">
|
||||
<span class="text-xs font-black text-slate-700 dark:text-slate-200 leading-tight">{{ $m->name }}</span>
|
||||
<span class="text-[10px] font-mono font-bold text-cyan-500 tracking-tighter mt-1">{{ $m->serial_no }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col px-3 py-1.5 rounded-xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-white/5 hover:border-cyan-500/30 transition-all duration-300 text-left">
|
||||
<span class="text-[11px] font-black text-slate-700 dark:text-slate-200 leading-tight">{{
|
||||
$m->name }}</span>
|
||||
<span class="text-[9px] font-mono font-bold text-cyan-500 tracking-tighter mt-0.5 opacity-80">{{
|
||||
$m->serial_no }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<div class="w-full text-center lg:text-left">
|
||||
<span class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest opacity-40 italic">-- {{ __('None') }} --</span>
|
||||
</div>
|
||||
<div class="w-full text-center lg:text-left">
|
||||
<span
|
||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest opacity-40 italic">--
|
||||
{{ __('None') }} --</span>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<button @click="openPermissionModal({{ json_encode(['id' => $user->id, 'name' => $user->name]) }})"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500 hover:text-white transition-all duration-300 text-xs font-black uppercase tracking-widest shadow-sm shadow-cyan-500/5 group/auth">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 00-2 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
||||
<span>{{ __('Authorize Btn') }}</span>
|
||||
<button
|
||||
@click='openPermissionModal({{ json_encode(["id" => $user->id, "name" => $user->name]) }})'
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500 hover:text-white transition-all duration-300 text-xs font-black uppercase tracking-widest shadow-sm shadow-cyan-500/5 group/auth">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 00-2 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<span>{{ __('Authorize') }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -451,8 +495,12 @@
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-24 text-center">
|
||||
<div class="flex flex-col items-center gap-3 opacity-20">
|
||||
<svg class="size-16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" /></svg>
|
||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
|
||||
<svg class="size-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" />
|
||||
</svg>
|
||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No
|
||||
accounts found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -460,8 +508,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $users_list->appends(['tab' => 'accounts'])->links('vendor.pagination.luxury') }}
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6 text-left mb-6">
|
||||
@if($users_list)
|
||||
{{ $users_list->appends(['tab' => 'permissions'])->links('vendor.pagination.luxury') }}
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@else
|
||||
@@ -1166,8 +1216,7 @@
|
||||
:confirm-text="__('Yes, regenerate')"
|
||||
/>
|
||||
|
||||
|
||||
<!-- 5. Machine Permissions Modal (Migrated) -->
|
||||
<!-- Machine Permissions Modal -->
|
||||
<template x-teleport='body'>
|
||||
<div x-show='showPermissionModal' class='fixed inset-0 z-[160] overflow-y-auto' x-cloak>
|
||||
<div class='flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0'>
|
||||
@@ -1179,8 +1228,7 @@
|
||||
|
||||
<span class='hidden sm:inline-block sm:align-middle sm:h-screen'>​</span>
|
||||
|
||||
<div x-show='showPermissionModal'
|
||||
x-transition:enter='ease-out duration-300'
|
||||
<div x-show='showPermissionModal' x-transition:enter='ease-out duration-300'
|
||||
x-transition:enter-start='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
||||
x-transition:enter-end='opacity-100 translate-y-0 sm:scale-100'
|
||||
x-transition:leave='ease-in duration-200'
|
||||
@@ -1190,62 +1238,101 @@
|
||||
|
||||
<div class='flex justify-between items-center mb-8'>
|
||||
<div>
|
||||
<h3 class='text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight'>{{ __('Authorized Machines Management') }}</h3>
|
||||
<div class='flex items-center gap-2 mt-1'>
|
||||
<span class='text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]'>{{ __('Account') }}:</span>
|
||||
<span class='text-xs font-bold text-cyan-500 uppercase tracking-widest' x-text='targetUserName'></span>
|
||||
<h3 class='text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight'>
|
||||
{{ __('Authorized Machines Management') }}</h3>
|
||||
<div class='flex items-center gap-2 mt-1 drop-shadow-sm'>
|
||||
<span class='text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]'>{{
|
||||
__('Account') }}:</span>
|
||||
<span class='text-xs font-bold text-cyan-500 uppercase tracking-widest'
|
||||
x-text='targetUserName'></span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click='showPermissionModal = false' class='text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors bg-slate-50 dark:bg-slate-800 p-2 rounded-xl'>
|
||||
<svg class='size-6' fill='none' stroke='currentColor' viewBox='0 0 24 24'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2.5' d='M6 18L18 6M6 6l12 12' /></svg>
|
||||
<button @click='showPermissionModal = false'
|
||||
class='text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors bg-slate-50 dark:bg-slate-800 p-2 rounded-xl'>
|
||||
<svg class='size-6' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2.5'
|
||||
d='M6 18L18 6M6 6l12 12' />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class='relative min-h-[400px]'>
|
||||
<div class='mb-6'>
|
||||
<div class='relative group'>
|
||||
<div class='mb-6 flex flex-col md:flex-row gap-4 items-center'>
|
||||
<div class='flex-1 relative group w-full text-left'>
|
||||
<span class='absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10'>
|
||||
<svg class='size-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'>
|
||||
<svg class='size-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors'
|
||||
viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5'
|
||||
stroke-linecap='round' stroke-linejoin='round'>
|
||||
<circle cx='11' cy='11' r='8'></circle>
|
||||
<line x1='21' y1='21' x2='16.65' y2='16.65'></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type='text' x-model='permissionSearchQuery' placeholder='{{ __("Search machines...") }}'
|
||||
class='luxury-input py-3 pl-12 pr-6 block w-full text-sm' @click.stop>
|
||||
<input type='text' x-model='permissionSearchQuery'
|
||||
placeholder='{{ __("Search machines...") }}'
|
||||
class='luxury-input py-3 pl-12 pr-6 block w-full text-sm font-extrabold' @click.stop>
|
||||
</div>
|
||||
<button @click="toggleSelectAll()"
|
||||
class="shrink-0 flex items-center gap-2 px-6 py-3 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200 dark:border-slate-700 font-black text-xs uppercase tracking-widest shadow-sm">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<span
|
||||
x-text="allMachines.filter(m => !permissionSearchQuery || m.name.toLowerCase().includes(permissionSearchQuery.toLowerCase()) || m.serial_no.toLowerCase().includes(permissionSearchQuery.toLowerCase())).every(m => permissions[m.id]) ? '{{ __('Deselect All') }}' : '{{ __('Select All') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template x-if='isPermissionsLoading'>
|
||||
<div class='absolute inset-0 flex items-center justify-center bg-white/50 dark:bg-slate-900/50 backdrop-blur-sm z-10 rounded-2xl'>
|
||||
<div
|
||||
class='absolute inset-0 flex items-center justify-center bg-white/50 dark:bg-slate-900/50 backdrop-blur-sm z-[170] rounded-2xl'>
|
||||
<div class='flex flex-col items-center gap-3'>
|
||||
<div class='w-10 h-10 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin'></div>
|
||||
<span class='text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em] animate-pulse'>{{ __('Syncing Permissions...') }}</span>
|
||||
<div
|
||||
class='w-10 h-10 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin'>
|
||||
</div>
|
||||
<span
|
||||
class='text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em] animate-pulse'>{{
|
||||
__('Syncing Permissions...') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[450px] overflow-y-auto pr-2 custom-scrollbar p-1'>
|
||||
<template x-for='machine in allMachines.filter(m => !permissionSearchQuery || m.name.toLowerCase().includes(permissionSearchQuery.toLowerCase()) || m.serial_no.toLowerCase().includes(permissionSearchQuery.toLowerCase()))' :key='machine.id'>
|
||||
<div
|
||||
class='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[450px] overflow-y-auto pr-2 custom-scrollbar p-1'>
|
||||
<template
|
||||
x-for='machine in allMachines.filter(m => !permissionSearchQuery || m.name.toLowerCase().includes(permissionSearchQuery.toLowerCase()) || m.serial_no.toLowerCase().includes(permissionSearchQuery.toLowerCase()))'
|
||||
:key='machine.id'>
|
||||
<div @click='togglePermission(machine.id)'
|
||||
:class='permissions[machine.id] ? "border-cyan-500 bg-cyan-500/5 dark:bg-cyan-500/10 ring-1 ring-cyan-500/20" : "border-slate-100 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-600"'
|
||||
class='p-4 rounded-2xl border-2 cursor-pointer transition-all duration-300 group relative overflow-hidden shadow-sm hover:shadow-md'>
|
||||
<div class='flex flex-col relative z-10'>
|
||||
:class='permissions[machine.id] ? "border-cyan-500 bg-cyan-500/5 dark:bg-cyan-500/10 ring-1 ring-cyan-500/20 shadow-md shadow-cyan-500/10" : "border-slate-100 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-600 shadow-sm"'
|
||||
class='p-4 rounded-2xl border-2 cursor-pointer transition-all duration-300 group relative overflow-hidden'>
|
||||
<div class='flex flex-col relative z-10 text-left'>
|
||||
<div class='flex items-center gap-2'>
|
||||
<div class='size-2 rounded-full' :class='permissions[machine.id] ? "bg-cyan-500" : "bg-slate-300 dark:bg-slate-700"'></div>
|
||||
<span class='text-sm font-extrabold truncate' :class='permissions[machine.id] ? "text-cyan-600 dark:text-cyan-400" : "text-slate-700 dark:text-slate-300"'
|
||||
<div class='size-2 rounded-full'
|
||||
:class='permissions[machine.id] ? "bg-cyan-500 animate-pulse" : "bg-slate-300 dark:bg-slate-700"'>
|
||||
</div>
|
||||
<span class='text-sm font-extrabold truncate drop-shadow-sm'
|
||||
:class='permissions[machine.id] ? "text-cyan-600 dark:text-cyan-400" : "text-slate-700 dark:text-slate-300"'
|
||||
x-text='machine.name'></span>
|
||||
</div>
|
||||
<span class='text-[10px] font-mono font-bold text-slate-400 mt-2 tracking-widest uppercase'
|
||||
<span
|
||||
class='text-[10px] font-mono font-bold text-slate-400 mt-2 tracking-widest uppercase opacity-70'
|
||||
x-text='machine.serial_no'></span>
|
||||
</div>
|
||||
<div class='absolute -right-2 -bottom-2 opacity-[0.03] text-slate-900 dark:text-white pointer-events-none group-hover:scale-110 transition-transform duration-700'>
|
||||
<div
|
||||
class='absolute -right-2 -bottom-2 opacity-[0.03] text-slate-900 dark:text-white pointer-events-none group-hover:scale-110 transition-transform duration-700'>
|
||||
<svg class='size-20' fill='currentColor' viewBox='0 0 24 24'>
|
||||
<path d='M5 2h14c1.1 0 2 .9 2 2v16c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2zm0 2v16h14V4H5zm3 3h8v6H8V7zm0 8h3v2H8v-2zm5 0h3v2h-3v-2z'/>
|
||||
<path
|
||||
d='M5 2h14c1.1 0 2 .9 2 2v16c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2zm0 2v16h14V4H5zm3 3h8v6H8V7zm0 8h3v2H8v-2zm5 0h3v2h-3v-2z' />
|
||||
</svg>
|
||||
</div>
|
||||
<div class='absolute top-4 right-4 animate-luxury-in' x-show='permissions[machine.id]'>
|
||||
<div class='size-5 rounded-full bg-cyan-500 flex items-center justify-center shadow-lg shadow-cyan-500/30'>
|
||||
<svg class='size-3 text-white' fill='none' stroke='currentColor' viewBox='0 0 24 24'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M5 13l4 4L19 7' /></svg>
|
||||
<div class='absolute top-4 right-4 animate-luxury-in'
|
||||
x-show='permissions[machine.id]'>
|
||||
<div
|
||||
class='size-5 rounded-full bg-cyan-500 flex items-center justify-center shadow-lg shadow-cyan-500/30'>
|
||||
<svg class='size-3 text-white' fill='none' stroke='currentColor'
|
||||
viewBox='0 0 24 24'>
|
||||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='3'
|
||||
d='M5 13l4 4L19 7' />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1253,22 +1340,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='flex flex-col sm:flex-row justify-between items-center mt-10 pt-8 border-t border-slate-100 dark:border-slate-800 gap-6'>
|
||||
<div
|
||||
class='flex flex-col sm:flex-row justify-between items-center mt-10 pt-8 border-t border-slate-100 dark:border-slate-800 gap-6'>
|
||||
<div class='flex items-center gap-3'>
|
||||
<div class='flex -space-x-2'>
|
||||
<template x-for='i in Math.min(3, Object.values(permissions).filter(v => v).length)' :key='i'>
|
||||
<div class='size-6 rounded-full border-2 border-white dark:border-slate-900 bg-cyan-500 flex items-center justify-center'>
|
||||
<svg class='size-3 text-white' fill='currentColor' viewBox='0 0 24 24'><path d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14.5v-9l6 4.5-6 4.5z'/></svg>
|
||||
<template x-for='i in Math.min(3, Object.values(permissions).filter(v => v).length)'
|
||||
:key='i'>
|
||||
<div
|
||||
class='size-6 rounded-full border-2 border-white dark:border-slate-900 bg-cyan-500 flex items-center justify-center shadow-sm'>
|
||||
<svg class='size-3 text-white' fill='currentColor' viewBox='0 0 24 24'>
|
||||
<path
|
||||
d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14.5v-9l6 4.5-6 4.5z' />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<p class='text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]'>
|
||||
{{ __('Selection') }}: <span class='text-cyan-500 text-xs' x-text='Object.values(permissions).filter(v => v).length'></span> / <span x-text='allMachines?.length || 0'></span> {{ __('Devices') }}
|
||||
{{ __('Selection') }}: <span class='text-cyan-500 text-xs font-extrabold'
|
||||
x-text='Object.values(permissions).filter(v => v).length'></span> / <span
|
||||
class="font-extrabold" x-text='allMachines?.length || 0'></span> {{ __('Devices') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class='flex gap-4 w-full sm:w-auto'>
|
||||
<button @click='showPermissionModal = false' class='flex-1 sm:flex-none btn-luxury-ghost px-8'>{{ __('Cancel') }}</button>
|
||||
<button @click='savePermissions()' class='flex-1 sm:flex-none btn-luxury-primary px-12' :disabled='isPermissionsLoading'>
|
||||
<button @click='showPermissionModal = false'
|
||||
class='flex-1 sm:flex-none btn-luxury-ghost px-8'>{{ __('Cancel') }}</button>
|
||||
<button @click='savePermissions()' class='flex-1 sm:flex-none btn-luxury-primary px-12 transition-all duration-300 shadow-lg shadow-cyan-500/20'
|
||||
:disabled='isPermissionsLoading'>
|
||||
<span>{{ __('Update Authorization') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1277,6 +1374,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -484,8 +484,20 @@
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||
__('Password') }}</label>
|
||||
<input type="password" name="admin_password" class="luxury-input w-full"
|
||||
placeholder="{{ __('Min 8 characters') }}">
|
||||
<div x-data="{ show: false }" class="relative items-center">
|
||||
<input :type="show ? 'text' : 'password'" name="admin_password" class="luxury-input w-full pr-12"
|
||||
placeholder="{{ __('Min 8 characters') }}">
|
||||
<button type="button" @click="show = !show"
|
||||
class="absolute inset-y-0 end-0 flex items-center z-20 px-4 cursor-pointer text-slate-400 hover:text-cyan-500 transition-colors">
|
||||
<svg x-show="!show" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<svg x-show="show" x-cloak class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
@@ -366,9 +366,21 @@ $roleSelectConfig = [
|
||||
<span class="text-rose-500">*</span>
|
||||
</template>
|
||||
</label>
|
||||
<input type="password" name="password" :required="!editing"
|
||||
class="luxury-input @error('password') border-rose-500 @enderror"
|
||||
placeholder="••••••••">
|
||||
<div x-data="{ show: false }" class="relative items-center">
|
||||
<input :type="show ? 'text' : 'password'" name="password" :required="!editing"
|
||||
class="luxury-input w-full pr-12 @error('password') border-rose-500 @enderror"
|
||||
placeholder="••••••••">
|
||||
<button type="button" @click="show = !show"
|
||||
class="absolute inset-y-0 end-0 flex items-center z-20 px-4 cursor-pointer text-slate-400 hover:text-cyan-500 transition-colors">
|
||||
<svg x-show="!show" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<svg x-show="show" x-cloak class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-x-4 pt-8">
|
||||
|
||||
378
resources/views/admin/machines/permissions.blade.php
Normal file
378
resources/views/admin/machines/permissions.blade.php
Normal file
@@ -0,0 +1,378 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-6 pb-20" x-data="{
|
||||
permissionSearchQuery: '',
|
||||
showPermissionModal: false,
|
||||
isPermissionsLoading: false,
|
||||
targetUserId: null,
|
||||
targetUserName: '',
|
||||
allMachines: [],
|
||||
allMachinesCount: 0,
|
||||
permissions: {},
|
||||
openPermissionModal(user) {
|
||||
this.targetUserId = user.id;
|
||||
this.targetUserName = user.name;
|
||||
this.showPermissionModal = true;
|
||||
this.isPermissionsLoading = true;
|
||||
this.permissions = {};
|
||||
this.allMachines = [];
|
||||
this.permissionSearchQuery = '';
|
||||
|
||||
fetch(`/admin/machines/permissions/accounts/${user.id}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.machines) {
|
||||
this.allMachines = data.machines;
|
||||
this.allMachinesCount = data.machines.length;
|
||||
const tempPermissions = {};
|
||||
data.machines.forEach(m => {
|
||||
tempPermissions[m.id] = (data.assigned_ids || []).includes(m.id);
|
||||
});
|
||||
this.permissions = tempPermissions;
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Failed to load permissions') }}', type: 'error' } }));
|
||||
})
|
||||
.finally(() => {
|
||||
this.isPermissionsLoading = false;
|
||||
});
|
||||
},
|
||||
togglePermission(machineId) {
|
||||
this.permissions = { ...this.permissions, [machineId]: !this.permissions[machineId] };
|
||||
},
|
||||
toggleSelectAll() {
|
||||
const filtered = this.allMachines.filter(m =>
|
||||
!this.permissionSearchQuery ||
|
||||
m.name.toLowerCase().includes(this.permissionSearchQuery.toLowerCase()) ||
|
||||
m.serial_no.toLowerCase().includes(this.permissionSearchQuery.toLowerCase())
|
||||
);
|
||||
if (filtered.length === 0) return;
|
||||
const allSelected = filtered.every(m => this.permissions[m.id]);
|
||||
filtered.forEach(m => this.permissions[m.id] = !allSelected);
|
||||
},
|
||||
savePermissions() {
|
||||
const machineIds = Object.keys(this.permissions).filter(id => this.permissions[id]);
|
||||
|
||||
fetch(`/admin/machines/permissions/accounts/${this.targetUserId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=\'csrf-token\']').content,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ machine_ids: machineIds })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: data.message, type: 'success' } }));
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
} else {
|
||||
throw new Error(data.error || 'Update failed');
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: e.message, type: 'error' } }));
|
||||
});
|
||||
}
|
||||
}">
|
||||
<!-- 1. Header Area -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Machine Permissions') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{
|
||||
__('Manage machine access permissions') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Main Content Card -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<!-- Toolbar & Filters -->
|
||||
<div class="flex flex-col md:flex-row items-center justify-between mb-8 gap-4">
|
||||
<form method="GET" action="{{ route('admin.machines.permissions') }}"
|
||||
class="flex flex-wrap items-center gap-4 w-full md:w-auto">
|
||||
<div class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
placeholder="{{ __('Search accounts...') }}"
|
||||
class="luxury-input py-2.5 pl-12 pr-6 block w-64 text-sm font-bold">
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-72">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
|
||||
:placeholder="__('All Companies')" onchange="this.form.submit()" />
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Account Info') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Company Name') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
|
||||
{{ __('Authorized Machines') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">
|
||||
{{ __('Action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($users_list as $user)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6 font-display">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{
|
||||
$user->name }}</span>
|
||||
<span
|
||||
class="text-xs font-mono font-bold text-slate-500 tracking-widest uppercase">{{
|
||||
$user->username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest uppercase">
|
||||
{{ $user->company->name ?? __('System') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div
|
||||
class="flex flex-wrap gap-2 justify-center lg:justify-start max-w-[420px] mx-auto lg:mx-0 max-h-[140px] overflow-y-auto pr-2 custom-scrollbar py-1">
|
||||
@forelse($user->machines as $m)
|
||||
<div
|
||||
class="flex flex-col px-3 py-1.5 rounded-xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-white/5 hover:border-cyan-500/30 transition-all duration-300">
|
||||
<span class="text-[11px] font-black text-slate-700 dark:text-slate-200 leading-tight">{{
|
||||
$m->name }}</span>
|
||||
<span class="text-[9px] font-mono font-bold text-cyan-500 tracking-tighter mt-0.5 opacity-80">{{
|
||||
$m->serial_no }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<div class="w-full text-center lg:text-left">
|
||||
<span
|
||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest opacity-40 italic">--
|
||||
{{ __('None') }} --</span>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<button
|
||||
@click="openPermissionModal({{ json_encode(['id' => $user->id, 'name' => $user->name]) }})"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500 hover:text-white transition-all duration-300 text-xs font-black uppercase tracking-widest shadow-sm shadow-cyan-500/5 group/auth">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 00-2 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<span>{{ __('Authorize') }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-24 text-center">
|
||||
<div class="flex flex-col items-center gap-3 opacity-20">
|
||||
<svg class="size-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" />
|
||||
</svg>
|
||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No
|
||||
accounts found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $users_list->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Machine Permissions Modal -->
|
||||
<template x-teleport='body'>
|
||||
<div x-show='showPermissionModal' class='fixed inset-0 z-[160] overflow-y-auto' x-cloak>
|
||||
<div class='flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0'>
|
||||
<div x-show='showPermissionModal' @click='showPermissionModal = false'
|
||||
x-transition:enter='ease-out duration-300' x-transition:enter-start='opacity-0'
|
||||
x-transition:enter-end='opacity-100' x-transition:leave='ease-in duration-200'
|
||||
x-transition:leave-start='opacity-100' x-transition:leave-end='opacity-0'
|
||||
class='fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity'></div>
|
||||
|
||||
<span class='hidden sm:inline-block sm:align-middle sm:h-screen'>​</span>
|
||||
|
||||
<div x-show='showPermissionModal' x-transition:enter='ease-out duration-300'
|
||||
x-transition:enter-start='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
||||
x-transition:enter-end='opacity-100 translate-y-0 sm:scale-100'
|
||||
x-transition:leave='ease-in duration-200'
|
||||
x-transition:leave-start='opacity-100 translate-y-0 sm:scale-100'
|
||||
x-transition:leave-end='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
||||
class='inline-block px-8 py-10 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-4xl sm:w-full overflow-hidden animate-luxury-in'>
|
||||
|
||||
<div class='flex justify-between items-center mb-8'>
|
||||
<div>
|
||||
<h3 class='text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight'>
|
||||
{{ __('Authorized Machines Management') }}</h3>
|
||||
<div class='flex items-center gap-2 mt-1'>
|
||||
<span class='text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]'>{{
|
||||
__('Account') }}:</span>
|
||||
<span class='text-xs font-bold text-cyan-500 uppercase tracking-widest'
|
||||
x-text='targetUserName'></span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click='showPermissionModal = false'
|
||||
class='text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors bg-slate-50 dark:bg-slate-800 p-2 rounded-xl'>
|
||||
<svg class='size-6' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2.5'
|
||||
d='M6 18L18 6M6 6l12 12' />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class='relative min-h-[400px]'>
|
||||
<div class='mb-6 flex flex-col md:flex-row gap-4 items-center'>
|
||||
<div class='flex-1 relative group w-full'>
|
||||
<span class='absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10'>
|
||||
<svg class='size-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors'
|
||||
viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5'
|
||||
stroke-linecap='round' stroke-linejoin='round'>
|
||||
<circle cx='11' cy='11' r='8'></circle>
|
||||
<line x1='21' y1='21' x2='16.65' y2='16.65'></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type='text' x-model='permissionSearchQuery'
|
||||
placeholder='{{ __("Search machines...") }}'
|
||||
class='luxury-input py-3 pl-12 pr-6 block w-full text-sm font-bold' @click.stop>
|
||||
</div>
|
||||
<button @click="toggleSelectAll()"
|
||||
class="shrink-0 flex items-center gap-2 px-6 py-3 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200 dark:border-slate-700 font-black text-xs uppercase tracking-widest">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<span
|
||||
x-text="allMachines.filter(m => !permissionSearchQuery || m.name.toLowerCase().includes(permissionSearchQuery.toLowerCase()) || m.serial_no.toLowerCase().includes(permissionSearchQuery.toLowerCase())).every(m => permissions[m.id]) ? '{{ __('Deselect All') }}' : '{{ __('Select All') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template x-if='isPermissionsLoading'>
|
||||
<div
|
||||
class='absolute inset-0 flex items-center justify-center bg-white/50 dark:bg-slate-900/50 backdrop-blur-sm z-10 rounded-2xl'>
|
||||
<div class='flex flex-col items-center gap-3'>
|
||||
<div
|
||||
class='w-10 h-10 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin'>
|
||||
</div>
|
||||
<span
|
||||
class='text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em] animate-pulse'>{{
|
||||
__('Syncing Permissions...') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[450px] overflow-y-auto pr-2 custom-scrollbar p-1'>
|
||||
<template
|
||||
x-for='machine in allMachines.filter(m => !permissionSearchQuery || m.name.toLowerCase().includes(permissionSearchQuery.toLowerCase()) || m.serial_no.toLowerCase().includes(permissionSearchQuery.toLowerCase()))'
|
||||
:key='machine.id'>
|
||||
<div @click='togglePermission(machine.id)'
|
||||
:class='permissions[machine.id] ? "border-cyan-500 bg-cyan-500/5 dark:bg-cyan-500/10 ring-1 ring-cyan-500/20" : "border-slate-100 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-600"'
|
||||
class='p-4 rounded-2xl border-2 cursor-pointer transition-all duration-300 group relative overflow-hidden shadow-sm hover:shadow-md'>
|
||||
<div class='flex flex-col relative z-10'>
|
||||
<div class='flex items-center gap-2'>
|
||||
<div class='size-2 rounded-full'
|
||||
:class='permissions[machine.id] ? "bg-cyan-500" : "bg-slate-300 dark:bg-slate-700"'>
|
||||
</div>
|
||||
<span class='text-sm font-extrabold truncate'
|
||||
:class='permissions[machine.id] ? "text-cyan-600 dark:text-cyan-400" : "text-slate-700 dark:text-slate-300"'
|
||||
x-text='machine.name'></span>
|
||||
</div>
|
||||
<span
|
||||
class='text-[10px] font-mono font-bold text-slate-400 mt-2 tracking-widest uppercase'
|
||||
x-text='machine.serial_no'></span>
|
||||
</div>
|
||||
<div
|
||||
class='absolute -right-2 -bottom-2 opacity-[0.03] text-slate-900 dark:text-white pointer-events-none group-hover:scale-110 transition-transform duration-700'>
|
||||
<svg class='size-20' fill='currentColor' viewBox='0 0 24 24'>
|
||||
<path
|
||||
d='M5 2h14c1.1 0 2 .9 2 2v16c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2zm0 2v16h14V4H5zm3 3h8v6H8V7zm0 8h3v2H8v-2zm5 0h3v2h-3v-2z' />
|
||||
</svg>
|
||||
</div>
|
||||
<div class='absolute top-4 right-4 animate-luxury-in'
|
||||
x-show='permissions[machine.id]'>
|
||||
<div
|
||||
class='size-5 rounded-full bg-cyan-500 flex items-center justify-center shadow-lg shadow-cyan-500/30'>
|
||||
<svg class='size-3 text-white' fill='none' stroke='currentColor'
|
||||
viewBox='0 0 24 24'>
|
||||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='3'
|
||||
d='M5 13l4 4L19 7' />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class='flex flex-col sm:flex-row justify-between items-center mt-10 pt-8 border-t border-slate-100 dark:border-slate-800 gap-6'>
|
||||
<div class='flex items-center gap-3'>
|
||||
<div class='flex -space-x-2'>
|
||||
<template x-for='i in Math.min(3, Object.values(permissions).filter(v => v).length)'
|
||||
:key='i'>
|
||||
<div
|
||||
class='size-6 rounded-full border-2 border-white dark:border-slate-900 bg-cyan-500 flex items-center justify-center'>
|
||||
<svg class='size-3 text-white' fill='currentColor' viewBox='0 0 24 24'>
|
||||
<path
|
||||
d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14.5v-9l6 4.5-6 4.5z' />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<p class='text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]'>
|
||||
{{ __('Selection') }}: <span class='text-cyan-500 text-xs'
|
||||
x-text='Object.values(permissions).filter(v => v).length'></span> / <span
|
||||
x-text='allMachines?.length || 0'></span> {{ __('Devices') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class='flex gap-4 w-full sm:w-auto'>
|
||||
<button @click='showPermissionModal = false'
|
||||
class='flex-1 sm:flex-none btn-luxury-ghost px-8'>{{ __('Cancel') }}</button>
|
||||
<button @click='savePermissions()' class='flex-1 sm:flex-none btn-luxury-primary px-12'
|
||||
:disabled='isPermissionsLoading'>
|
||||
<span>{{ __('Update Authorization') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -74,7 +74,7 @@
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Time') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine Information') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Company') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Category') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Engineer') }}</th>
|
||||
@@ -89,8 +89,8 @@
|
||||
</td>
|
||||
<td class="px-6 py-6 cursor-pointer group/cell" @click="openDetail({{ $record->load('machine', 'user', 'company')->toJson() }})">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-black text-slate-800 dark:text-slate-100 group-hover/cell:text-cyan-600 transition-colors">{{ $record->machine->name }}</span>
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest group-hover/cell:text-cyan-500/60 transition-colors">{{ $record->machine->serial_no }}</span>
|
||||
<span class="text-sm font-black text-slate-800 dark:text-slate-100 group-hover/cell:text-cyan-600 transition-colors">{{ $record->machine->name ?? 'N/A' }}</span>
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest group-hover/cell:text-cyan-500/60 transition-colors">{{ $record->machine->serial_no ?? 'N/A' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
<div class="space-y-2">
|
||||
<label class="text-[11px] font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Role Name') }}</label>
|
||||
<input type="text" name="name" value="{{ old('name', $role->name) }}" required
|
||||
class="luxury-input w-full @error('name') border-rose-500 @enderror"
|
||||
class="luxury-input w-full @error('name') border-rose-500 @enderror @if($role->name === 'super-admin') bg-slate-50 dark:bg-slate-800/50 cursor-not-allowed @endif"
|
||||
placeholder="{{ __('Enter role name') }}"
|
||||
{{ $role->name === 'super-admin' ? 'readonly' : '' }}>
|
||||
@error('name')
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-8">
|
||||
<div class="space-y-3">
|
||||
<label class="text-xs font-black text-emerald-500 uppercase tracking-widest pl-1">{{ __('Retail Price') }} <span class="text-rose-500">*</span></label>
|
||||
<label class="text-xs font-black text-emerald-500 uppercase tracking-widest pl-1">{{ __('Sale Price') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-emerald-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.price = Math.max(0, parseInt(formData.price || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@php
|
||||
$names = [];
|
||||
foreach(['zh_TW', 'en', 'ja'] as $locale) {
|
||||
$names[$locale] = $product->translations->where('locale', $locale)->first()?->text ?? '';
|
||||
$names[$locale] = $product->translations->where('locale', $locale)->first()?->value ?? '';
|
||||
}
|
||||
// If zh_TW translation is empty, fallback to product->name
|
||||
if (empty($names['zh_TW'])) {
|
||||
@@ -207,7 +207,7 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-8">
|
||||
<div class="space-y-3">
|
||||
<label class="text-xs font-black text-emerald-500 uppercase tracking-widest pl-1">{{ __('Retail Price') }} <span class="text-rose-500">*</span></label>
|
||||
<label class="text-xs font-black text-emerald-500 uppercase tracking-widest pl-1">{{ __('Sale Price') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-emerald-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.price = Math.max(0, parseInt(formData.price || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
|
||||
@@ -71,7 +71,7 @@ $roleSelectConfig = [
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Company') }}</th>
|
||||
@endif
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Price / Member') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Sale Price') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Channel Limits (Track/Spring)') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
||||
@@ -90,7 +90,7 @@ $roleSelectConfig = [
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $product->name }}</span>
|
||||
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $product->localized_name }}</span>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
@php
|
||||
$catName = $product->category->name ?? __('Uncategorized');
|
||||
@@ -236,7 +236,7 @@ $roleSelectConfig = [
|
||||
<div class="px-6 py-3 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between sticky top-0 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md z-20">
|
||||
<div>
|
||||
<h2 class="text-xl font-black text-slate-800 dark:text-white">{{ __('Product Details') }}</h2>
|
||||
<p class="text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] mt-1" x-text="selectedProduct?.name + ' (' + getCategoryName(selectedProduct?.category_id) + ')'"></p>
|
||||
<p class="text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] mt-1" x-text="(selectedProduct?.localized_name || selectedProduct?.name) + ' (' + getCategoryName(selectedProduct?.category_id) + ')'"></p>
|
||||
</div>
|
||||
<button @click="isDetailOpen = false"
|
||||
class="p-2 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
@@ -281,6 +281,10 @@ $roleSelectConfig = [
|
||||
<span class="text-xs font-bold text-slate-400 uppercase tracking-widest block mb-1.5">{{ __('Barcode') }}</span>
|
||||
<div class="text-[15px] font-mono font-bold text-slate-700 dark:text-slate-200" x-text="selectedProduct?.barcode || '-'"></div>
|
||||
</div>
|
||||
<div class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80 group hover:border-cyan-500/30 transition-colors">
|
||||
<span class="text-xs font-bold text-slate-400 uppercase tracking-widest block mb-1.5">{{ __('Specification') }}</span>
|
||||
<div class="text-[15px] font-bold text-slate-700 dark:text-slate-200" x-text="selectedProduct?.spec || '-'"></div>
|
||||
</div>
|
||||
<template x-if="selectedProduct?.metadata?.material_code">
|
||||
<div class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80 group hover:border-cyan-500/30 transition-colors">
|
||||
<span class="text-xs font-bold text-slate-400 uppercase tracking-widest block mb-1.5">{{ __('Material Code') }}</span>
|
||||
@@ -305,7 +309,7 @@ $roleSelectConfig = [
|
||||
<h3 class="text-xs font-black text-emerald-500 uppercase tracking-[0.3em]">{{ __('Pricing Information') }}</h3>
|
||||
<div class="luxury-card divide-y divide-slate-50 dark:divide-white/5 overflow-hidden border border-slate-100 dark:border-white/5 shadow-sm">
|
||||
<div class="p-5 flex items-center justify-between group hover:bg-slate-50/50 dark:hover:bg-white/5 transition-colors">
|
||||
<span class="text-[15px] font-bold text-slate-500">{{ __('Retail Price') }}</span>
|
||||
<span class="text-[15px] font-bold text-slate-500">{{ __('Sale Price') }}</span>
|
||||
<span class="text-lg font-black text-slate-800 dark:text-white">$<span x-text="formatNumber(selectedProduct?.price)"></span></span>
|
||||
</div>
|
||||
<div class="p-5 flex items-center justify-between group hover:bg-slate-50/50 dark:hover:bg-white/5 transition-colors">
|
||||
@@ -387,7 +391,7 @@ $roleSelectConfig = [
|
||||
<img :src="selectedProduct?.image_url" class="max-w-full max-h-full rounded-[2.5rem] shadow-2xl border border-white/10 animate-luxury-in">
|
||||
|
||||
<div class="absolute bottom-[-4rem] left-1/2 -translate-x-1/2 text-white/60 text-sm font-bold tracking-widest uppercase animate-luxury-in" style="animation-delay: 200ms">
|
||||
<span x-text="selectedProduct?.name"></span>
|
||||
<span x-text="selectedProduct?.localized_name || selectedProduct?.name"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,17 +83,24 @@
|
||||
<!-- End Form Group -->
|
||||
|
||||
<!-- Form Group -->
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<label for="password" class="block text-sm mb-2 dark:text-white">密碼</label>
|
||||
<div x-data="{ showPassword: false }">
|
||||
<div class="flex items-center justify-between">
|
||||
<label for="password" class="block text-sm mb-2 dark:text-white font-bold">密碼</label>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input type="password" id="password" name="password" class="py-3 px-4 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-slate-900 dark:border-slate-700 dark:text-gray-400 dark:focus:ring-gray-600" required autocomplete="current-password">
|
||||
<div class="hidden absolute inset-y-0 end-0 flex items-center pointer-events-none pe-3">
|
||||
<svg class="h-5 w-5 text-red-500" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/>
|
||||
<input :type="showPassword ? 'text' : 'password'" id="password" name="password"
|
||||
class="py-3 px-4 block w-full border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-900 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 dark:text-gray-400 dark:focus:ring-gray-600 transition-all pr-12"
|
||||
required autocomplete="current-password">
|
||||
<button type="button" @click="showPassword = !showPassword"
|
||||
class="absolute inset-y-0 end-0 flex items-center z-20 px-4 cursor-pointer text-gray-400 hover:text-blue-600 transition-colors">
|
||||
<svg x-show="!showPassword" class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<svg x-show="showPassword" x-cloak class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@if ($errors->get('password'))
|
||||
<p class="text-xs text-red-600 mt-2" id="password-error">{{ $errors->first('password') }}</p>
|
||||
|
||||
@@ -12,21 +12,42 @@
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<div class="mt-4" x-data="{ show: false }">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||
<div class="relative items-center mt-1">
|
||||
<x-text-input id="password" class="block w-full pr-12" :type="show ? 'text' : 'password'" name="password" required autocomplete="new-password" />
|
||||
<button type="button" @click="show = !show"
|
||||
class="absolute inset-y-0 end-0 flex items-center z-20 px-4 cursor-pointer text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<svg x-show="!show" class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<svg x-show="show" x-cloak class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mt-4">
|
||||
<div class="mt-4" x-data="{ show: false }">
|
||||
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||
|
||||
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password_confirmation" required autocomplete="new-password" />
|
||||
|
||||
<div class="relative items-center mt-1">
|
||||
<x-text-input id="password_confirmation" class="block w-full pr-12"
|
||||
:type="show ? 'text' : 'password'"
|
||||
name="password_confirmation" required autocomplete="new-password" />
|
||||
<button type="button" @click="show = !show"
|
||||
class="absolute inset-y-0 end-0 flex items-center z-20 px-4 cursor-pointer text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<svg x-show="!show" class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<svg x-show="show" x-cloak class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -64,6 +64,13 @@
|
||||
</a></li>
|
||||
@endcan
|
||||
|
||||
@can('menu.machines.permissions')
|
||||
<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') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
||||
{{ __('Machine Permissions') }}
|
||||
</a></li>
|
||||
@endcan
|
||||
|
||||
@can('menu.machines.utilization')
|
||||
<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') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>
|
||||
|
||||
@@ -13,22 +13,58 @@
|
||||
@csrf
|
||||
@method('put')
|
||||
|
||||
<div>
|
||||
<div x-data="{ show: false }">
|
||||
<x-input-label for="update_password_current_password" :value="__('Current Password')" class="text-xs font-black text-slate-500 uppercase tracking-widest mb-2 ml-1" />
|
||||
<input id="update_password_current_password" name="current_password" type="password" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" autocomplete="current-password" />
|
||||
<div class="relative items-center">
|
||||
<input id="update_password_current_password" name="current_password" :type="show ? 'text' : 'password'" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none pr-12" autocomplete="current-password" />
|
||||
<button type="button" @click="show = !show"
|
||||
class="absolute inset-y-0 end-0 flex items-center z-20 px-4 cursor-pointer text-slate-400 hover:text-cyan-500 transition-colors">
|
||||
<svg x-show="!show" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<svg x-show="show" x-cloak class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div x-data="{ show: false }">
|
||||
<x-input-label for="update_password_password" :value="__('New Password')" class="text-xs font-black text-slate-500 uppercase tracking-widest mb-2 ml-1" />
|
||||
<input id="update_password_password" name="password" type="password" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" autocomplete="new-password" />
|
||||
<div class="relative items-center">
|
||||
<input id="update_password_password" name="password" :type="show ? 'text' : 'password'" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none pr-12" autocomplete="new-password" />
|
||||
<button type="button" @click="show = !show"
|
||||
class="absolute inset-y-0 end-0 flex items-center z-20 px-4 cursor-pointer text-slate-400 hover:text-cyan-500 transition-colors">
|
||||
<svg x-show="!show" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<svg x-show="show" x-cloak class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div x-data="{ show: false }">
|
||||
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" class="text-xs font-black text-slate-500 uppercase tracking-widest mb-2 ml-1" />
|
||||
<input id="update_password_password_confirmation" name="password_confirmation" type="password" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" autocomplete="new-password" />
|
||||
<div class="relative items-center">
|
||||
<input id="update_password_password_confirmation" name="password_confirmation" :type="show ? 'text' : 'password'" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none pr-12" autocomplete="new-password" />
|
||||
<button type="button" @click="show = !show"
|
||||
class="absolute inset-y-0 end-0 flex items-center z-20 px-4 cursor-pointer text-slate-400 hover:text-cyan-500 transition-colors">
|
||||
<svg x-show="!show" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<svg x-show="show" x-cloak class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,10 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
Route::resource('gift-definitions', App\Http\Controllers\Admin\GiftDefinitionController::class)->except(['show', 'create', 'edit']);
|
||||
|
||||
Route::prefix('machines')->name('machines.')->group(function () {
|
||||
// Route::get('/permissions', [App\Http\Controllers\Admin\MachineController::class , 'permissions'])->name('permissions'); // Merged into Sub-account Management
|
||||
Route::get('/permissions', [App\Http\Controllers\Admin\Machine\MachinePermissionController::class, 'index'])->name('permissions')->middleware('can:menu.machines.permissions');
|
||||
Route::get('/permissions/accounts/{user}', [App\Http\Controllers\Admin\Machine\MachinePermissionController::class, 'getAccountMachines'])->name('permissions.accounts.get');
|
||||
Route::post('/permissions/accounts/{user}', [App\Http\Controllers\Admin\Machine\MachinePermissionController::class, 'syncAccountMachines'])->name('permissions.accounts.sync');
|
||||
|
||||
Route::get('/utilization', [App\Http\Controllers\Admin\MachineController::class , 'utilization'])->name('utilization');
|
||||
Route::get('/utilization-ajax/{id?}', [App\Http\Controllers\Admin\MachineController::class, 'utilizationData'])->name('utilization-ajax');
|
||||
Route::get('/{machine}/slots-ajax', [App\Http\Controllers\Admin\MachineController::class, 'slotsAjax'])->name('slots-ajax');
|
||||
@@ -186,9 +189,7 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
Route::post('/', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'store'])->name('store');
|
||||
Route::post('/{machine}/regenerate-token', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'regenerateToken'])->name('regenerate-token');
|
||||
|
||||
// 權限管理 (從 MachineController 遷移)
|
||||
Route::get('/permissions/accounts/{user}', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'getAccountMachines'])->name('permissions.accounts.get');
|
||||
Route::post('/permissions/accounts/{user}', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'syncAccountMachines'])->name('permissions.accounts.sync');
|
||||
Route::post('/{machine}/regenerate-token', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'regenerateToken'])->name('regenerate-token');
|
||||
});
|
||||
|
||||
// 客戶金流設定
|
||||
|
||||
Reference in New Issue
Block a user