Compare commits
4 Commits
demo
...
7784270781
| Author | SHA1 | Date | |
|---|---|---|---|
| 7784270781 | |||
| ebac2525dc | |||
| e8c6c12e8d | |||
| d2131aaf06 |
@@ -4,22 +4,78 @@ namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\RemoteCommand;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class RemoteController extends Controller
|
||||
{
|
||||
// 機台庫存
|
||||
/**
|
||||
* 遠端管理指揮中心
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$machines = Machine::withCount(['slots'])->orderBy('name')->get();
|
||||
$selectedMachine = null;
|
||||
|
||||
if ($request->has('machine_id')) {
|
||||
$selectedMachine = Machine::with(['slots.product', 'commands' => function($query) {
|
||||
$query->latest()->limit(10);
|
||||
}])->find($request->machine_id);
|
||||
}
|
||||
|
||||
return view('admin.remote.index', [
|
||||
'machines' => $machines,
|
||||
'selectedMachine' => $selectedMachine,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 儲存遠端指令
|
||||
*/
|
||||
public function storeCommand(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'machine_id' => 'required|exists:machines,id',
|
||||
'command_type' => 'required|string|in:reboot,reboot_card,checkout,lock,unlock,change,dispense',
|
||||
'amount' => 'nullable|integer|min:0',
|
||||
'slot_no' => 'nullable|string',
|
||||
'note' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$payload = [];
|
||||
if ($validated['command_type'] === 'change') {
|
||||
$payload['amount'] = $validated['amount'];
|
||||
} elseif ($validated['command_type'] === 'dispense') {
|
||||
$payload['slot_no'] = $validated['slot_no'];
|
||||
}
|
||||
|
||||
RemoteCommand::create([
|
||||
'machine_id' => $validated['machine_id'],
|
||||
'command_type' => $validated['command_type'],
|
||||
'payload' => $payload,
|
||||
'status' => 'pending',
|
||||
'note' => $validated['note'] ?? null,
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', __('Command has been queued successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 機台庫存管理 (現有功能保留)
|
||||
*/
|
||||
public function stock(Request $request)
|
||||
{
|
||||
$machines = \App\Models\Machine\Machine::withCount([
|
||||
$machines = Machine::withCount([
|
||||
'slots as slots_count',
|
||||
'slots as low_stock_count' => function ($query) {
|
||||
$query->where('stock', '<=', 5);
|
||||
}
|
||||
])->orderBy('name')->get();
|
||||
$selectedMachine = null;
|
||||
|
||||
$selectedMachine = null;
|
||||
if ($request->has('machine_id')) {
|
||||
$selectedMachine = \App\Models\Machine\Machine::find($request->machine_id);
|
||||
$selectedMachine = Machine::with('slots.product')->find($request->machine_id);
|
||||
}
|
||||
|
||||
return view('admin.remote.stock', [
|
||||
@@ -27,58 +83,4 @@ class RemoteController extends Controller
|
||||
'selectedMachine' => $selectedMachine,
|
||||
]);
|
||||
}
|
||||
|
||||
// 機台重啟
|
||||
public function restart()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '遠端重啟機台',
|
||||
'description' => '遠端重啟機台系統',
|
||||
]);
|
||||
}
|
||||
|
||||
// 卡機重啟
|
||||
public function restartCardReader()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '遠端重啟刷卡機',
|
||||
'description' => '遠端重啟刷卡機設備',
|
||||
]);
|
||||
}
|
||||
|
||||
// 遠端結帳
|
||||
public function checkout()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '遠端結帳',
|
||||
'description' => '遠端執行結帳流程',
|
||||
]);
|
||||
}
|
||||
|
||||
// 遠端鎖定頁
|
||||
public function lock()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '遠端鎖定頁',
|
||||
'description' => '遠端鎖定機台頁面',
|
||||
]);
|
||||
}
|
||||
|
||||
// 遠端找零
|
||||
public function change()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '遠端找零',
|
||||
'description' => '遠端執行找零功能',
|
||||
]);
|
||||
}
|
||||
|
||||
// 遠端出貨
|
||||
public function dispense()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '遠端出貨',
|
||||
'description' => '遠端控制商品出貨',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,58 @@ class MachineController extends Controller
|
||||
// 異步處理狀態更新
|
||||
ProcessHeartbeat::dispatch($machine->serial_no, $data);
|
||||
|
||||
// 取出待處理指令
|
||||
$command = \App\Models\Machine\RemoteCommand::where('machine_id', $machine->id)
|
||||
->pending()
|
||||
->first();
|
||||
|
||||
$status = '49'; // 預設 49 (OK / No Command)
|
||||
$message = 'OK';
|
||||
|
||||
if ($command) {
|
||||
switch ($command->command_type) {
|
||||
case 'reboot':
|
||||
$status = '51';
|
||||
$message = 'reboot';
|
||||
break;
|
||||
case 'reboot_card':
|
||||
$status = '60';
|
||||
$message = 'reboot card machine';
|
||||
break;
|
||||
case 'checkout':
|
||||
$status = '61';
|
||||
$message = 'checkout';
|
||||
break;
|
||||
case 'lock':
|
||||
$status = '71';
|
||||
$message = 'lock';
|
||||
break;
|
||||
case 'unlock':
|
||||
$status = '70';
|
||||
$message = 'unlock';
|
||||
break;
|
||||
case 'change':
|
||||
$status = '82';
|
||||
$message = $command->payload['amount'] ?? '0';
|
||||
break;
|
||||
case 'dispense':
|
||||
$status = '85';
|
||||
$message = $command->payload['slot_no'] ?? '';
|
||||
break;
|
||||
case 'reload_stock':
|
||||
$status = '49';
|
||||
$message = 'reload B017';
|
||||
break;
|
||||
}
|
||||
// 標記為已發送 (sent)
|
||||
$command->update(['status' => 'sent', 'executed_at' => now()]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => 'OK',
|
||||
'status' => '49' // 某些硬體可能需要的成功碼
|
||||
'message' => $message,
|
||||
'status' => $status
|
||||
], 202); // 202 Accepted
|
||||
}
|
||||
|
||||
@@ -69,6 +116,8 @@ class MachineController extends Controller
|
||||
'capacity' => $slot->capacity,
|
||||
'price' => $slot->price,
|
||||
'status' => $slot->status,
|
||||
'expiry_date' => $slot->expiry_date,
|
||||
'batch_no' => $slot->batch_no,
|
||||
];
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -11,16 +11,15 @@ class RemoteCommand extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'machine_id',
|
||||
'command',
|
||||
'command_type',
|
||||
'payload',
|
||||
'status',
|
||||
'response_payload',
|
||||
'ttl',
|
||||
'executed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'payload' => 'array',
|
||||
'response_payload' => 'array',
|
||||
'executed_at' => 'datetime',
|
||||
];
|
||||
|
||||
@@ -28,4 +27,12 @@ class RemoteCommand extends Model
|
||||
{
|
||||
return $this->belongsTo(Machine::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for pending commands
|
||||
*/
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('status', 'pending')->orderBy('created_at', 'asc');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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('remote_commands', function (Blueprint $table) {
|
||||
$table->string('note', 255)->nullable()->after('payload');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('remote_commands', function (Blueprint $table) {
|
||||
$table->dropColumn('note');
|
||||
});
|
||||
}
|
||||
};
|
||||
427
resources/views/admin/remote/index.blade.php
Normal file
427
resources/views/admin/remote/index.blade.php
Normal file
@@ -0,0 +1,427 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<script>
|
||||
window.remoteControlApp = function(initialMachineId) {
|
||||
return {
|
||||
machines: @js($machines),
|
||||
searchQuery: '',
|
||||
selectedMachine: null,
|
||||
commands: [],
|
||||
viewMode: initialMachineId ? 'control' : 'list',
|
||||
loading: false,
|
||||
submitting: false,
|
||||
|
||||
// Form States
|
||||
lockStatus: false, // false = unlocked, true = locked
|
||||
changeAmount: 100,
|
||||
selectedSlot: '',
|
||||
note: '',
|
||||
|
||||
async init() {
|
||||
if (initialMachineId) {
|
||||
const machine = this.machines.find(m => m.id == initialMachineId);
|
||||
if (machine) {
|
||||
await this.selectMachine(machine);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async selectMachine(machine) {
|
||||
this.selectedMachine = machine;
|
||||
this.viewMode = 'control';
|
||||
this.loading = true;
|
||||
this.commands = [];
|
||||
|
||||
// Update URL without refresh
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('machine_id', machine.id);
|
||||
window.history.pushState({}, '', url);
|
||||
|
||||
try {
|
||||
// Fetch recent commands and full machine info (with slots)
|
||||
const res = await fetch(`/admin/remote?machine_id=${machine.id}`, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
// Note: For now we'll just use the already loaded $selectedMachine if available
|
||||
// But in a real app, an AJAX fetch for commands is better.
|
||||
// Since we are doing a full page load or partial, I'll assume we have commands passed.
|
||||
this.commands = @js($selectedMachine ? $selectedMachine->commands : []);
|
||||
} catch (e) {
|
||||
console.error('Fetch error:', e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
backToList() {
|
||||
this.viewMode = 'list';
|
||||
this.selectedMachine = null;
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.delete('machine_id');
|
||||
window.history.pushState({}, '', url);
|
||||
},
|
||||
|
||||
async sendCommand(type, params = {}) {
|
||||
if (!confirm(`{{ __('Are you sure you want to send this command?') }}`)) return;
|
||||
|
||||
this.submitting = true;
|
||||
try {
|
||||
const csrf = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
const formData = new FormData();
|
||||
formData.append('machine_id', this.selectedMachine.id);
|
||||
formData.append('command_type', type);
|
||||
formData.append('note', this.note);
|
||||
|
||||
if (params.amount) formData.append('amount', params.amount);
|
||||
if (params.slot_no) formData.append('slot_no', params.slot_no);
|
||||
|
||||
const res = await fetch('{{ route('admin.remote.store-command') }}', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': csrf },
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// Success feedback
|
||||
const toast = window.HSStaticMethods.autoInit ? null : null; // Use Preline toast if available
|
||||
alert('{{ __('Command queued successfully.') }}');
|
||||
location.reload(); // Simple refresh to see new command in list
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Command error:', e);
|
||||
} finally {
|
||||
this.submitting = false;
|
||||
}
|
||||
},
|
||||
|
||||
getCommandBadgeClass(status) {
|
||||
switch(status) {
|
||||
case 'pending': return 'bg-amber-100 text-amber-600 dark:bg-amber-500/10 dark:text-amber-400 border-amber-200 dark:border-amber-500/20';
|
||||
case 'sent': return 'bg-cyan-100 text-cyan-600 dark:bg-cyan-500/10 dark:text-cyan-400 border-cyan-200 dark:border-cyan-500/20';
|
||||
case 'success': return 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20';
|
||||
case 'failed': return 'bg-rose-100 text-rose-600 dark:bg-rose-500/10 dark:text-rose-400 border-rose-200 dark:border-rose-500/20';
|
||||
default: return 'bg-slate-100 text-slate-600 border-slate-200';
|
||||
}
|
||||
},
|
||||
|
||||
getCommandName(type) {
|
||||
const names = {
|
||||
'reboot': {{ Js::from(__('System Reboot')) }},
|
||||
'reboot_card': {{ Js::from(__('Card Reader Reboot')) }},
|
||||
'checkout': {{ Js::from(__('Remote Settlement')) }},
|
||||
'lock': {{ Js::from(__('Page Lock')) }},
|
||||
'unlock': {{ Js::from(__('Page Unlock')) }},
|
||||
'change': {{ Js::from(__('Remote Change')) }},
|
||||
'dispense': {{ Js::from(__('Remote Dispense')) }},
|
||||
'reload_stock': {{ Js::from(__('Stock Sync')) }}
|
||||
};
|
||||
return names[type] || type;
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="space-y-4 pb-20 mt-4"
|
||||
x-data="remoteControlApp({{ Js::from($selectedMachine ? $selectedMachine->id : '') }})">
|
||||
|
||||
<!-- Master View: Machine List -->
|
||||
<template x-if="viewMode === 'list'">
|
||||
<div class="space-y-6 animate-luxury-in">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">
|
||||
{{ __('Remote Command Center') }}
|
||||
</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
|
||||
{{ __('Execute maintenance and operational commands remotely') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="relative group max-w-md">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 transition-transform duration-300 group-focus-within:scale-110">
|
||||
<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.5">
|
||||
<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="searchQuery"
|
||||
class="luxury-input w-full pl-11 py-3 text-sm focus:ring-cyan-500/20"
|
||||
placeholder="{{ __('Search by name or S/N...') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<template x-for="machine in machines.filter(m =>
|
||||
m.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
m.serial_no.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)" :key="machine.id">
|
||||
<div @click="selectMachine(machine)"
|
||||
class="luxury-card rounded-[2.5rem] p-8 border border-slate-200/60 dark:border-slate-800/60 hover:border-cyan-500/50 hover:shadow-2xl hover:shadow-cyan-500/10 transition-all duration-500 cursor-pointer group flex flex-col justify-between h-full relative overflow-hidden">
|
||||
|
||||
<div class="absolute -right-10 -top-10 w-32 h-32 bg-cyan-500/5 rounded-full blur-3xl group-hover:bg-cyan-500/10 transition-colors"></div>
|
||||
|
||||
<div class="flex items-start gap-5 relative z-10">
|
||||
<div class="w-16 h-16 rounded-2xl bg-slate-50 dark:bg-slate-900 flex items-center justify-center text-slate-400 border border-slate-100 dark:border-slate-800 overflow-hidden shadow-inner group-hover:scale-110 transition-transform duration-500 shrink-0">
|
||||
<svg class="w-8 h-8 opacity-40 group-hover:text-cyan-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 x-text="machine.name" class="text-xl font-black text-slate-800 dark:text-white truncate"></h3>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span x-text="machine.serial_no" class="text-xs font-mono font-bold text-cyan-600 dark:text-cyan-400 tracking-widest uppercase"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex items-center justify-between relative z-10">
|
||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Click to Open Dashboard') }}</span>
|
||||
<div class="w-10 h-10 rounded-full bg-slate-50 dark:bg-slate-900 flex items-center justify-center text-slate-300 dark:text-slate-600 border border-slate-100 dark:border-slate-800 transition-all duration-300 group-hover:bg-cyan-500 group-hover:text-white group-hover:border-cyan-500 group-hover:scale-110">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Detail View: Remote Control Dashboard -->
|
||||
<template x-if="viewMode === 'control'">
|
||||
<div class="space-y-6 animate-luxury-in">
|
||||
<!-- Header Controls -->
|
||||
<div class="flex items-center gap-4 mb-2 px-1">
|
||||
<button @click="backToList()"
|
||||
class="p-2.5 rounded-xl bg-white dark:bg-slate-900 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all border border-slate-200/50 dark:border-slate-700/50 shadow-sm hover:shadow-md active:scale-95">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Command Center') }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
<!-- Left: Control Actions (Spans 2 columns) -->
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
|
||||
<!-- Machine Status Card -->
|
||||
<div class="luxury-card rounded-[2.5rem] p-8 border border-slate-200/60 dark:border-slate-800/60 flex items-center justify-between">
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="w-16 h-16 rounded-2xl bg-cyan-500/10 flex items-center justify-center text-cyan-500 border border-cyan-500/20">
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-black text-slate-800 dark:text-white leading-tight" x-text="selectedMachine.name"></h2>
|
||||
<p class="text-xs font-mono font-bold text-slate-400 mt-1 uppercase tracking-widest" x-text="selectedMachine.serial_no"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<span class="px-4 py-2 rounded-full bg-emerald-500/10 text-emerald-600 text-xs font-black uppercase tracking-widest border border-emerald-500/20 flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
{{ __('Connected') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
<!-- Quick Actions Card -->
|
||||
<div class="luxury-card rounded-[2.5rem] p-8 border border-slate-200/60 dark:border-slate-800/60 transition-all duration-300">
|
||||
<h3 class="text-lg font-black text-slate-800 dark:text-white uppercase tracking-wider mb-6 flex items-center gap-3">
|
||||
<span class="w-2 h-6 bg-cyan-500 rounded-full"></span>
|
||||
{{ __('Quick Maintenance') }}
|
||||
</h3>
|
||||
<div class="grid gap-4">
|
||||
<button @click="sendCommand('reboot')" class="luxury-card p-5 rounded-2xl border border-slate-100 dark:border-slate-800 flex items-center gap-4 hover:border-cyan-500/50 hover:bg-cyan-500/5 group transition-all">
|
||||
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-900 flex items-center justify-center text-slate-400 group-hover:text-cyan-500 group-hover:scale-110 transition-all duration-300">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('System Reboot') }}</div>
|
||||
<div class="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">{{ __('Restart entire machine') }}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button @click="sendCommand('reboot_card')" class="luxury-card p-5 rounded-2xl border border-slate-100 dark:border-slate-800 flex items-center gap-4 hover:border-cyan-500/50 hover:bg-cyan-500/5 group transition-all">
|
||||
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-900 flex items-center justify-center text-slate-400 group-hover:text-cyan-500 group-hover:scale-110 transition-all duration-300">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" /></svg>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Card Reader Reboot') }}</div>
|
||||
<div class="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">{{ __('Reset POS terminal') }}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button @click="sendCommand('checkout')" class="luxury-card p-5 rounded-2xl border border-slate-100 dark:border-slate-800 flex items-center gap-4 hover:border-emerald-500/50 hover:bg-emerald-500/5 group transition-all">
|
||||
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-900 flex items-center justify-center text-slate-400 group-hover:text-emerald-500 group-hover:scale-110 transition-all duration-300">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Remote Settlement') }}</div>
|
||||
<div class="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">{{ __('Force end current session') }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stateful Actions Card -->
|
||||
<div class="luxury-card rounded-[2.5rem] p-8 border border-slate-200/60 dark:border-slate-800/60 transition-all duration-300">
|
||||
<h3 class="text-lg font-black text-slate-800 dark:text-white uppercase tracking-wider mb-6 flex items-center gap-3">
|
||||
<span class="w-2 h-6 bg-rose-500 rounded-full"></span>
|
||||
{{ __('Security & State') }}
|
||||
</h3>
|
||||
<div class="space-y-6">
|
||||
<!-- Lock/Unlock Toggle -->
|
||||
<div class="p-6 rounded-[2rem] bg-slate-50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800/50 relative overflow-hidden group">
|
||||
<div class="flex items-center justify-between relative z-10">
|
||||
<div>
|
||||
<div class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Page Lock Status') }}</div>
|
||||
<div class="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">{{ __('Restrict machine UI access') }}</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button @click="sendCommand('unlock')" class="px-4 py-2 rounded-xl bg-emerald-500/10 text-emerald-600 text-[10px] font-black uppercase tracking-widest border border-emerald-500/20 hover:bg-emerald-500 hover:text-white transition-all">{{ __('Unlock') }}</button>
|
||||
<button @click="sendCommand('lock')" class="px-4 py-2 rounded-xl bg-rose-500/10 text-rose-600 text-[10px] font-black uppercase tracking-widest border border-rose-500/20 hover:bg-rose-500 hover:text-white transition-all">{{ __('Lock') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute -right-4 -bottom-4 opacity-[0.03] group-hover:scale-110 transition-transform duration-700">
|
||||
<svg class="w-24 h-24" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note Field -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1">{{ __('Operation Note') }}</label>
|
||||
<textarea x-model="note" class="luxury-input w-full min-h-[100px] text-sm py-4" placeholder="{{ __('Reason for this command...') }}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parametric Actions -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
<!-- Remote Change -->
|
||||
<div class="luxury-card rounded-[2.5rem] p-8 border border-slate-200/60 dark:border-slate-800/60">
|
||||
<h3 class="text-lg font-black text-slate-800 dark:text-white uppercase tracking-wider mb-6 flex items-center gap-3">
|
||||
<span class="w-2 h-6 bg-amber-500 rounded-full"></span>
|
||||
{{ __('Remote Change') }}
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4 bg-slate-50 dark:bg-slate-900/50 p-6 rounded-3xl border border-slate-100 dark:border-slate-800/50">
|
||||
<div class="text-3xl font-black text-slate-300 dark:text-slate-700 shrink-0">$</div>
|
||||
<input type="number" x-model="changeAmount" class="w-full bg-transparent border-none p-0 text-3xl font-black text-slate-800 dark:text-white focus:ring-0">
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<template x-for="amt in [10, 50, 100, 200]">
|
||||
<button @click="changeAmount = amt" class="py-2.5 rounded-xl border border-slate-200 dark:border-slate-800 text-[10px] font-black uppercase tracking-widest hover:border-cyan-500 hover:text-cyan-500 transition-all" x-text="amt"></button>
|
||||
</template>
|
||||
</div>
|
||||
<button @click="sendCommand('change', { amount: changeAmount })" :disabled="submitting" class="btn-luxury-primary w-full py-4 mt-2">
|
||||
{{ __('Execute Change') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remote Dispense -->
|
||||
<div class="luxury-card rounded-[2.5rem] p-8 border border-slate-200/60 dark:border-slate-800/60">
|
||||
<h3 class="text-lg font-black text-slate-800 dark:text-white uppercase tracking-wider mb-6 flex items-center gap-3">
|
||||
<span class="w-2 h-6 bg-violet-500 rounded-full"></span>
|
||||
{{ __('Remote Dispense') }}
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1">{{ __('Select Cargo Lane') }}</label>
|
||||
<template x-if="selectedMachine.slots && selectedMachine.slots.length > 0">
|
||||
<select x-model="selectedSlot" class="luxury-input w-full py-4 text-sm bg-white dark:bg-slate-900">
|
||||
<option value="">{{ __('Select Slot...') }}</option>
|
||||
<template x-for="slot in selectedMachine.slots" :key="slot.id">
|
||||
<option :value="slot.slot_no" x-text="'Slot ' + slot.slot_no + ' (' + (slot.product ? slot.product.name : 'Unknown') + ')'"></option>
|
||||
</template>
|
||||
</select>
|
||||
</template>
|
||||
<template x-if="!selectedMachine.slots || selectedMachine.slots.length == 0">
|
||||
<div class="p-4 rounded-xl bg-rose-50 text-rose-500 text-[10px] font-black uppercase text-center border border-rose-100">
|
||||
{{ __('No active cargo lanes found') }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<button @click="sendCommand('dispense', { slot_no: selectedSlot })"
|
||||
:disabled="submitting || !selectedSlot"
|
||||
class="btn-luxury-ghost w-full py-4 mt-2 border-violet-500/30 text-violet-600 dark:text-violet-400 hover:bg-violet-500 hover:text-white transition-all shadow-sm">
|
||||
{{ __('Trigger Dispense') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Command History (Sidebar) -->
|
||||
<div class="space-y-6">
|
||||
<h3 class="text-lg font-black text-slate-800 dark:text-white uppercase tracking-wider flex items-center gap-3">
|
||||
<span class="w-2 h-6 bg-slate-300 dark:bg-slate-700 rounded-full"></span>
|
||||
{{ __('Recent Commands') }}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4 max-h-[1000px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
<template x-for="cmd in commands" :key="cmd.id">
|
||||
<div class="luxury-card p-5 rounded-[2rem] border border-slate-100 dark:border-slate-800 transition-all hover:bg-slate-50 dark:hover:bg-slate-900/40 relative group">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<span class="text-xs font-black text-slate-800 dark:text-white" x-text="getCommandName(cmd.command_type)"></span>
|
||||
<span :class="getCommandBadgeClass(cmd.status)" class="px-2.5 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider border" x-text="cmd.status"></span>
|
||||
</div>
|
||||
<div class="text-[9px] font-bold text-slate-400 flex items-center gap-2 mb-2">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span x-text="new Date(cmd.created_at).toLocaleString()"></span>
|
||||
</div>
|
||||
<template x-if="cmd.payload && Object.keys(cmd.payload).length > 0">
|
||||
<div class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-[10px] font-mono text-slate-500 dark:text-slate-400 break-all">
|
||||
<span x-text="JSON.stringify(cmd.payload)"></span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="cmd.note">
|
||||
<div class="mt-2 text-[10px] font-bold text-slate-400 italic" x-text="'\"' + cmd.note + '\"'"></div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="commands.length === 0">
|
||||
<div class="py-20 text-center flex flex-col items-center opacity-30">
|
||||
<svg class="w-12 h-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
||||
<div class="text-[10px] font-black uppercase tracking-widest">{{ __('No command history') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Custom Scrollbar for Luxury UI */
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e133; border-radius: 10px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #cbd5e166; }
|
||||
|
||||
/* Hide default number spinners */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: none;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@@ -236,7 +236,7 @@
|
||||
<!-- End Sidebar -->
|
||||
|
||||
<!-- Content -->
|
||||
<div class="w-full pt-6 lg:pt-10 pb-12 px-4 sm:px-6 md:px-8 lg:pl-72">
|
||||
<div class="w-full pt-4 lg:pt-5 pb-12 px-4 sm:px-6 md:px-8 lg:pl-72">
|
||||
<x-breadcrumbs class="mb-4 hidden lg:flex" />
|
||||
<main class="animate-fade-up">
|
||||
@yield('content')
|
||||
|
||||
@@ -259,13 +259,14 @@
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.stock') ? '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.remote.stock') }}">{{ __('Machine Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.restart') ? '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.remote.restart') }}">{{ __('Machine Restart') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.restart-card-reader') ? '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.remote.restart-card-reader') }}">{{ __('Card Reader Restart') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.checkout') ? '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.remote.checkout') }}">{{ __('Remote Checkout') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.lock') ? '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.remote.lock') }}">{{ __('Remote Lock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.change') ? '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.remote.change') }}">{{ __('Remote Change') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.dispense') ? '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.remote.dispense') }}">{{ __('Remote Dispense') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.index') }}">
|
||||
<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 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
|
||||
{{ __('Command Center') }}
|
||||
</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.stock') ? '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.remote.stock') }}">
|
||||
<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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
|
||||
{{ __('Machine Stock') }}
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -144,13 +144,9 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
|
||||
// 10. 遠端管理
|
||||
Route::prefix('remote')->name('remote.')->group(function () {
|
||||
Route::get('/', [App\Http\Controllers\Admin\RemoteController::class, 'index'])->name('index');
|
||||
Route::post('/command', [App\Http\Controllers\Admin\RemoteController::class, 'storeCommand'])->name('store-command');
|
||||
Route::get('/stock', [App\Http\Controllers\Admin\RemoteController::class, 'stock'])->name('stock');
|
||||
Route::get('/restart', [App\Http\Controllers\Admin\RemoteController::class, 'restart'])->name('restart');
|
||||
Route::get('/restart-card-reader', [App\Http\Controllers\Admin\RemoteController::class, 'restartCardReader'])->name('restart-card-reader');
|
||||
Route::get('/checkout', [App\Http\Controllers\Admin\RemoteController::class, 'checkout'])->name('checkout');
|
||||
Route::get('/lock', [App\Http\Controllers\Admin\RemoteController::class, 'lock'])->name('lock');
|
||||
Route::get('/change', [App\Http\Controllers\Admin\RemoteController::class, 'change'])->name('change');
|
||||
Route::get('/dispense', [App\Http\Controllers\Admin\RemoteController::class, 'dispense'])->name('dispense');
|
||||
});
|
||||
|
||||
// 11. Line管理
|
||||
|
||||
Reference in New Issue
Block a user