[FIX] 修正 IoT 管理介面分頁持久化與實作 B055 遠端出貨 API
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 3m58s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 3m58s
1. 視圖持久化優化:將 index/stock 視圖切換從 x-if 改為 x-show,解決 HSSelect 在切換分頁後失效的問題。 2. 變數存取安全:為所有 selectedMachine 屬性存取補上可選鏈 (?.) 保護,防止 x-show 模式下的 null 錯誤。 3. UI 體驗提升:放大「指令中心」與「庫存管理」歷史紀錄的時間字體至 15px 並加粗顯示。 4. API 功能實作:在 routes/api.php 與 MachineController 中實作 B055 遠端指令出貨控制端點。 5. 文件同步:更新 SKILL.md 技術規格文件,明確定義 B055 的請求與回應格式。 6. 樣式調整:修改 app.css 優化奢華風 UI 字體與間距細節。
This commit is contained in:
@@ -341,16 +341,22 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
||||
### 3.11 B055: 遠端指令出貨控制 (Remote Dispense / Force Open)
|
||||
用於遠端手動驅動機台出貨。通常用於補償使用者、測試機台或客服協助開門的情景。
|
||||
|
||||
- **URL**: POST|PUT /api/v1/app/machine/dispense/B055
|
||||
- **URL**: GET|PUT /api/v1/app/machine/dispense/B055
|
||||
- **Authentication**: Bearer Token (Header)
|
||||
- **運作模式**:
|
||||
- **POST (查詢)**: 當 B010 收到 `status: 85` 時呼叫。雲端會回傳待執行的貨道編號與指令 ID。
|
||||
- **PUT (回報)**: 實體出貨完成後回報結果,以便雲端將該指令標記為「已執行」。
|
||||
- **GET (查詢)**:當 B010 收到 `status: 85` 時呼叫。雲端會回傳待執行的貨道編號與指令 ID。
|
||||
- **PUT (回報)**:實體出貨完成後回饋結果,以便雲端將該指令標記為「已執行」。
|
||||
|
||||
- **Response Body (GET - 查詢階段):**
|
||||
|
||||
| 參數 | 類型 | 說明 | 範例 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| data | Array | 指令物件陣列 | [{"res1": "ID", "res2": "SlotNo"}] |
|
||||
|
||||
- **Request Body (PUT - 回報階段):**
|
||||
|
||||
| 參數 | 類型 | 必填 | 說明 | 範例 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| id | String | 是 | 雲端下發的指令 ID | "20260414001" |
|
||||
| type | String | 是 | 出貨類型代碼 (通常為 0) | "0" |
|
||||
| id | String | 是 | 雲端下發的指令 ID | "99" |
|
||||
| type | String | 是 | 出貨類型代碼 (0: 成功, 其他: 失敗) | "0" |
|
||||
| stock | String | 是 | 出貨後的貨道剩餘數量 | "9" |
|
||||
|
||||
@@ -17,7 +17,49 @@ class RemoteController extends Controller
|
||||
{
|
||||
$machines = Machine::withCount(['slots'])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc')->get();
|
||||
$selectedMachine = null;
|
||||
$history = RemoteCommand::where('command_type', '!=', 'reload_stock')->with(['machine', 'user'])->latest()->limit(50)->get();
|
||||
|
||||
$historyQuery = RemoteCommand::where('command_type', '!=', 'reload_stock')
|
||||
->with(['machine', 'user']);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->input('search');
|
||||
$historyQuery->where(function($q) use ($search) {
|
||||
$q->whereHas('machine', function($mq) use ($search) {
|
||||
$mq->where('name', 'like', "%{$search}%")
|
||||
->orWhere('serial_no', 'like', "%{$search}%");
|
||||
})->orWhereHas('user', function($uq) use ($search) {
|
||||
$uq->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 時間區間過濾 (created_at)
|
||||
if ($request->filled('start_date') || $request->filled('end_date')) {
|
||||
try {
|
||||
if ($request->filled('start_date')) {
|
||||
$start = \Illuminate\Support\Carbon::parse($request->input('start_date'));
|
||||
$historyQuery->where('created_at', '>=', $start);
|
||||
}
|
||||
if ($request->filled('end_date')) {
|
||||
$end = \Illuminate\Support\Carbon::parse($request->input('end_date'));
|
||||
$historyQuery->where('created_at', '<=', $end);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 忽略解析錯誤
|
||||
}
|
||||
}
|
||||
|
||||
// 指令類型過濾
|
||||
if ($request->filled('command_type')) {
|
||||
$historyQuery->where('command_type', $request->input('command_type'));
|
||||
}
|
||||
|
||||
// 狀態過濾
|
||||
if ($request->filled('status')) {
|
||||
$historyQuery->where('status', $request->input('status'));
|
||||
}
|
||||
|
||||
$history = $historyQuery->latest()->paginate($request->input('per_page', 10));
|
||||
|
||||
if ($request->has('machine_id')) {
|
||||
$selectedMachine = Machine::with(['slots.product', 'commands' => function($query) {
|
||||
@@ -112,7 +154,44 @@ class RemoteController extends Controller
|
||||
}
|
||||
])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc')->get();
|
||||
|
||||
$history = RemoteCommand::where('command_type', 'reload_stock')->with(['machine', 'user'])->latest()->limit(50)->get();
|
||||
$historyQuery = RemoteCommand::with(['machine', 'user']);
|
||||
|
||||
$historyQuery->where('command_type', 'reload_stock');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->input('search');
|
||||
$historyQuery->where(function($q) use ($search) {
|
||||
$q->whereHas('machine', function($mq) use ($search) {
|
||||
$mq->where('name', 'like', "%{$search}%")
|
||||
->orWhere('serial_no', 'like', "%{$search}%");
|
||||
})->orWhereHas('user', function($uq) use ($search) {
|
||||
$uq->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 時間區間過濾 (created_at)
|
||||
if ($request->filled('start_date') || $request->filled('end_date')) {
|
||||
try {
|
||||
if ($request->filled('start_date')) {
|
||||
$start = \Illuminate\Support\Carbon::parse($request->input('start_date'));
|
||||
$historyQuery->where('created_at', '>=', $start);
|
||||
}
|
||||
if ($request->filled('end_date')) {
|
||||
$end = \Illuminate\Support\Carbon::parse($request->input('end_date'));
|
||||
$historyQuery->where('created_at', '<=', $end);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 忽略解析錯誤
|
||||
}
|
||||
}
|
||||
|
||||
// 狀態過濾
|
||||
if ($request->filled('status')) {
|
||||
$historyQuery->where('status', $request->input('status'));
|
||||
}
|
||||
|
||||
$history = $historyQuery->latest()->paginate($request->input('per_page', 10));
|
||||
|
||||
$selectedMachine = null;
|
||||
if ($request->has('machine_id')) {
|
||||
|
||||
@@ -146,6 +146,114 @@ class MachineController extends Controller
|
||||
], 202); // 202 Accepted
|
||||
}
|
||||
|
||||
/**
|
||||
* B055: Get Remote Dispense Queue (POST/GET)
|
||||
* 用於獲取雲端下發的遠端出貨指令詳情。
|
||||
*/
|
||||
public function getDispenseQueue(Request $request)
|
||||
{
|
||||
$machine = $request->get('machine');
|
||||
|
||||
// 查找該機台狀態為「已發送 (sent)」且類型為「出貨 (dispense)」的最新指令
|
||||
$command = \App\Models\Machine\RemoteCommand::where('machine_id', $machine->id)
|
||||
->where('command_type', 'dispense')
|
||||
->where('status', 'sent')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if (!$command) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
|
||||
// 映射 Java APP 預期格式: res1 = ID, res2 = SlotNo
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'data' => [
|
||||
[
|
||||
'res1' => (string) $command->id,
|
||||
'res2' => (string) ($command->payload['slot_no'] ?? ''),
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* B055: Report Remote Dispense Result (PUT)
|
||||
* 用於機台回報遠端出貨執行的最終結果。
|
||||
*/
|
||||
public function reportDispenseResult(Request $request, \App\Services\Machine\MachineService $machineService)
|
||||
{
|
||||
$commandId = $request->input('id');
|
||||
$type = $request->input('type'); // 通常為 0
|
||||
$stockReported = $request->input('stock'); // 機台回報的剩餘庫存
|
||||
|
||||
$command = \App\Models\Machine\RemoteCommand::find($commandId);
|
||||
|
||||
if (!$command) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'code' => 404,
|
||||
'message' => 'Command not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// 更新指令狀態 (0 代表成功)
|
||||
$status = ($type == '0') ? 'success' : 'failed';
|
||||
|
||||
$payloadUpdates = [
|
||||
'reported_stock' => $stockReported,
|
||||
'report_type' => $type
|
||||
];
|
||||
|
||||
// 若成功,連動更新貨道庫存
|
||||
if ($status === 'success' && isset($command->payload['slot_no'])) {
|
||||
$machine = $command->machine;
|
||||
$slotNo = $command->payload['slot_no'];
|
||||
|
||||
$slot = $machine->slots()->where('slot_no', $slotNo)->first();
|
||||
if ($slot) {
|
||||
$oldStock = $slot->stock;
|
||||
// 若 APP 回傳的庫存大於 0 則使用回傳值,否則執行扣減
|
||||
$newStock = (int)($stockReported ?? 0);
|
||||
if ($newStock <= 0 && $oldStock > 0) {
|
||||
$newStock = $oldStock - 1;
|
||||
}
|
||||
|
||||
$finalStock = max(0, $newStock);
|
||||
$slot->update(['stock' => $finalStock]);
|
||||
|
||||
// 紀錄庫存變化供前端顯示
|
||||
$payloadUpdates['old_stock'] = $oldStock;
|
||||
$payloadUpdates['new_stock'] = $finalStock;
|
||||
|
||||
// 記錄狀態變化
|
||||
ProcessStateLog::dispatch(
|
||||
$machine->id,
|
||||
$machine->company_id,
|
||||
"Remote dispense successful for slot {$slotNo}",
|
||||
'info'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$command->update([
|
||||
'status' => $status,
|
||||
'executed_at' => now(),
|
||||
'payload' => array_merge($command->payload, $payloadUpdates)
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => 'Result reported'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* B018: Record Machine Restock/Setup Report (Asynchronous)
|
||||
*/
|
||||
|
||||
@@ -62,9 +62,11 @@
|
||||
"All": "全部",
|
||||
"All Affiliations": "所有公司",
|
||||
"All Categories": "所有分類",
|
||||
"All Command Types": "所有指令類型",
|
||||
"All Companies": "所有公司",
|
||||
"All Levels": "所有層級",
|
||||
"All Machines": "所有機台",
|
||||
"All Status": "所有狀態",
|
||||
"All Stable": "狀態穩定",
|
||||
"All Times System Timezone": "所有時間為系統時區",
|
||||
"Amount": "金額",
|
||||
@@ -274,6 +276,7 @@
|
||||
"Discord Notifications": "Discord通知",
|
||||
"Dispense Failed": "出貨失敗",
|
||||
"Dispense Success": "出貨成功",
|
||||
"Displaying": "目前顯示",
|
||||
"Dispensing": "出貨",
|
||||
"Duration": "時長",
|
||||
"Duration (Seconds)": "播放秒數",
|
||||
@@ -393,7 +396,7 @@
|
||||
"Initial Role": "初始角色",
|
||||
"Installation": "裝機",
|
||||
"Invoice Status": "發票開立狀態",
|
||||
"Items": "個項目",
|
||||
"Items": "筆",
|
||||
"items": "筆項目",
|
||||
"Japanese": "日文",
|
||||
"JKO_MERCHANT_ID": "街口支付 商店代號",
|
||||
@@ -631,7 +634,7 @@
|
||||
"OEE.Hours": "小時",
|
||||
"OEE.Orders": "訂單",
|
||||
"OEE.Sales": "銷售",
|
||||
"of": "總計",
|
||||
"of": "/共",
|
||||
"Offline": "離線",
|
||||
"Offline Machines": "離線機台",
|
||||
"Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "一旦您的帳號被刪除,其所有資源和數據將被永久刪除。在刪除帳號之前,請下載您希望保留的任何數據或資訊。",
|
||||
@@ -853,6 +856,7 @@
|
||||
"Select Category": "選擇類別",
|
||||
"Select Company": "選擇公司名稱",
|
||||
"Select Company (Default: System)": "選擇公司 (預設:系統)",
|
||||
"Select Date Range": "選擇建立日期區間",
|
||||
"Select date to sync data": "選擇日期以同步數據",
|
||||
"Select Machine": "選擇機台",
|
||||
"Select Machine to view metrics": "請選擇機台以查看指標",
|
||||
@@ -908,6 +912,7 @@
|
||||
"Stock & Expiry Overview": "庫存與效期一覽",
|
||||
"Stock Management": "庫存管理單",
|
||||
"Stock Quantity": "庫存數量",
|
||||
"Stock Update": "同步庫存",
|
||||
"Stock:": "庫存:",
|
||||
"Store Gifts": "來店禮",
|
||||
"Store ID": "商店代號",
|
||||
|
||||
@@ -373,11 +373,27 @@
|
||||
color: #06b6d4 !important;
|
||||
}
|
||||
|
||||
.flatpickr-day.selected {
|
||||
.flatpickr-day.selected,
|
||||
.flatpickr-day.startRange,
|
||||
.flatpickr-day.endRange {
|
||||
background: linear-gradient(135deg, #06b6d4, #3b82f6) !important;
|
||||
border-color: transparent !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 8px 15px -3px rgba(6, 182, 212, 0.4) !important;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.flatpickr-day.inRange {
|
||||
background: #ecfeff !important;
|
||||
border-color: transparent !important;
|
||||
box-shadow: -5px 0 0 #ecfeff, 5px 0 0 #ecfeff !important;
|
||||
color: #0891b2 !important;
|
||||
}
|
||||
|
||||
.dark .flatpickr-day.inRange {
|
||||
background: rgba(6, 182, 212, 0.15) !important;
|
||||
box-shadow: -5px 0 0 rgba(6, 182, 212, 0.15), 5px 0 0 rgba(6, 182, 212, 0.15) !important;
|
||||
color: #22d3ee !important;
|
||||
}
|
||||
|
||||
.flatpickr-day:not(.selected):hover {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,8 @@ window.stockApp = function(initialMachineId) {
|
||||
searchQuery: '',
|
||||
selectedMachine: null,
|
||||
slots: [],
|
||||
viewMode: initialMachineId ? 'detail' : 'history',
|
||||
viewMode: initialMachineId ? 'detail' : (new URLSearchParams(window.location.search).has('search') || new URLSearchParams(window.location.search).has('page') ? 'history' : 'history'),
|
||||
// 預設為 history,但我們會確保在有搜尋或分頁時維持 history
|
||||
history: @js($history),
|
||||
loading: false,
|
||||
updating: false,
|
||||
@@ -271,9 +272,89 @@ window.stockApp = function(initialMachineId) {
|
||||
<div class="mt-6">
|
||||
|
||||
<!-- History View: Operation Records -->
|
||||
<template x-if="viewMode === 'history'">
|
||||
<div x-show="viewMode === 'history'" x-cloak>
|
||||
<div class="space-y-6 animate-luxury-in">
|
||||
<div class="luxury-card rounded-3xl p-8 overflow-hidden">
|
||||
<!-- Filters Area -->
|
||||
<div class="mb-8">
|
||||
<form method="GET" action="{{ route('admin.remote.stock') }}" class="flex flex-wrap items-center gap-4">
|
||||
<!-- Search Box -->
|
||||
<div class="relative group flex-[1.5] min-w-[200px]">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
placeholder="{{ __('Search machines...') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full">
|
||||
</div>
|
||||
|
||||
<!-- Date Range -->
|
||||
<div class="relative group flex-[2] min-w-[340px]"
|
||||
x-data="{
|
||||
fp: null,
|
||||
startDate: '{{ request('start_date') }}',
|
||||
endDate: '{{ request('end_date') }}'
|
||||
}"
|
||||
x-init="fp = flatpickr($refs.dateRange, {
|
||||
mode: 'range',
|
||||
dateFormat: 'Y-m-d H:i', enableTime: true, time_24hr: true,
|
||||
locale: 'zh_tw',
|
||||
defaultDate: startDate && endDate ? [startDate, endDate] : (startDate ? [startDate] : []),
|
||||
onChange: function(selectedDates, dateStr, instance) {
|
||||
if (selectedDates.length === 2) {
|
||||
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
|
||||
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
|
||||
} else if (selectedDates.length === 0) {
|
||||
$refs.startDate.value = '';
|
||||
$refs.endDate.value = '';
|
||||
}
|
||||
}
|
||||
})">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
</span>
|
||||
<input type="hidden" name="start_date" x-ref="startDate" value="{{ request('start_date') }}">
|
||||
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
|
||||
<input type="text" x-ref="dateRange"
|
||||
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
|
||||
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
||||
</div>
|
||||
|
||||
<!-- Command Type -->
|
||||
<!-- Status -->
|
||||
<div class="flex-1 min-w-[160px]">
|
||||
<x-searchable-select
|
||||
name="status"
|
||||
:options="[
|
||||
'pending' => __('Pending'),
|
||||
'sent' => __('Sent'),
|
||||
'success' => __('Success'),
|
||||
'failed' => __('Failed'),
|
||||
'superseded' => __('Superseded'),
|
||||
]"
|
||||
:selected="request('status')"
|
||||
:placeholder="__('All Status')"
|
||||
:hasSearch="false"
|
||||
onchange="this.form.submit()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="submit" class="p-2.5 rounded-xl bg-cyan-600 text-white hover:bg-cyan-500 shadow-lg shadow-cyan-500/20 transition-all active:scale-95">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||
</button>
|
||||
<a href="{{ route('admin.remote.stock') }}" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0 text-sm">
|
||||
<thead>
|
||||
@@ -287,9 +368,9 @@ window.stockApp = function(initialMachineId) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
<template x-for="item in history" :key="item.id">
|
||||
@foreach ($history as $item)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6 cursor-pointer" @click="selectMachine(item.machine)">
|
||||
<td class="px-6 py-6 cursor-pointer" @click="selectMachine(@js($item->machine))">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 shadow-sm overflow-hidden">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
@@ -297,67 +378,65 @@ window.stockApp = function(initialMachineId) {
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight" x-text="item.machine.name"></div>
|
||||
<div class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5" x-text="item.machine.serial_no"></div>
|
||||
<div class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">{{ $item->machine->name }}</div>
|
||||
<div class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ $item->machine->serial_no }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
|
||||
<div class="flex flex-col">
|
||||
<span x-text="new Date(item.created_at).toLocaleDateString()"></span>
|
||||
<span class="text-[10px] opacity-70" x-text="new Date(item.created_at).toLocaleTimeString()"></span>
|
||||
<span>{{ $item->created_at->format('Y/m/d') }}</span>
|
||||
<span class="text-[15px] font-bold text-slate-500 dark:text-slate-400">{{ $item->created_at->format('H:i:s') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
|
||||
<template x-if="item.executed_at">
|
||||
@if($item->executed_at)
|
||||
<div class="flex flex-col text-cyan-600/80 dark:text-cyan-400/60">
|
||||
<span x-text="new Date(item.executed_at).toLocaleDateString()"></span>
|
||||
<span class="text-[10px] opacity-70" x-text="new Date(item.executed_at).toLocaleTimeString()"></span>
|
||||
<span>{{ $item->executed_at->format('Y/m/d') }}</span>
|
||||
<span class="text-[15px] font-bold">{{ $item->executed_at->format('H:i:s') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!item.executed_at">
|
||||
@else
|
||||
<span class="text-slate-300 dark:text-slate-700">-</span>
|
||||
</template>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex flex-col min-w-[200px]">
|
||||
<span class="text-sm font-black text-slate-700 dark:text-slate-300 tracking-tight" x-text="getCommandName(item.command_type)"></span>
|
||||
<span class="text-sm font-black text-slate-700 dark:text-slate-300 tracking-tight" x-text="getCommandName(@js($item->command_type))"></span>
|
||||
<div class="flex flex-col gap-0.5 mt-1">
|
||||
<template x-if="getPayloadDetails(item)">
|
||||
<span class="text-[11px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit" x-text="getPayloadDetails(item)"></span>
|
||||
</template>
|
||||
<template x-if="item.note">
|
||||
<span class="text-[10px] text-slate-400 italic pl-1" x-text="translateNote(item.note)"></span>
|
||||
</template>
|
||||
<span x-show="getPayloadDetails(@js($item))" class="text-[11px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit" x-text="getPayloadDetails(@js($item))"></span>
|
||||
@if($item->note)
|
||||
<span class="text-[10px] text-slate-400 italic pl-1" x-text="translateNote(@js($item->note))"></span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 whitespace-nowrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-6 h-6 rounded-full bg-cyan-500/10 flex items-center justify-center text-[10px] font-black text-cyan-600 dark:text-cyan-400 border border-cyan-500/20"
|
||||
x-text="getOperatorName(item.user).substring(0,1)"></div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300" x-text="getOperatorName(item.user)"></span>
|
||||
<div class="w-6 h-6 rounded-full bg-cyan-500/10 flex items-center justify-center text-[10px] font-black text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
|
||||
{{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }}
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $item->user ? $item->user->name : __('System') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-center">
|
||||
<div class="flex flex-col items-center gap-1.5">
|
||||
<div class="inline-flex items-center px-4 py-1.5 rounded-full border text-[10px] font-black uppercase tracking-widest shadow-sm"
|
||||
:class="getCommandBadgeClass(item.status)">
|
||||
:class="getCommandBadgeClass(@js($item->status))">
|
||||
<div class="w-1.5 h-1.5 rounded-full mr-2"
|
||||
:class="{
|
||||
'bg-amber-500 animate-pulse': item.status === 'pending',
|
||||
'bg-cyan-500': item.status === 'sent',
|
||||
'bg-emerald-500': item.status === 'success',
|
||||
'bg-rose-500': item.status === 'failed',
|
||||
'bg-slate-400': item.status === 'superseded'
|
||||
'bg-amber-500 animate-pulse': @js($item->status) === 'pending',
|
||||
'bg-cyan-500': @js($item->status) === 'sent',
|
||||
'bg-emerald-500': @js($item->status) === 'success',
|
||||
'bg-rose-500': @js($item->status) === 'failed',
|
||||
'bg-slate-400': @js($item->status) === 'superseded'
|
||||
}"></div>
|
||||
<span x-text="getCommandStatus(item.status)"></span>
|
||||
<span x-text="getCommandStatus(@js($item->status))"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="history.length === 0">
|
||||
@endforeach
|
||||
@if($history->isEmpty())
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
@@ -370,17 +449,22 @@ window.stockApp = function(initialMachineId) {
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Area -->
|
||||
<div class="mt-8">
|
||||
{{ $history->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Master View: Machine List -->
|
||||
<template x-if="viewMode === 'list'">
|
||||
<div x-show="viewMode === 'list'" x-cloak>
|
||||
<div class="space-y-6 animate-luxury-in">
|
||||
<div class="luxury-card rounded-3xl p-8 overflow-hidden">
|
||||
<!-- Filters Area -->
|
||||
@@ -503,10 +587,10 @@ window.stockApp = function(initialMachineId) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Detail View: Cabinet Management -->
|
||||
<template x-if="viewMode === 'detail'">
|
||||
<div x-show="viewMode === 'detail'" x-cloak>
|
||||
<div class="space-y-8 animate-luxury-in">
|
||||
|
||||
<!-- Machine Header Info -->
|
||||
@@ -646,7 +730,7 @@ window.stockApp = function(initialMachineId) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Integrated Edit Modal -->
|
||||
<div x-show="showEditModal"
|
||||
|
||||
@@ -74,6 +74,10 @@ Route::prefix('v1')->middleware(['throttle:api'])->group(function () {
|
||||
// 機台故障與異常上報 (B013)
|
||||
Route::post('machine/error/B013', [App\Http\Controllers\Api\V1\App\MachineController::class, 'reportError']);
|
||||
|
||||
// 遠端指令出貨控制 (B055)
|
||||
Route::get('machine/dispense/B055', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getDispenseQueue']);
|
||||
Route::put('machine/dispense/B055', [App\Http\Controllers\Api\V1\App\MachineController::class, 'reportDispenseResult']);
|
||||
|
||||
// 交易、發票與出貨 (B600, B601, B602)
|
||||
Route::post('machine/restock/B018', [App\Http\Controllers\Api\V1\App\MachineController::class, 'recordRestock']);
|
||||
Route::post('B600', [App\Http\Controllers\Api\V1\App\TransactionController::class, 'store']);
|
||||
|
||||
Reference in New Issue
Block a user