[FEAT] 重構機台狀態判定邏輯並優化全站多語系支援
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m18s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m18s
1. 重構機台在線狀態判定機制:移除資料庫 status 欄位,改由 Model 根據心跳時間動態計算。 2. 修正儀表板 (Dashboard) 與機台管理頁面的多語系顯示問題,解決換行導致翻譯失效的 Bug。 3. 修正個人檔案頁面的麵包屑 (Breadcrumbs) 導航,補齊「個人設定」層級。 4. 更新 IoT API (B010, B600) 的認證機制與日誌處理邏輯。 5. 同步更新繁中、英文、日文語言檔,確保 UI 標籤一致性。
This commit is contained in:
@@ -105,7 +105,6 @@ class MachineSettingController extends AdminController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$machine = Machine::create(array_merge($validated, [
|
$machine = Machine::create(array_merge($validated, [
|
||||||
'status' => 'offline',
|
|
||||||
'api_token' => \Illuminate\Support\Str::random(60),
|
'api_token' => \Illuminate\Support\Str::random(60),
|
||||||
'creator_id' => auth()->id(),
|
'creator_id' => auth()->id(),
|
||||||
'updater_id' => auth()->id(),
|
'updater_id' => auth()->id(),
|
||||||
|
|||||||
@@ -12,28 +12,31 @@ class DashboardController extends Controller
|
|||||||
{
|
{
|
||||||
// 每頁顯示筆數限制 (預設為 10)
|
// 每頁顯示筆數限制 (預設為 10)
|
||||||
$perPage = (int) request()->input('per_page', 10);
|
$perPage = (int) request()->input('per_page', 10);
|
||||||
if ($perPage <= 0) $perPage = 10;
|
if ($perPage <= 0)
|
||||||
|
$perPage = 10;
|
||||||
|
|
||||||
// 從資料庫獲取真實統計數據
|
// 從資料庫獲取真實統計數據
|
||||||
$totalRevenue = \App\Models\Member\MemberWallet::sum('balance');
|
$totalRevenue = \App\Models\Member\MemberWallet::sum('balance');
|
||||||
$activeMachines = Machine::where('status', 'online')->count();
|
$activeMachines = Machine::online()->count();
|
||||||
$alertsPending = Machine::where('status', 'error')->count();
|
$offlineMachines = Machine::offline()->count();
|
||||||
|
$alertsPending = Machine::hasError()->count();
|
||||||
$memberCount = \App\Models\Member\Member::count();
|
$memberCount = \App\Models\Member\Member::count();
|
||||||
|
|
||||||
// 獲取機台列表 (分頁)
|
// 獲取機台列表 (分頁)
|
||||||
$machines = Machine::when($request->search, function($query, $search) {
|
$machines = Machine::when($request->search, function ($query, $search) {
|
||||||
$query->where(function($q) use ($search) {
|
$query->where(function ($q) use ($search) {
|
||||||
$q->where('name', 'like', "%{$search}%")
|
$q->where('name', 'like', "%{$search}%")
|
||||||
->orWhere('serial_no', 'like', "%{$search}%");
|
->orWhere('serial_no', 'like', "%{$search}%");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
->latest()
|
->orderByDesc('last_heartbeat_at')
|
||||||
->paginate($perPage)
|
->paginate($perPage)
|
||||||
->withQueryString();
|
->withQueryString();
|
||||||
|
|
||||||
return view('admin.dashboard', compact(
|
return view('admin.dashboard', compact(
|
||||||
'totalRevenue',
|
'totalRevenue',
|
||||||
'activeMachines',
|
'activeMachines',
|
||||||
|
'offlineMachines',
|
||||||
'alertsPending',
|
'alertsPending',
|
||||||
'memberCount',
|
'memberCount',
|
||||||
'machines'
|
'machines'
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ use Illuminate\View\View;
|
|||||||
|
|
||||||
class MachineController extends AdminController
|
class MachineController extends AdminController
|
||||||
{
|
{
|
||||||
public function __construct(protected MachineService $machineService) {}
|
public function __construct(protected MachineService $machineService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public function index(Request $request): View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
@@ -34,14 +36,31 @@ class MachineController extends AdminController
|
|||||||
return view('admin.machines.index', compact('machines'));
|
return view('admin.machines.index', compact('machines'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新機台基本資訊 (目前僅名稱)
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Machine $machine)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$machine->update($validated);
|
||||||
|
|
||||||
|
return redirect()->route('admin.machines.index')
|
||||||
|
->with('success', __('Machine updated successfully.'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 顯示特定機台的日誌與詳細資訊
|
* 顯示特定機台的日誌與詳細資訊
|
||||||
*/
|
*/
|
||||||
public function show(int $id): View
|
public function show(int $id): View
|
||||||
{
|
{
|
||||||
$machine = Machine::with(['logs' => function ($query) {
|
$machine = Machine::with([
|
||||||
|
'logs' => function ($query) {
|
||||||
$query->latest()->limit(50);
|
$query->latest()->limit(50);
|
||||||
}])->findOrFail($id);
|
}
|
||||||
|
])->findOrFail($id);
|
||||||
|
|
||||||
return view('admin.machines.show', compact('machine'));
|
return view('admin.machines.show', compact('machine'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class MachineController extends Controller
|
|||||||
public function heartbeat(Request $request)
|
public function heartbeat(Request $request)
|
||||||
{
|
{
|
||||||
$machine = $request->get('machine');
|
$machine = $request->get('machine');
|
||||||
$data = $request->all();
|
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入的 Model 物件與認證 key
|
||||||
|
|
||||||
// 異步處理狀態更新
|
// 異步處理狀態更新
|
||||||
ProcessHeartbeat::dispatch($machine->serial_no, $data);
|
ProcessHeartbeat::dispatch($machine->serial_no, $data);
|
||||||
@@ -84,7 +84,7 @@ class MachineController extends Controller
|
|||||||
public function recordRestock(Request $request)
|
public function recordRestock(Request $request)
|
||||||
{
|
{
|
||||||
$machine = $request->get('machine');
|
$machine = $request->get('machine');
|
||||||
$data = $request->all();
|
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入物件
|
||||||
$data['serial_no'] = $machine->serial_no;
|
$data['serial_no'] = $machine->serial_no;
|
||||||
|
|
||||||
\App\Jobs\Machine\ProcessRestockReport::dispatch($data);
|
\App\Jobs\Machine\ProcessRestockReport::dispatch($data);
|
||||||
@@ -135,7 +135,7 @@ class MachineController extends Controller
|
|||||||
public function syncTimer(Request $request)
|
public function syncTimer(Request $request)
|
||||||
{
|
{
|
||||||
$machine = $request->get('machine');
|
$machine = $request->get('machine');
|
||||||
$data = $request->all();
|
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入物件
|
||||||
|
|
||||||
ProcessTimerStatus::dispatch($machine->serial_no, $data);
|
ProcessTimerStatus::dispatch($machine->serial_no, $data);
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ class MachineController extends Controller
|
|||||||
public function syncCoinInventory(Request $request)
|
public function syncCoinInventory(Request $request)
|
||||||
{
|
{
|
||||||
$machine = $request->get('machine');
|
$machine = $request->get('machine');
|
||||||
$data = $request->all();
|
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入物件
|
||||||
|
|
||||||
ProcessCoinInventory::dispatch($machine->serial_no, $data);
|
ProcessCoinInventory::dispatch($machine->serial_no, $data);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class TransactionController extends Controller
|
|||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
$machine = $request->get('machine');
|
$machine = $request->get('machine');
|
||||||
$data = $request->all();
|
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入物件
|
||||||
$data['serial_no'] = $machine->serial_no;
|
$data['serial_no'] = $machine->serial_no;
|
||||||
|
|
||||||
ProcessTransaction::dispatch($data);
|
ProcessTransaction::dispatch($data);
|
||||||
@@ -34,7 +34,7 @@ class TransactionController extends Controller
|
|||||||
public function recordInvoice(Request $request)
|
public function recordInvoice(Request $request)
|
||||||
{
|
{
|
||||||
$machine = $request->get('machine');
|
$machine = $request->get('machine');
|
||||||
$data = $request->all();
|
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入物件
|
||||||
$data['serial_no'] = $machine->serial_no;
|
$data['serial_no'] = $machine->serial_no;
|
||||||
|
|
||||||
ProcessInvoice::dispatch($data);
|
ProcessInvoice::dispatch($data);
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class Machine extends Model
|
|||||||
'serial_no',
|
'serial_no',
|
||||||
'model',
|
'model',
|
||||||
'location',
|
'location',
|
||||||
'status',
|
|
||||||
'current_page',
|
'current_page',
|
||||||
'door_status',
|
'door_status',
|
||||||
'temperature',
|
'temperature',
|
||||||
@@ -69,7 +68,88 @@ class Machine extends Model
|
|||||||
'updater_id',
|
'updater_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $appends = ['image_urls'];
|
protected $appends = ['image_urls', 'calculated_status'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 動態計算機台當前狀態
|
||||||
|
* 1. 離線 (offline):超過 30 秒未收到心跳
|
||||||
|
* 2. 異常 (error):在線但過去 15 分鐘內有錯誤/警告日誌
|
||||||
|
* 3. 在線 (online):正常在線
|
||||||
|
*/
|
||||||
|
public function getCalculatedStatusAttribute(): string
|
||||||
|
{
|
||||||
|
// 判定離線
|
||||||
|
if (!$this->last_heartbeat_at || $this->last_heartbeat_at->diffInSeconds(now()) >= 30) {
|
||||||
|
return 'offline';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判定異常 (檢查過去 15 分鐘內是否有 error 或 warning 日誌)
|
||||||
|
$hasRecentErrors = $this->logs()
|
||||||
|
->whereIn('level', ['error', 'warning'])
|
||||||
|
->where('created_at', '>=', now()->subMinutes(15))
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($hasRecentErrors) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'online';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: 判定在線 (30 秒內有心跳)
|
||||||
|
*/
|
||||||
|
public function scopeOnline($query)
|
||||||
|
{
|
||||||
|
return $query->where('last_heartbeat_at', '>=', now()->subSeconds(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: 判定離線 (超過 30 秒未收到心跳或從未收到心跳)
|
||||||
|
*/
|
||||||
|
public function scopeOffline($query)
|
||||||
|
{
|
||||||
|
return $query->where(function ($q) {
|
||||||
|
$q->whereNull('last_heartbeat_at')
|
||||||
|
->orWhere('last_heartbeat_at', '<', now()->subSeconds(30));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: 判定異常 (過去 15 分鐘內有錯誤或警告日誌)
|
||||||
|
*/
|
||||||
|
public function scopeHasError($query)
|
||||||
|
{
|
||||||
|
return $query->whereExists(function ($q) {
|
||||||
|
$q->select(\Illuminate\Support\Facades\DB::raw(1))
|
||||||
|
->from('machine_logs')
|
||||||
|
->whereColumn('machine_logs.machine_id', 'machines.id')
|
||||||
|
->whereIn('level', ['error', 'warning'])
|
||||||
|
->where('created_at', '>=', now()->subMinutes(15));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: 判定運行中 (在線且無近期異常)
|
||||||
|
*/
|
||||||
|
public function scopeRunning($query)
|
||||||
|
{
|
||||||
|
return $query->online()->whereNotExists(function ($q) {
|
||||||
|
$q->select(\Illuminate\Support\Facades\DB::raw(1))
|
||||||
|
->from('machine_logs')
|
||||||
|
->whereColumn('machine_logs.machine_id', 'machines.id')
|
||||||
|
->whereIn('level', ['error', 'warning'])
|
||||||
|
->where('created_at', '>=', now()->subMinutes(15));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: 判定異常在線 (在線且有近期異常)
|
||||||
|
*/
|
||||||
|
public function scopeErrorOnline($query)
|
||||||
|
{
|
||||||
|
return $query->online()->hasError();
|
||||||
|
}
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'last_heartbeat_at' => 'datetime',
|
'last_heartbeat_at' => 'datetime',
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ class MachineService
|
|||||||
$model = $data['model'] ?? $machine->model;
|
$model = $data['model'] ?? $machine->model;
|
||||||
|
|
||||||
$updateData = [
|
$updateData = [
|
||||||
'status' => 'online',
|
|
||||||
'temperature' => $temperature,
|
'temperature' => $temperature,
|
||||||
'current_page' => $currentPage,
|
'current_page' => $currentPage,
|
||||||
'door_status' => $doorStatus,
|
'door_status' => $doorStatus,
|
||||||
@@ -176,10 +175,10 @@ class MachineService
|
|||||||
$start = Carbon::parse($date)->startOfDay();
|
$start = Carbon::parse($date)->startOfDay();
|
||||||
$end = Carbon::parse($date)->endOfDay();
|
$end = Carbon::parse($date)->endOfDay();
|
||||||
|
|
||||||
// 1. Online Count (Base on current status)
|
// 1. Online Count (Base on new heartbeat logic)
|
||||||
$machines = Machine::all(); // This is filtered by TenantScoped
|
$machines = Machine::all(); // This is filtered by TenantScoped
|
||||||
$totalMachines = $machines->count();
|
$totalMachines = $machines->count();
|
||||||
$onlineCount = $machines->where('status', 'online')->count();
|
$onlineCount = Machine::online()->count();
|
||||||
|
|
||||||
$machineIds = $machines->pluck('id')->toArray();
|
$machineIds = $machines->pluck('id')->toArray();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class MachineFactory extends Factory
|
|||||||
return [
|
return [
|
||||||
'name' => 'Machine-' . fake()->unique()->numberBetween(101, 999),
|
'name' => 'Machine-' . fake()->unique()->numberBetween(101, 999),
|
||||||
'location' => fake()->address(),
|
'location' => fake()->address(),
|
||||||
'status' => fake()->randomElement(['online', 'offline', 'error']),
|
|
||||||
'temperature' => fake()->randomFloat(2, 2, 10),
|
'temperature' => fake()->randomFloat(2, 2, 10),
|
||||||
'firmware_version' => 'v' . fake()->randomElement(['1.0.0', '1.1.2', '2.0.1']),
|
'firmware_version' => 'v' . fake()->randomElement(['1.0.0', '1.1.2', '2.0.1']),
|
||||||
'serial_no' => 'SN-' . strtoupper(fake()->unique()->bothify('??###?')),
|
'serial_no' => 'SN-' . strtoupper(fake()->unique()->bothify('??###?')),
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('remote_commands', function (Blueprint $table) {
|
if (DB::getDriverName() !== 'sqlite') {
|
||||||
DB::statement("ALTER TABLE remote_commands MODIFY COLUMN status ENUM('pending', 'sent', 'success', 'failed', 'superseded') DEFAULT 'pending'");
|
DB::statement("ALTER TABLE remote_commands MODIFY COLUMN status ENUM('pending', 'sent', 'success', 'failed', 'superseded') DEFAULT 'pending'");
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,8 +21,8 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::table('remote_commands', function (Blueprint $table) {
|
if (DB::getDriverName() !== 'sqlite') {
|
||||||
DB::statement("ALTER TABLE remote_commands MODIFY COLUMN status ENUM('pending', 'sent', 'success', 'failed') DEFAULT 'pending'");
|
DB::statement("ALTER TABLE remote_commands MODIFY COLUMN status ENUM('pending', 'sent', 'success', 'failed') DEFAULT 'pending'");
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('machines', function (Blueprint $table) {
|
||||||
|
if (Schema::hasColumn('machines', 'status')) {
|
||||||
|
$table->dropColumn('status');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('machines', function (Blueprint $table) {
|
||||||
|
$table->string('status')->default('offline')->after('location');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
1167
lang/en.json
1167
lang/en.json
File diff suppressed because it is too large
Load Diff
1154
lang/ja.json
1154
lang/ja.json
File diff suppressed because it is too large
Load Diff
1164
lang/zh_TW.json
1164
lang/zh_TW.json
File diff suppressed because it is too large
Load Diff
@@ -194,8 +194,7 @@
|
|||||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Machine Settings') }}</h1>
|
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Machine Settings') }}</h1>
|
||||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Management of operational parameters and models') }}</p>
|
||||||
__('Management of operational parameters and models') }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@if($tab === 'machines')
|
@if($tab === 'machines')
|
||||||
@@ -499,8 +498,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
<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" />
|
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>
|
</svg>
|
||||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No
|
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
|
||||||
accounts found') }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -628,8 +626,7 @@
|
|||||||
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-3xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full animate-luxury-in border border-slate-100 dark:border-slate-800">
|
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-3xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full animate-luxury-in border border-slate-100 dark:border-slate-800">
|
||||||
<div
|
<div
|
||||||
class="px-8 pt-8 pb-6 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
|
class="px-8 pt-8 pb-6 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
|
||||||
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Add
|
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Add Machine') }}</h3>
|
||||||
Machine') }}</h3>
|
|
||||||
<button @click="showCreateMachineModal = false"
|
<button @click="showCreateMachineModal = false"
|
||||||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -732,8 +729,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
|
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
|
||||||
<button type="button" @click="showCreateMachineModal = false" class="btn-luxury-ghost">{{
|
<button type="button" @click="showCreateMachineModal = false" class="btn-luxury-ghost">{{ __('Cancel') }}</button>
|
||||||
__('Cancel') }}</button>
|
|
||||||
<button type="submit" class="btn-luxury-primary px-8">{{ __('Save') }}</button>
|
<button type="submit" class="btn-luxury-primary px-8">{{ __('Save') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -757,8 +753,7 @@
|
|||||||
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-3xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full animate-luxury-in border border-slate-100 dark:border-slate-800">
|
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-3xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full animate-luxury-in border border-slate-100 dark:border-slate-800">
|
||||||
<div
|
<div
|
||||||
class="px-8 pt-8 pb-6 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
|
class="px-8 pt-8 pb-6 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
|
||||||
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Add
|
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Add Machine Model') }}</h3>
|
||||||
Machine Model') }}</h3>
|
|
||||||
<button @click="showCreateModelModal = false"
|
<button @click="showCreateModelModal = false"
|
||||||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -774,16 +769,14 @@
|
|||||||
<div class="px-8 py-8 space-y-6">
|
<div class="px-8 py-8 space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{
|
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Model Name') }}</label>
|
||||||
__('Model Name') }}</label>
|
|
||||||
<input type="text" name="name" required class="luxury-input w-full"
|
<input type="text" name="name" required class="luxury-input w-full"
|
||||||
placeholder="{{ __('Enter model name') }}">
|
placeholder="{{ __('Enter model name') }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
|
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
|
||||||
<button type="button" @click="showCreateModelModal = false" class="btn-luxury-ghost">{{
|
<button type="button" @click="showCreateModelModal = false" class="btn-luxury-ghost">{{ __('Cancel') }}</button>
|
||||||
__('Cancel') }}</button>
|
|
||||||
<button type="submit" class="btn-luxury-primary px-8">{{ __('Create') }}</button>
|
<button type="submit" class="btn-luxury-primary px-8">{{ __('Create') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -807,8 +800,7 @@
|
|||||||
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-3xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full animate-luxury-in border border-slate-100 dark:border-slate-800">
|
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-3xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full animate-luxury-in border border-slate-100 dark:border-slate-800">
|
||||||
<div
|
<div
|
||||||
class="px-8 pt-8 pb-6 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
|
class="px-8 pt-8 pb-6 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
|
||||||
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{
|
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Edit Machine Model') }}</h3>
|
||||||
__('Edit Machine Model') }}</h3>
|
|
||||||
<button @click="showEditModelModal = false"
|
<button @click="showEditModelModal = false"
|
||||||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -824,16 +816,14 @@
|
|||||||
<div class="px-8 py-8 space-y-6">
|
<div class="px-8 py-8 space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{
|
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Model Name') }}</label>
|
||||||
__('Model Name') }}</label>
|
|
||||||
<input type="text" name="name" x-model="currentModel.name" required
|
<input type="text" name="name" x-model="currentModel.name" required
|
||||||
class="luxury-input w-full" placeholder="{{ __('Enter model name') }}">
|
class="luxury-input w-full" placeholder="{{ __('Enter model name') }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
|
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
|
||||||
<button type="button" @click="showEditModelModal = false" class="btn-luxury-ghost">{{
|
<button type="button" @click="showEditModelModal = false" class="btn-luxury-ghost">{{ __('Cancel') }}</button>
|
||||||
__('Cancel') }}</button>
|
|
||||||
<button type="submit" class="btn-luxury-primary px-8">{{ __('Save') }}</button>
|
<button type="submit" class="btn-luxury-primary px-8">{{ __('Save') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -1103,8 +1093,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<section class="space-y-6">
|
<section class="space-y-6">
|
||||||
<h3 class="text-xs font-black text-cyan-500 uppercase tracking-[0.3em]">{{ __('Hardware
|
<h3 class="text-xs font-black text-cyan-500 uppercase tracking-[0.3em]">{{ __('Hardware & Network') }}</h3>
|
||||||
& Network') }}</h3>
|
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<div
|
<div
|
||||||
class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80">
|
class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80">
|
||||||
@@ -1291,9 +1280,7 @@
|
|||||||
<div
|
<div
|
||||||
class='w-10 h-10 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin'>
|
class='w-10 h-10 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin'>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span class='text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em] animate-pulse'>{{ __('Syncing Permissions...') }}</span>
|
||||||
class='text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em] animate-pulse'>{{
|
|
||||||
__('Syncing Permissions...') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,9 +11,11 @@
|
|||||||
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Connectivity Status') }}</h3>
|
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Connectivity Status') }}</h3>
|
||||||
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-1">{{ __('Real-time status monitoring') }}</p>
|
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-1">{{ __('Real-time status monitoring') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-x-1.5 px-3 py-1 rounded-full bg-cyan-500/10 text-cyan-500 border border-cyan-500/20">
|
<div
|
||||||
|
class="flex items-center gap-x-1.5 px-3 py-1 rounded-full bg-cyan-500/10 text-cyan-500 border border-cyan-500/20">
|
||||||
<span class="relative flex h-2 w-2">
|
<span class="relative flex h-2 w-2">
|
||||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75"></span>
|
<span
|
||||||
|
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75"></span>
|
||||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-cyan-500"></span>
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-cyan-500"></span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[10px] font-black uppercase tracking-wider">{{ __('LIVE') }}</span>
|
<span class="text-[10px] font-black uppercase tracking-wider">{{ __('LIVE') }}</span>
|
||||||
@@ -35,14 +37,14 @@
|
|||||||
<div class="w-2 h-2 rounded-full bg-rose-500 shadow-[0_0_10px_rgba(244,63,94,0.6)]"></div>
|
<div class="w-2 h-2 rounded-full bg-rose-500 shadow-[0_0_10px_rgba(244,63,94,0.6)]"></div>
|
||||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ __('Offline Machines') }}</span>
|
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ __('Offline Machines') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-2xl font-black text-rose-500">{{ $alertsPending }}</span>
|
<span class="text-2xl font-black text-rose-500">{{ $offlineMachines }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between pr-10">
|
<div class="flex items-center justify-between pr-10">
|
||||||
<div class="flex items-center gap-x-4">
|
<div class="flex items-center gap-x-4">
|
||||||
<div class="w-2 h-2 rounded-full bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.6)]"></div>
|
<div class="w-2 h-2 rounded-full bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.6)]"></div>
|
||||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ __('Alerts Pending') }}</span>
|
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ __('Alerts Pending') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-2xl font-black text-slate-900 dark:text-white">0</span>
|
<span class="text-2xl font-black text-slate-900 dark:text-white">{{ $alertsPending }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -51,8 +53,11 @@
|
|||||||
|
|
||||||
<!-- Right: Big Total -->
|
<!-- Right: Big Total -->
|
||||||
<div class="w-40 text-center">
|
<div class="w-40 text-center">
|
||||||
<p class="text-7xl font-black text-cyan-500 drop-shadow-[0_0_20px_rgba(6,182,212,0.3)] leading-none">{{ $activeMachines }}</p>
|
<p
|
||||||
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em] mt-4">{{ __('Total Connected') }}</p>
|
class="text-7xl font-black text-cyan-500 drop-shadow-[0_0_20px_rgba(6,182,212,0.3)] leading-none">
|
||||||
|
{{ $activeMachines }}</p>
|
||||||
|
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em] mt-4">
|
||||||
|
{{ __('Total Connected') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,25 +69,38 @@
|
|||||||
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Monthly Transactions') }}</h3>
|
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Monthly Transactions') }}</h3>
|
||||||
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-1">{{ __('Monthly cumulative revenue overview') }}</p>
|
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-1">{{ __('Monthly cumulative revenue overview') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800/80 text-slate-400 dark:text-slate-500 border border-transparent dark:border-slate-700/50">
|
<div
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800/80 text-slate-400 dark:text-slate-500 border border-transparent dark:border-slate-700/50">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
|
<path
|
||||||
|
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col space-y-4 justify-center">
|
<div class="flex-1 flex flex-col space-y-4 justify-center">
|
||||||
<!-- Today Stat Card -->
|
<!-- Today Stat Card -->
|
||||||
<div class="group flex items-center justify-between p-5 rounded-2xl bg-white dark:bg-slate-900 shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)] dark:shadow-none border border-slate-100 dark:border-slate-800 transition-all hover:border-cyan-500/30">
|
<div
|
||||||
|
class="group flex items-center justify-between p-5 rounded-2xl bg-white dark:bg-slate-900 shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)] dark:shadow-none border border-slate-100 dark:border-slate-800 transition-all hover:border-cyan-500/30">
|
||||||
<div class="flex items-center gap-x-4">
|
<div class="flex items-center gap-x-4">
|
||||||
<div class="w-12 h-12 rounded-xl bg-cyan-500/10 dark:bg-cyan-500/20 flex items-center justify-center text-cyan-600 dark:text-cyan-400 shadow-sm transition-transform group-hover:scale-110">
|
<div
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18L9 11.25l4.5 4.5L21.75 7.5M21.75 7.5V12m0-4.5H17.25"/></svg>
|
class="w-12 h-12 rounded-xl bg-cyan-500/10 dark:bg-cyan-500/20 flex items-center justify-center text-cyan-600 dark:text-cyan-400 shadow-sm transition-transform group-hover:scale-110">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
stroke-width="2.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M2.25 18L9 11.25l4.5 4.5L21.75 7.5M21.75 7.5V12m0-4.5H17.25" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-bold text-slate-500 dark:text-slate-400">{{ __("Today's Transactions") }}</p>
|
<p class="text-xs font-bold text-slate-500 dark:text-slate-400">{{ __("Today's Transactions") }}</p>
|
||||||
<p class="text-4xl font-black text-slate-900 dark:text-white mt-1 tracking-tight drop-shadow-sm">${{ number_format($totalRevenue / 30, 0) }}</p>
|
<p
|
||||||
|
class="text-4xl font-black text-slate-900 dark:text-white mt-1 tracking-tight drop-shadow-sm">
|
||||||
|
${{ number_format($totalRevenue / 30, 0) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-end gap-y-1">
|
<div class="flex flex-col items-end gap-y-1">
|
||||||
<span class="text-[10px] font-black text-emerald-500 bg-emerald-500/10 px-2.5 py-0.5 rounded-full">+12.5%</span>
|
<span
|
||||||
|
class="text-[10px] font-black text-emerald-500 bg-emerald-500/10 px-2.5 py-0.5 rounded-full">+12.5%</span>
|
||||||
<p class="text-[9px] font-bold text-slate-300 dark:text-slate-500 uppercase tracking-tighter">{{ __('vs Yesterday') }}</p>
|
<p class="text-[9px] font-bold text-slate-300 dark:text-slate-500 uppercase tracking-tighter">{{ __('vs Yesterday') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,25 +108,39 @@
|
|||||||
<!-- Previous Days Stats Row -->
|
<!-- Previous Days Stats Row -->
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<!-- Yesterday Card -->
|
<!-- Yesterday Card -->
|
||||||
<div class="group flex flex-col p-5 rounded-2xl bg-slate-50/50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 transition-all hover:border-cyan-500/20">
|
<div
|
||||||
|
class="group flex flex-col p-5 rounded-2xl bg-slate-50/50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 transition-all hover:border-cyan-500/20">
|
||||||
<div class="flex justify-between items-start mb-2">
|
<div class="flex justify-between items-start mb-2">
|
||||||
<p class="text-xs font-bold text-slate-500 dark:text-slate-400">{{ __("Yesterday") }}</p>
|
<p class="text-xs font-bold text-slate-500 dark:text-slate-400">{{ __("Yesterday") }}</p>
|
||||||
<div class="w-6 h-6 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400">
|
<div
|
||||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
class="w-6 h-6 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
stroke-width="2.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xl font-black text-slate-800 dark:text-slate-200">${{ number_format($totalRevenue / 25, 0) }}</p>
|
<p class="text-xl font-black text-slate-800 dark:text-slate-200">${{ number_format($totalRevenue
|
||||||
|
/ 25, 0) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Before Yesterday Card -->
|
<!-- Before Yesterday Card -->
|
||||||
<div class="group flex flex-col p-5 rounded-2xl bg-slate-50/50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 transition-all hover:border-cyan-500/20">
|
<div
|
||||||
|
class="group flex flex-col p-5 rounded-2xl bg-slate-50/50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 transition-all hover:border-cyan-500/20">
|
||||||
<div class="flex justify-between items-start mb-2">
|
<div class="flex justify-between items-start mb-2">
|
||||||
<p class="text-xs font-bold text-slate-500 dark:text-slate-400">{{ __("Day Before") }}</p>
|
<p class="text-xs font-bold text-slate-500 dark:text-slate-400">{{ __("Day Before") }}</p>
|
||||||
<div class="w-6 h-6 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400">
|
<div
|
||||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg>
|
class="w-6 h-6 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
stroke-width="2.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xl font-black text-slate-800 dark:text-slate-200">${{ number_format($totalRevenue / 40, 0) }}</p>
|
<p class="text-xl font-black text-slate-800 dark:text-slate-200">${{ number_format($totalRevenue
|
||||||
|
/ 40, 0) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,7 +153,8 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-x-3">
|
<div class="flex items-center gap-x-3">
|
||||||
<h2 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Status List') }}</h2>
|
<h2 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Status List') }}</h2>
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-lg text-xs font-black bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700 uppercase tracking-tighter">
|
<span
|
||||||
|
class="inline-flex items-center px-2.5 py-0.5 rounded-lg text-xs font-black bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700 uppercase tracking-tighter">
|
||||||
{{ __('Total items', ['count' => $machines->total()]) }}
|
{{ __('Total items', ['count' => $machines->total()]) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,12 +164,16 @@
|
|||||||
<form action="{{ route('admin.dashboard') }}" method="GET" class="flex flex-wrap items-center gap-4">
|
<form action="{{ route('admin.dashboard') }}" method="GET" class="flex flex-wrap items-center gap-4">
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
||||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||||
|
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<input type="text" name="search" value="{{ request('search') }}" class="py-3 pl-12 pr-6 block w-64 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" placeholder="{{ __('Quick search...') }}">
|
<input type="text" name="search" value="{{ request('search') }}"
|
||||||
|
class="py-3 pl-12 pr-6 block w-64 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none"
|
||||||
|
placeholder="{{ __('Quick search...') }}">
|
||||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -146,12 +183,24 @@
|
|||||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
|
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
|
||||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Machine Info') }}</th>
|
<th
|
||||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Running Status') }}</th>
|
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">
|
||||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Today Cumulative Sales') }}</th>
|
{{ __('Machine Info') }}</th>
|
||||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Current Stock') }}</th>
|
<th
|
||||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Last Signal') }}</th>
|
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
|
||||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Alert Summary') }}</th>
|
{{ __('Running Status') }}</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
|
||||||
|
{{ __('Today Cumulative Sales') }}</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
|
||||||
|
{{ __('Current Stock') }}</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
|
||||||
|
{{ __('Last Signal') }}</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">
|
||||||
|
{{ __('Alert Summary') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/50">
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/50">
|
||||||
@@ -159,24 +208,57 @@
|
|||||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||||
<td class="px-6 py-6">
|
<td class="px-6 py-6">
|
||||||
<div class="flex items-center gap-x-5">
|
<div class="flex items-center gap-x-5">
|
||||||
<div class="w-11 h-11 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 dark:text-slate-300 group-hover:bg-cyan-500 group-hover:text-white transition-all shadow-sm">
|
<div
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/></svg>
|
class="w-11 h-11 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 dark:text-slate-300 group-hover:bg-cyan-500 group-hover:text-white transition-all shadow-sm">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
stroke-width="2">
|
||||||
|
<path
|
||||||
|
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<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">{{ $machine->name }}</span>
|
<span
|
||||||
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-1 uppercase tracking-[0.15em]">(SN: {{ $machine->serial_no }})</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">{{
|
||||||
|
$machine->name }}</span>
|
||||||
|
<span
|
||||||
|
class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-1 uppercase tracking-[0.15em]">(SN:
|
||||||
|
{{ $machine->serial_no }})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6 text-center">
|
<td class="px-6 py-6 text-center">
|
||||||
@if($machine->status === 'online')
|
@php
|
||||||
<span class="inline-flex items-center px-4 py-1.5 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">
|
$cStatus = $machine->calculated_status;
|
||||||
{{ __('Online') }}
|
@endphp
|
||||||
</span>
|
|
||||||
|
@if($cStatus === 'online')
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20">
|
||||||
|
<div class="relative flex h-2 w-2">
|
||||||
|
<span
|
||||||
|
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||||
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 tracking-widest uppercase">{{
|
||||||
|
__('Online') }}</span>
|
||||||
|
</div>
|
||||||
|
@elseif($cStatus === 'error')
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20">
|
||||||
|
<div class="h-2 w-2 rounded-full bg-rose-500 animate-pulse"></div>
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-black text-rose-600 dark:text-rose-400 tracking-widest uppercase">{{
|
||||||
|
__('Error') }}</span>
|
||||||
|
</div>
|
||||||
@else
|
@else
|
||||||
<span class="inline-flex items-center px-4 py-1.5 rounded-full text-[11px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">
|
<div
|
||||||
{{ __('Offline') }}
|
class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20">
|
||||||
</span>
|
<div class="h-2 w-2 rounded-full bg-slate-400"></div>
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-black text-slate-600 dark:text-slate-400 tracking-widest uppercase">{{
|
||||||
|
__('Offline') }}</span>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6 text-center">
|
<td class="px-6 py-6 text-center">
|
||||||
@@ -184,19 +266,26 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6">
|
<td class="px-6 py-6">
|
||||||
<div class="flex flex-col items-center gap-y-2.5">
|
<div class="flex flex-col items-center gap-y-2.5">
|
||||||
<div class="w-32 h-2 bg-slate-100 dark:bg-slate-800 rounded-full overflow-hidden shadow-inner">
|
<div
|
||||||
<div class="h-full bg-rose-500 rounded-full shadow-[0_0_8px_rgba(244,63,94,0.4)]" style="width: 15.5%"></div>
|
class="w-32 h-2 bg-slate-100 dark:bg-slate-800 rounded-full overflow-hidden shadow-inner">
|
||||||
|
<div class="h-full bg-rose-500 rounded-full shadow-[0_0_8px_rgba(244,63,94,0.4)]"
|
||||||
|
style="width: 15.5%"></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[11px] font-black text-rose-500 uppercase tracking-[0.2em]">15.5% {{ __('Low Stock') }}</span>
|
<span class="text-[11px] font-black text-rose-500 uppercase tracking-[0.2em]">15.5% {{
|
||||||
|
__('Low Stock') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6 text-center">
|
<td class="px-6 py-6 text-center">
|
||||||
<div class="text-xs font-black text-slate-400 dark:text-slate-400/80 uppercase tracking-widest leading-none">
|
<div
|
||||||
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->format('Y/m/d H:i') : '---' }}
|
class="text-xs font-black text-slate-400 dark:text-slate-400/80 uppercase tracking-widest leading-none">
|
||||||
|
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->format('Y/m/d H:i') :
|
||||||
|
'---' }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6 text-right">
|
<td class="px-6 py-6 text-right">
|
||||||
<span class="text-[11px] font-bold text-slate-400/30 dark:text-slate-500 uppercase tracking-widest group-hover:text-slate-400 transition-colors">{{ __('No alert summary') }}</span>
|
<span
|
||||||
|
class="text-[11px] font-bold text-slate-400/30 dark:text-slate-500 uppercase tracking-widest group-hover:text-slate-400 transition-colors">{{
|
||||||
|
__('No alert summary') }}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<script>
|
<script>
|
||||||
window.machineApp = function() {
|
window.machineApp = function () {
|
||||||
return {
|
return {
|
||||||
showLogPanel: false,
|
showLogPanel: false,
|
||||||
|
showEditModal: false,
|
||||||
|
editMachineId: '',
|
||||||
|
editMachineName: '',
|
||||||
activeTab: 'status',
|
activeTab: 'status',
|
||||||
currentMachineId: '',
|
currentMachineId: '',
|
||||||
currentMachineSn: '',
|
currentMachineSn: '',
|
||||||
@@ -41,6 +44,12 @@ window.machineApp = function() {
|
|||||||
await this.fetchLogs();
|
await this.fetchLogs();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openEditModal(id, name) {
|
||||||
|
this.editMachineId = id;
|
||||||
|
this.editMachineName = name;
|
||||||
|
this.showEditModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
async fetchLogs() {
|
async fetchLogs() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -51,7 +60,7 @@ window.machineApp = function() {
|
|||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) this.logs = data.data.data || data.data || [];
|
if (data.success) this.logs = data.data.data || data.data || [];
|
||||||
} catch(e) { console.error('fetchLogs error:', e); }
|
} catch (e) { console.error('fetchLogs error:', e); }
|
||||||
finally { this.loading = false; }
|
finally { this.loading = false; }
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -66,21 +75,21 @@ window.machineApp = function() {
|
|||||||
this.slots = data.slots;
|
this.slots = data.slots;
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
} catch(e) { console.error('openCabinet error:', e); }
|
} catch (e) { console.error('openCabinet error:', e); }
|
||||||
finally { this.loading = false; }
|
finally { this.loading = false; }
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-4 pb-20 mt-4" x-data="machineApp()"
|
<div class="space-y-4 pb-20 mt-4" x-data="machineApp()" @keydown.escape.window="showLogPanel = false">
|
||||||
@keydown.escape.window="showLogPanel = false">
|
|
||||||
<!-- Top Header & Actions -->
|
<!-- Top Header & Actions -->
|
||||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display transition-all duration-300">
|
<h1
|
||||||
|
class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display transition-all duration-300">
|
||||||
{{ __('Machine List') }}
|
{{ __('Machine List') }}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
|
||||||
@@ -167,9 +176,13 @@ window.machineApp = function() {
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6 text-center">
|
<td class="px-6 py-6 text-center">
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="flex items-center justify-center gap-2">
|
||||||
@if($machine->status === 'online')
|
@php
|
||||||
<div
|
$cStatus = $machine->calculated_status;
|
||||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20">
|
@endphp
|
||||||
|
|
||||||
|
@if($cStatus === 'online')
|
||||||
|
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 tooltip"
|
||||||
|
title="{{ __('Machine is heartbeat normal') }}">
|
||||||
<div class="relative flex h-2 w-2">
|
<div class="relative flex h-2 w-2">
|
||||||
<span
|
<span
|
||||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||||
@@ -179,17 +192,17 @@ window.machineApp = function() {
|
|||||||
class="text-xs font-black text-emerald-600 dark:text-emerald-400 tracking-widest uppercase">{{
|
class="text-xs font-black text-emerald-600 dark:text-emerald-400 tracking-widest uppercase">{{
|
||||||
__('Online') }}</span>
|
__('Online') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@elseif($machine->status === 'error')
|
@elseif($cStatus === 'error')
|
||||||
<div
|
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20 tooltip"
|
||||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20">
|
title="{{ __('Recently reported errors or warnings in logs') }}">
|
||||||
<div class="h-2 w-2 rounded-full bg-rose-500 animate-pulse"></div>
|
<div class="h-2 w-2 rounded-full bg-rose-500 animate-pulse"></div>
|
||||||
<span
|
<span
|
||||||
class="text-xs font-black text-rose-600 dark:text-rose-400 tracking-widest uppercase">{{
|
class="text-xs font-black text-rose-600 dark:text-rose-400 tracking-widest uppercase">{{
|
||||||
__('Error') }}</span>
|
__('Error') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div
|
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20 tooltip"
|
||||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20">
|
title="{{ __('No heartbeat for over 30 seconds') }}">
|
||||||
<div class="h-2 w-2 rounded-full bg-slate-400"></div>
|
<div class="h-2 w-2 rounded-full bg-slate-400"></div>
|
||||||
<span
|
<span
|
||||||
class="text-xs font-black text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{
|
class="text-xs font-black text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{
|
||||||
@@ -230,15 +243,16 @@ window.machineApp = function() {
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6 text-right">
|
<td class="px-6 py-6 text-right">
|
||||||
<div class="flex items-center justify-end gap-2">
|
<div class="flex items-center justify-end gap-2">
|
||||||
<a href="{{ route('admin.machines.edit', $machine->id) }}"
|
<button type="button"
|
||||||
|
@click="openEditModal('{{ $machine->id }}', '{{ addslashes($machine->name) }}')"
|
||||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn tooltip"
|
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn tooltip"
|
||||||
title="{{ __('Edit') }}">
|
title="{{ __('Edit Name') }}">
|
||||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
||||||
viewBox="0 0 24 24">
|
viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}')"
|
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}')"
|
||||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn tooltip"
|
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn tooltip"
|
||||||
@@ -504,4 +518,57 @@ window.machineApp = function() {
|
|||||||
</div>
|
</div>
|
||||||
</div><!-- /Offcanvas -->
|
</div><!-- /Offcanvas -->
|
||||||
|
|
||||||
@endsection
|
<!-- Edit Machine Name Modal -->
|
||||||
|
<div x-show="showEditModal" class="fixed inset-0 z-[100] overflow-y-auto" style="display: none;" role="dialog" aria-modal="true">
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<!-- Background Backdrop -->
|
||||||
|
<div x-show="showEditModal" 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" @click="showEditModal = false">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen" aria-hidden="true">​</span>
|
||||||
|
|
||||||
|
<!-- Modal Panel -->
|
||||||
|
<div x-show="showEditModal"
|
||||||
|
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 align-bottom bg-white dark:bg-slate-900 rounded-[2.5rem] p-10 text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full border border-slate-100 dark:border-slate-800">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none mb-2">{{ __('Edit Machine Name') }}</h3>
|
||||||
|
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Update identification for your asset') }}</p>
|
||||||
|
|
||||||
|
<form :action="'/admin/machines/' + editMachineId" method="POST" class="mt-8 space-y-6">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.1em]">{{ __('New Machine Name') }}</label>
|
||||||
|
<input type="text" name="name" x-model="editMachineName" required
|
||||||
|
class="luxury-input block w-full px-6 py-4 text-base font-bold text-slate-800 dark:text-white bg-slate-50/50 dark:bg-slate-900/50"
|
||||||
|
placeholder="{{ __('Enter machine name...') }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4 pt-4">
|
||||||
|
<button type="button" @click="showEditModal = false"
|
||||||
|
class="px-8 py-4 bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black rounded-2xl border border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700 transition-all">
|
||||||
|
{{ __('Cancel') }}
|
||||||
|
</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="flex-1 bg-cyan-500 hover:bg-cyan-600 text-white font-black py-4 rounded-2xl shadow-lg shadow-cyan-500/30 transition-all duration-300 transform hover:-translate-y-0.5 active:scale-95">
|
||||||
|
{{ __('Save Changes') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /Edit Modal -->
|
||||||
|
|
||||||
|
@endsection
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
'stock' => __('Stock & Expiry'),
|
'stock' => __('Stock & Expiry'),
|
||||||
default => null,
|
default => null,
|
||||||
},
|
},
|
||||||
'edit' => str_starts_with($routeName, 'profile') ? null : __('Edit'),
|
'edit' => str_starts_with($routeName, 'profile') ? __('Profile') : __('Edit'),
|
||||||
'create' => __('Create'),
|
'create' => __('Create'),
|
||||||
'show' => __('Detail'),
|
'show' => __('Detail'),
|
||||||
'logs' => __('Machine Logs'),
|
'logs' => __('Machine Logs'),
|
||||||
|
|||||||
86
tests/Feature/MachineStatusTest.php
Normal file
86
tests/Feature/MachineStatusTest.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Machine\Machine;
|
||||||
|
use App\Models\Machine\MachineLog;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class MachineStatusTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test machine is online with recent heartbeat and no errors.
|
||||||
|
*/
|
||||||
|
public function test_machine_is_online_with_recent_heartbeat(): void
|
||||||
|
{
|
||||||
|
$machine = Machine::factory()->create([
|
||||||
|
'last_heartbeat_at' => now()->subSeconds(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals('online', $machine->calculated_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test machine is error with recent heartbeat but recent errors.
|
||||||
|
*/
|
||||||
|
public function test_machine_is_error_with_recent_logs(): void
|
||||||
|
{
|
||||||
|
$machine = Machine::factory()->create([
|
||||||
|
'last_heartbeat_at' => now()->subSeconds(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add an error log 10 mins ago
|
||||||
|
MachineLog::create([
|
||||||
|
'machine_id' => $machine->id,
|
||||||
|
'level' => 'error',
|
||||||
|
'created_at' => now()->subMinutes(10),
|
||||||
|
'message' => 'Test error',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals('error', $machine->calculated_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test machine is offline with old heartbeat (30s+).
|
||||||
|
*/
|
||||||
|
public function test_machine_is_offline_with_old_heartbeat(): void
|
||||||
|
{
|
||||||
|
$machine = Machine::factory()->create([
|
||||||
|
'last_heartbeat_at' => now()->subSeconds(35),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals('offline', $machine->calculated_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test machine scopes (online, offline, hasError).
|
||||||
|
*/
|
||||||
|
public function test_machine_scopes(): void
|
||||||
|
{
|
||||||
|
// 1. Online machine
|
||||||
|
$onlineMachine = Machine::factory()->create(['last_heartbeat_at' => now()->subSeconds(10)]);
|
||||||
|
|
||||||
|
// 2. Offline machine
|
||||||
|
$offlineMachine = Machine::factory()->create(['last_heartbeat_at' => now()->subSeconds(40)]);
|
||||||
|
|
||||||
|
// 3. Online machine with error
|
||||||
|
$errorMachine = Machine::factory()->create(['last_heartbeat_at' => now()->subSeconds(5)]);
|
||||||
|
MachineLog::create([
|
||||||
|
'machine_id' => $errorMachine->id,
|
||||||
|
'level' => 'error',
|
||||||
|
'created_at' => now()->subMinutes(5),
|
||||||
|
'message' => 'Error log',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(2, Machine::online()->count());
|
||||||
|
$this->assertEquals(1, Machine::offline()->count());
|
||||||
|
$this->assertEquals(1, Machine::online()->hasError()->count());
|
||||||
|
|
||||||
|
$this->assertTrue(Machine::online()->get()->contains($onlineMachine));
|
||||||
|
$this->assertTrue(Machine::offline()->get()->contains($offlineMachine));
|
||||||
|
$this->assertTrue(Machine::online()->hasError()->get()->contains($errorMachine));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user