diff --git a/.agents/skills/api-technical-specs/SKILL.md b/.agents/skills/api-technical-specs/SKILL.md index 4ac7127..e0e2738 100644 --- a/.agents/skills/api-technical-specs/SKILL.md +++ b/.agents/skills/api-technical-specs/SKILL.md @@ -24,6 +24,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 - **URL**: POST /api/v1/app/admin/login/B000 - **Request Body:** + | 參數 | 類型 | 必填 | 說明 | 範例 | | :--- | :--- | :--- | :--- | :--- | | machine | String | 是 | 機台編號 (serial_no) | M-001 | @@ -35,6 +36,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 - **Response Body:** > [!IMPORTANT] > 為了相容 Java APP 現有邏輯,這裡嚴格規定成功必須回傳字串 Success。 + | 參數 | 類型 | 說明 | 範例 | | :--- | :--- | :--- | :--- | | message | String | 驗證結果 (Success 或 Failed) | Success | @@ -48,6 +50,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 - **Request Body:** 無 (GET 請求) - **Response Body:** + | 參數 | 類型 | 說明 | 範例 | | :--- | :--- | :--- | :--- | | success | Boolean | 請求是否成功 | true | @@ -74,12 +77,14 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 - **URL**: PUT /api/v1/app/products/supplementary/B009 - **Request Body:** + | 參數 | 類型 | 必填 | 說明 | 範例 | | :--- | :--- | :--- | :--- | :--- | | account | String | 是 | 操作人員帳號 | 0999123456 | | data | Array | 是 | 貨道數據陣列 | [{"tid":"1", "t060v00":"1", "num":"10"}] | - **data 陣列內部欄位:** + | 欄位 | 類型 | 說明 | 範例 | | :--- | :--- | :--- | :--- | | tid | Integer | 貨道編號 (Slot No) | 1 | @@ -92,6 +97,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 > 當後端收到 B009 時,會根據 type 自動從該商品的配置中選取 spring_limit 或 track_limit 並自動更新該貨道的 max_stock 欄位。機台端無需手動計算上限。 - **Response Body (Success 200):** + | 參數 | 類型 | 說明 | 範例 | | :--- | :--- | :--- | :--- | | success | Boolean | 同步是否成功 | true | @@ -107,6 +113,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 - **URL**: POST /api/v1/app/machine/status/B010 - **Authentication**: Bearer Token (Header) - **Request Body:** + | 參數 | 類型 | 必填 | 說明 | 範例 | | :--- | :--- | :--- | :--- | :--- | | current_page | Integer | 是 | 當前頁面代碼 (見下表) | 1 | @@ -119,6 +126,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 | log_payload | Object | 否 | 額外日誌 JSON 對象 | {"code":500} | - **Response Body:** + | 參數 | 類型 | 說明 | 範例 | | :--- | :--- | :--- | :--- | | success | Boolean | 請求是否處理成功 | true | @@ -175,3 +183,36 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 | t060v41 | String | 物料編碼 (Material Code) | SKU-001 | | spring_limit | Integer | **彈簧貨道上限** (建議使用此欄位) | 10 | | track_limit | Integer | **履帶貨道上限** (建議使用此欄位) | 15 | + +--- + +### 3.6 B013: 機台故障與異常狀態上報 (Error/Status Report) +用於接收機台發出的即時硬體狀態代碼(如卡貨、門未關),並自動由雲端後端翻譯為易讀日誌。 + +- **URL**: POST /api/v1/app/machine/error/B013 +- **Authentication**: Bearer Token (Header) +- **Request Body:** + +| 參數 | 類型 | 必填 | 說明 | 範例 | +| :--- | :--- | :--- | :--- | :--- | +| tid | Integer | 否 | 涉及之貨道編號 (Slot No) | 12 | +| error_code | String | 是 | 硬體狀態代碼 (4 位 16 進位) | "0403" | + +- **回應 (Success 202):** + +| 參數 | 類型 | 說明 | 範例 | +| :--- | :--- | :--- | :--- | +| success | Boolean | 請求已接收 | true | +| code | Integer | 200 | 200 | + +#### B013 硬體代碼對照表 (由 MachineService 自動翻譯) + +| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 | +| :--- | :--- | :--- | :--- | +| **0402** | Dispense successful | info | 出貨成功 | +| **0403** | Slot jammed | error | **貨道卡貨** (重大異常) | +| **0202** | Product empty | warning | 貨道缺貨 | +| **0412** | Elevator rise error | error | 昇降機上升異常 | +| **0415** | Pickup door error | error | 取貨門異常 | +| **5402** | Pickup door not closed | warning | **取貨門未關** (警告) | +| **5403** | Elevator failure | error | 昇降系統故障 | diff --git a/app/Http/Controllers/Admin/MachineController.php b/app/Http/Controllers/Admin/MachineController.php index c8ce46a..f732afb 100644 --- a/app/Http/Controllers/Admin/MachineController.php +++ b/app/Http/Controllers/Admin/MachineController.php @@ -71,17 +71,21 @@ class MachineController extends AdminController */ public function logsAjax(Request $request, Machine $machine) { - $per_page = $request->input('per_page', 10); + $per_page = $request->input('per_page', 20); - $startDate = $request->get('start_date', now()->format('Y-m-d')); - $endDate = $request->get('end_date', now()->format('Y-m-d')); + $startDate = $request->get('start_date'); + $endDate = $request->get('end_date'); $logs = $machine->logs() ->when($request->level, function ($query, $level) { return $query->where('level', $level); }) - ->whereDate('created_at', '>=', $startDate) - ->whereDate('created_at', '<=', $endDate) + ->when($startDate, function ($query, $start) { + return $query->where('created_at', '>=', str_replace('T', ' ', $start)); + }) + ->when($endDate, function ($query, $end) { + return $query->where('created_at', '<=', str_replace('T', ' ', $end)); + }) ->when($request->type, function ($query, $type) { return $query->where('type', $type); }) diff --git a/app/Http/Controllers/Admin/PermissionController.php b/app/Http/Controllers/Admin/PermissionController.php index 46ac8c2..5a909e1 100644 --- a/app/Http/Controllers/Admin/PermissionController.php +++ b/app/Http/Controllers/Admin/PermissionController.php @@ -318,7 +318,11 @@ class PermissionController extends Controller } if ($user->isSystemAdmin() && $request->filled('company_id')) { - $query->where('company_id', $request->company_id); + if ($request->company_id === 'system') { + $query->whereNull('company_id'); + } else { + $query->where('company_id', $request->company_id); + } } $per_page = $request->input('per_page', 10); diff --git a/app/Http/Controllers/Api/V1/App/MachineAuthController.php b/app/Http/Controllers/Api/V1/App/MachineAuthController.php index 601e3dc..8b38da9 100644 --- a/app/Http/Controllers/Api/V1/App/MachineAuthController.php +++ b/app/Http/Controllers/Api/V1/App/MachineAuthController.php @@ -9,6 +9,7 @@ use App\Models\System\User; use App\Models\Machine\Machine; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Log; +use App\Jobs\Machine\ProcessStateLog; class MachineAuthController extends Controller { @@ -20,30 +21,14 @@ class MachineAuthController extends Controller { // 1. 驗證欄位 (相容舊版 Java App 發送的 JSON 格式) $validated = $request->validate([ - 'machine' => 'required|string', - 'Su_Account' => 'required|string', + 'machine' => 'required|string', + 'Su_Account' => 'required|string', 'Su_Password' => 'required|string', - 'ip' => 'nullable|string', - 'type' => 'nullable|string', + 'ip' => 'nullable|string', + 'type' => 'nullable|string', ]); - // 2. 透過帳號尋找使用者 (允許使用 username 或 email) - $user = User::where('username', $validated['Su_Account']) - ->orWhere('email', $validated['Su_Account']) - ->first(); - - // 若無此帳號或密碼錯誤 - if (!$user || !Hash::check($validated['Su_Password'], $user->password)) { - Log::warning("B000 機台登入失敗: 帳密錯誤", [ - 'account' => $validated['Su_Account'], - 'machine' => $validated['machine'] - ]); - // 按現行設定,Java 端只認 Success 字串,其餘視為帳密錯誤 - return response()->json(['message' => 'Failed']); - } - - // 3. 取得機台物件 - // 因為此 API 無狀態 (沒有登入 session),為了避免被 global scope 擋住,直接取消所有 scope 來撈取 + // 2. 取得機台物件 (需優先於帳密驗證,以便記錄日誌到正確機台) $machine = Machine::withoutGlobalScopes()->where('serial_no', $validated['machine'])->first(); if (!$machine) { @@ -53,38 +38,79 @@ class MachineAuthController extends Controller return response()->json(['message' => 'Failed']); } - // 4. RBAC 權限驗證 (遵循多租戶與機台授權規範) + // 3. 透過帳號尋找使用者 (允許使用 username 或 email) + $user = User::where('username', $validated['Su_Account']) + ->orWhere('email', $validated['Su_Account']) + ->first(); + + // 4. 驗證密碼 + if (!$user || !Hash::check($validated['Su_Password'], $user->password)) { + Log::warning("B000 機台登入失敗: 帳密錯誤", [ + 'account' => $validated['Su_Account'], + 'machine' => $validated['machine'] + ]); + + // 寫入機台日誌 + ProcessStateLog::dispatch( + $machine->id, + $machine->company_id, + __("Login failed: :account", ['account' => $validated['Su_Account']]), + 'warning', + [], + 'login' + ); + + return response()->json(['message' => 'Failed']); + } + + // 5. RBAC 權限驗證 (遵循多租戶與機台授權規範) + $isAuthorized = false; if ($user->isSystemAdmin()) { - // [系統管理員] : 擁有最高權限,可登入平台下轄所有機台,直接放行 - + $isAuthorized = true; } elseif ($user->is_admin) { - // [公司管理員] : 不需要檢查 machine_user 表,但【必須驗證】該機台是否隸屬於他的公司 - if ($machine->company_id !== $user->company_id) { - Log::warning("B000 機台登入失敗: 企圖越權登入其他公司的機台", [ - 'user_id' => $user->id, - 'user_company' => $user->company_id, - 'machine_company' => $machine->company_id - ]); - return response()->json(['message' => 'Forbidden']); + if ($machine->company_id === $user->company_id) { + $isAuthorized = true; } - } else { - // [一般租戶帳號] : (包括補貨員等)必須嚴格檢查該帳號有沒有被分配到這台機器的 machine_user 關聯授權 - if (!$user->machines()->where('machine_id', $machine->id)->exists()) { - Log::warning("B000 機台登入失敗: 該帳號沒有此機台的授權", [ - 'user_id' => $user->id, - 'machine_id' => $machine->id - ]); - return response()->json(['message' => 'Forbidden']); + if ($user->machines()->where('machine_id', $machine->id)->exists()) { + $isAuthorized = true; } } - // 5. 驗證完美通過!回傳固定字串 Success 讓 Java 端放行 + if (!$isAuthorized) { + Log::warning("B000 機台登入失敗: 權限不足", [ + 'user_id' => $user->id, + 'machine_id' => $machine->id + ]); + + ProcessStateLog::dispatch( + $machine->id, + $machine->company_id, + __("Unauthorized login attempt: :account", ['account' => $user->username]), + 'warning', + [], + 'login' + ); + + return response()->json(['message' => 'Forbidden']); + } + + // 6. 驗證完美通過! Log::info("B000 機台登入成功", [ - 'account' => $user->username, + 'account' => $user->username, 'machine' => $machine->serial_no ]); - + + // 寫入成功登入日誌 + ProcessStateLog::dispatch( + $machine->id, + $machine->company_id, + __("User logged in: :name", ['name' => $user->name ?? $user->username]), + 'info', + [], + 'login' + ); + return response()->json([ 'message' => 'Success' ]); diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index e86d38d..dabac4e 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -9,8 +9,11 @@ use App\Models\System\User; use App\Jobs\Machine\ProcessHeartbeat; use App\Jobs\Machine\ProcessTimerStatus; use App\Jobs\Machine\ProcessCoinInventory; +use App\Jobs\Machine\ProcessMachineError; +use App\Jobs\Machine\ProcessStateLog; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Validator; +use Illuminate\Support\Facades\Cache; class MachineController extends Controller { @@ -22,6 +25,69 @@ class MachineController extends Controller $machine = $request->get('machine'); $data = $request->except(['machine', 'key']); // 排除 Middleware 注入的 Model 物件與認證 key + // === 狀態異動觸發 (Redis 快取免查 DB) === + $cacheKey = "machine:{$machine->serial_no}:state"; + $oldState = Cache::get($cacheKey); + + $currentPage = $data['current_page'] ?? null; + $doorStatus = $data['door_status'] ?? null; + $firmwareVersion = $data['firmware_version'] ?? null; + $model = $data['model'] ?? null; + + if ($currentPage !== null || $doorStatus !== null || $firmwareVersion !== null || $model !== null) { + // 更新目前狀態到 Redis (保存 1 天) + $newState = $oldState ?? []; + if ($currentPage !== null) $newState['current_page'] = $currentPage; + if ($doorStatus !== null) $newState['door_status'] = $doorStatus; + if ($firmwareVersion !== null) $newState['firmware_version'] = $firmwareVersion; + if ($model !== null) $newState['model'] = $model; + + Cache::put($cacheKey, $newState, 86400); + + // 若有歷史紀錄才進行比對 (避開 Cache Miss 造成的雪崩) + if ($oldState !== null) { + // 1. 判斷頁面是否變更 + if ($currentPage !== null && (string)$currentPage !== (string)($oldState['current_page'] ?? '')) { + // 只記錄「絕對狀態」,配合 lang 中 "Page X" 的翻譯 + ProcessStateLog::dispatch($machine->id, $machine->company_id, "Page {$currentPage}", 'info'); + } + + // 2. 判斷門禁是否變更 (0: 關閉, 1: 開啟) + if ($doorStatus !== null && (string)$doorStatus !== (string)($oldState['door_status'] ?? '')) { + $doorMessage = $doorStatus == 1 ? "Door Opened" : "Door Closed"; + $doorLevel = 'info'; // 不論開關門皆為 info,避免觸發異常狀態 + ProcessStateLog::dispatch($machine->id, $machine->company_id, $doorMessage, $doorLevel); + } + + // 3. 判斷韌體版本是否變更 + if ($firmwareVersion !== null && (string)$firmwareVersion !== (string)($oldState['firmware_version'] ?? '')) { + $oldVersion = $oldState['firmware_version'] ?? 'Unknown'; + // 直接在 Controller 進行翻譯並填值,確保儲存到 DB 的是最終正確字串 + $versionMessage = __("Firmware updated to :version", ['version' => $firmwareVersion]); + ProcessStateLog::dispatch( + $machine->id, + $machine->company_id, + $versionMessage, + 'info', + ['old' => $oldVersion, 'new' => $firmwareVersion] + ); + } + + // 4. 判斷型號是否變更 + if ($model !== null && (string)$model !== (string)($oldState['model'] ?? '')) { + $oldModel = $oldState['model'] ?? 'Unknown'; + $modelMessage = __("Model changed to :model", ['model' => $model]); + ProcessStateLog::dispatch( + $machine->id, + $machine->company_id, + $modelMessage, + 'info', + ['old' => $oldModel, 'new' => $model] + ); + } + } + } + // 異步處理狀態更新 ProcessHeartbeat::dispatch($machine->serial_no, $data); @@ -372,4 +438,22 @@ class MachineController extends Controller 'data' => $products ]); } + + /** + * B013: Report Machine Hardware Error/Status (Asynchronous) + */ + public function reportError(Request $request) + { + $machine = $request->get('machine'); + $data = $request->only(['tid', 'error_code']); + + // 異步分派處理 (Dispatch to queue) + ProcessMachineError::dispatch($machine->serial_no, $data); + + return response()->json([ + 'success' => true, + 'code' => 200, + 'message' => 'Error report accepted', + ], 202); // 202 Accepted + } } diff --git a/app/Jobs/Machine/ProcessMachineError.php b/app/Jobs/Machine/ProcessMachineError.php new file mode 100644 index 0000000..a5ed78c --- /dev/null +++ b/app/Jobs/Machine/ProcessMachineError.php @@ -0,0 +1,40 @@ +serialNo = $serialNo; + $this->data = $data; + } + + /** + * Execute the job. + */ + public function handle(MachineService $service): void + { + $machine = Machine::where('serial_no', $this->serialNo)->first(); + + if ($machine) { + $service->recordErrorLog($machine, $this->data); + } + } +} diff --git a/app/Jobs/Machine/ProcessStateLog.php b/app/Jobs/Machine/ProcessStateLog.php new file mode 100644 index 0000000..e4f0ebd --- /dev/null +++ b/app/Jobs/Machine/ProcessStateLog.php @@ -0,0 +1,56 @@ +machineId = $machineId; + $this->companyId = $companyId; + $this->message = $message; + $this->level = $level; + $this->context = $context; + $this->type = $type; + } + + /** + * Execute the job. + */ + public function handle(): void + { + try { + MachineLog::create([ + 'machine_id' => $this->machineId, + 'company_id' => $this->companyId, + 'type' => $this->type, + 'level' => $this->level, + 'message' => $this->message, + 'context' => $this->context, + ]); + } catch (\Exception $e) { + Log::error("Failed to create state log for machine {$this->machineId}: " . $e->getMessage()); + throw $e; + } + } +} diff --git a/app/Models/Machine/MachineLog.php b/app/Models/Machine/MachineLog.php index 6c11beb..b37a067 100644 --- a/app/Models/Machine/MachineLog.php +++ b/app/Models/Machine/MachineLog.php @@ -24,6 +24,33 @@ class MachineLog extends Model 'context' => 'array', ]; + protected $appends = [ + 'translated_message', + ]; + + /** + * 動態重組翻譯後的訊息 + */ + public function getTranslatedMessageAttribute(): string + { + $context = $this->context; + + // 若 context 中已有翻譯標籤 (B013 封裝),則進行動態重組 + if (isset($context['translated_label'])) { + $label = __($context['translated_label']); + $tid = $context['tid'] ?? null; + $code = $context['raw_code'] ?? '0000'; + + if ($tid) { + return __('Slot') . " {$tid}: {$label} (Code: {$code})"; + } + return "{$label} (Code: {$code})"; + } + + // 預設退回原始 message (支援歷史資料的翻譯判定與佔位符替換) + return __($this->message, $context ?? []); + } + public function machine() { return $this->belongsTo(Machine::class); diff --git a/app/Services/Machine/MachineService.php b/app/Services/Machine/MachineService.php index 90b63c2..17205ab 100644 --- a/app/Services/Machine/MachineService.php +++ b/app/Services/Machine/MachineService.php @@ -10,6 +10,56 @@ use Carbon\Carbon; class MachineService { + /** + * B013: 硬體狀態代碼對照表 (Hardware Status Code Mapping) + */ + public const ERROR_CODE_MAP = [ + // 出貨狀態類 (Prefix: 04 - BUY_STATUS) + '0401' => ['label' => 'Dispensing in progress', 'level' => 'info'], + '0402' => ['label' => 'Dispense successful', 'level' => 'info'], + '0403' => ['label' => 'Slot jammed', 'level' => 'error'], + '0404' => ['label' => 'Motor not stopped', 'level' => 'warning'], + '0406' => ['label' => 'Slot not found', 'level' => 'error'], + '0407' => ['label' => 'Dispense error (0407)', 'level' => 'error'], + '0408' => ['label' => 'Dispense error (0408)', 'level' => 'error'], + '0409' => ['label' => 'Dispense error (0409)', 'level' => 'error'], + '040A' => ['label' => 'Dispense error (040A)', 'level' => 'error'], + '0410' => ['label' => 'Elevator rising', 'level' => 'info'], + '0411' => ['label' => 'Elevator descending', 'level' => 'info'], + '0412' => ['label' => 'Elevator rise error', 'level' => 'error'], + '0413' => ['label' => 'Elevator descent error', 'level' => 'error'], + '0414' => ['label' => 'Pickup door closed', 'level' => 'info'], + '0415' => ['label' => 'Pickup door error', 'level' => 'error'], + '0416' => ['label' => 'Delivery door opened', 'level' => 'info'], + '0417' => ['label' => 'Delivery door open error', 'level' => 'error'], + '0418' => ['label' => 'Delivering product', 'level' => 'info'], + '0419' => ['label' => 'Delivery door closed', 'level' => 'info'], + '0420' => ['label' => 'Delivery door close error', 'level' => 'error'], + '0421' => ['label' => 'Hopper empty', 'level' => 'warning'], + '0422' => ['label' => 'Hopper overheated', 'level' => 'warning'], + '0423' => ['label' => 'Hopper heating timeout', 'level' => 'error'], + '0424' => ['label' => 'Hopper error (0424)', 'level' => 'error'], + '0426' => ['label' => 'Microwave door opened', 'level' => 'info'], + '0427' => ['label' => 'Microwave door error', 'level' => 'error'], + '04FF' => ['label' => 'Dispense stopped', 'level' => 'info'], + + // 貨道狀態類 (Prefix: 02 - SLOT_STATUS) + '0201' => ['label' => 'Slot normal', 'level' => 'info'], + '0202' => ['label' => 'Product empty', 'level' => 'warning'], + '0203' => ['label' => 'Slot empty', 'level' => 'warning'], + '0206' => ['label' => 'Slot not closed', 'level' => 'warning'], + '0207' => ['label' => 'Slot motor error (0207)', 'level' => 'error'], + '0208' => ['label' => 'Slot motor error (0208)', 'level' => 'error'], + '0209' => ['label' => 'Slot motor error (0209)', 'level' => 'error'], + '0212' => ['label' => 'Hopper empty (0212)', 'level' => 'warning'], + + // 機台整體狀態類 (Prefix: 54 - MACHINE_STATUS) + '5400' => ['label' => 'Machine normal', 'level' => 'info'], + '5401' => ['label' => 'Elevator sensor error', 'level' => 'error'], + '5402' => ['label' => 'Pickup door not closed', 'level' => 'warning'], + '5403' => ['label' => 'Elevator failure', 'level' => 'error'], + ]; + /** * Update machine heartbeat and status. * @@ -178,6 +228,36 @@ class MachineService }); } + /** + * B013: Record machine hardware error/status log with auto-translation. + * + * @param Machine $machine + * @param array $data + * @return MachineLog + */ + public function recordErrorLog(Machine $machine, array $data): MachineLog + { + $errorCode = $data['error_code'] ?? '0000'; + $mapping = self::ERROR_CODE_MAP[$errorCode] ?? ['label' => 'Unknown Status', 'level' => 'error']; + + $slotNo = $data['tid'] ?? null; + $label = $mapping['label']; + + // 儲存原始英文格式作為 DB 備用,前端顯示會優先使用 model accessor 的動態翻譯內容 + $message = $slotNo ? "Slot {$slotNo}: {$label} (Code: {$errorCode})" : "{$label} (Code: {$errorCode})"; + + return $machine->logs()->create([ + 'company_id' => $machine->company_id, + 'type' => 'submachine', + 'level' => $mapping['level'], + 'message' => $message, + 'context' => array_merge($data, [ + 'translated_label' => $label, + 'raw_code' => $errorCode + ]), + ]); + } + /** * Update machine slot stock (single slot). * Legacy support for recordLog (Existing code). diff --git a/config/api-docs.php b/config/api-docs.php index 8f65550..564dc41 100644 --- a/config/api-docs.php +++ b/config/api-docs.php @@ -302,6 +302,54 @@ return [ ] ], 'notes' => '運作邏輯 (Client-side Logic): GET 執行全量同步,App 應於收到成功回應後,先執行 deleteAll() 再進行 insertAll()。PATCH 執行增量更新,App 僅對記憶體中的既存商品進行欄位值覆蓋 (Patching)。' + ], + [ + 'name' => 'B013: 機台故障與異常狀態上報 (Error/Status Report)', + 'slug' => 'b013-error-report', + 'method' => 'POST', + 'path' => '/api/v1/app/machine/error/B013', + 'description' => '用於接收機台發出的即時硬體狀態代碼(如卡貨、門未關)。身份由 Bearer Token 識別,回傳成功代表伺服器已將任務列入異步隊列處理。', + 'headers' => [ + 'Authorization' => 'Bearer ', + 'Content-Type' => 'application/json', + ], + 'parameters' => [ + 'tid' => [ + 'type' => 'integer', + 'required' => false, + 'description' => '涉及到之具體貨道編號 (Slot No)', + 'example' => 12 + ], + 'error_code' => [ + 'type' => 'string', + 'required' => true, + 'description' => '硬體狀態代碼 (4 位 16 進位字串)', + 'example' => '0403' + ], + ], + 'response_parameters' => [ + 'success' => [ + 'type' => 'boolean', + 'description' => '請求是否已成功接收', + 'example' => true + ], + 'code' => [ + 'type' => 'integer', + 'description' => '內部業務狀態碼', + 'example' => 200 + ], + ], + 'request' => [ + 'tid' => 12, + 'error_code' => '0403', + ], + 'response' => [ + 'success' => true, + 'code' => 200, + 'message' => 'Error report accepted' + ], + 'notes' => '硬體代碼對照表見後端 MachineService::ERROR_CODE_MAP 定義。 +0402: 出貨成功, 0403: 貨道卡貨, 0202: 貨道缺貨, 0415: 取貨門異常...等。' ] ] ] diff --git a/lang/en.json b/lang/en.json index 06e5339..06e11cc 100644 --- a/lang/en.json +++ b/lang/en.json @@ -17,7 +17,7 @@ "Account updated successfully.": "Account updated successfully.", "Account:": "Account:", "accounts": "Account Management", - "Accounts / Machines": "Accounts / Machines", + "Accounts \/ Machines": "Accounts \/ Machines", "Action": "Action", "Actions": "Actions", "Active": "Active", @@ -51,7 +51,7 @@ "Advertisement Management": "Advertisement Management", "Advertisement updated successfully": "Advertisement updated successfully", "Advertisement updated successfully.": "Advertisement updated successfully.", - "Advertisement Video/Image": "Advertisement Video/Image", + "Advertisement Video\/Image": "Advertisement Video\/Image", "Affiliated Company": "Affiliated Company", "Affiliated Unit": "Company Name", "Affiliation": "Company Name", @@ -127,7 +127,7 @@ "Back to List": "Back to List", "Badge Settings": "Badge Settings", "Barcode": "Barcode", - "Barcode / Material": "Barcode / Material", + "Barcode \/ Material": "Barcode \/ Material", "Basic Information": "Basic Information", "Basic Settings": "Basic Settings", "Basic Specifications": "Basic Specifications", @@ -164,7 +164,7 @@ "Change": "Change", "Change Stock": "Change Stock", "Channel Limits": "Channel Limits", - "Channel Limits (Track/Spring)": "Channel Limits (Track/Spring)", + "Channel Limits (Track\/Spring)": "Channel Limits (Track\/Spring)", "Channel Limits Configuration": "Channel Limits Configuration", "ChannelId": "ChannelId", "ChannelSecret": "ChannelSecret", @@ -277,7 +277,7 @@ "Dispensing": "Dispensing", "Duration": "Duration", "Duration (Seconds)": "Duration (Seconds)", - "e.g. 500ml / 300g": "e.g. 500ml / 300g", + "e.g. 500ml \/ 300g": "e.g. 500ml \/ 300g", "e.g. John Doe": "e.g. John Doe", "e.g. johndoe": "e.g. johndoe", "e.g. Taiwan Star": "e.g. Taiwan Star", @@ -315,7 +315,7 @@ "Enable Material Code": "Enable Material Code", "Enable Points": "Enable Points", "Enabled": "Enabled", - "Enabled/Disabled": "Enabled/Disabled", + "Enabled\/Disabled": "Enabled\/Disabled", "End Date": "End Date", "Engineer": "Engineer", "English": "English", @@ -341,7 +341,7 @@ "Execution Time": "Execution Time", "Exp": "Exp", "Expired": "Expired", - "Expired / Disabled": "Expired / Disabled", + "Expired \/ Disabled": "Expired \/ Disabled", "Expiring": "Expiring", "Expiry": "Expiry", "Expiry Date": "Expiry Date", @@ -570,7 +570,7 @@ "Monthly cumulative revenue overview": "Monthly cumulative revenue overview", "Monthly Transactions": "Monthly Transactions", "Multilingual Names": "Multilingual Names", - "N/A": "N/A", + "N\/A": "N\/A", "Name": "Name", "Name in English": "Name in English", "Name in Japanese": "Name in Japanese", @@ -713,7 +713,7 @@ "Position": "Position", "Preview": "Preview", "Previous": "Previous", - "Price / Member": "Price / Member", + "Price \/ Member": "Price \/ Member", "Pricing Information": "Pricing Information", "Product Count": "Product Count", "Product created successfully": "Product created successfully", @@ -825,7 +825,7 @@ "Scale level and access control": "層級與存取控制", "Scan this code to quickly access the maintenance form for this device.": "Scan this code to quickly access the maintenance form for this device.", "Search accounts...": "Search accounts...", - "Search by name or S/N...": "Search by name or S/N...", + "Search by name or S\/N...": "Search by name or S\/N...", "Search cargo lane": "Search cargo lane", "Search Company Title...": "Search Company Title...", "Search company...": "Search company...", @@ -880,7 +880,6 @@ "Showing :from to :to of :total items": "Showing :from to :to of :total items", "Sign in to your account": "Sign in to your account", "Signed in as": "Signed in as", - "Slot": "Slot", "Slot Mechanism (default: Conveyor, check for Spring)": "Slot Mechanism (default: Conveyor, check for Spring)", "Slot No": "Slot No", "Slot Status": "Slot Status", @@ -902,7 +901,7 @@ "Start Date": "Start Date", "Statistics": "Statistics", "Status": "Status", - "Status / Temp / Sub / Card / Scan": "Status / Temp / Sub / Card / Scan", + "Status \/ Temp \/ Sub \/ Card \/ Scan": "Status \/ Temp \/ Sub \/ Card \/ Scan", "Stock": "Stock", "Stock & Expiry": "Stock & Expiry", "Stock & Expiry Management": "Stock & Expiry Management", @@ -1052,5 +1051,73 @@ "You cannot delete your own account.": "You cannot delete your own account.", "Your email address is unverified.": "Your email address is unverified.", "Your recent account activity": "Your recent account activity", - "待填寫": "待填寫" + "待填寫": "待填寫", + "Dispensing in progress": "Dispensing in progress", + "Dispense successful": "Dispense successful", + "Slot jammed": "Slot jammed", + "Motor not stopped": "Motor not stopped", + "Slot not found": "Slot not found", + "Dispense error (0407)": "Dispense error (0407)", + "Dispense error (0408)": "Dispense error (0408)", + "Dispense error (0409)": "Dispense error (0409)", + "Dispense error (040A)": "Dispense error (040A)", + "Elevator rising": "Elevator rising", + "Elevator descending": "Elevator descending", + "Elevator rise error": "Elevator rise error", + "Elevator descent error": "Elevator descent error", + "Pickup door closed": "Pickup door closed", + "Pickup door error": "Pickup door error", + "Delivery door opened": "Delivery door opened", + "Delivery door open error": "Delivery door open error", + "Delivering product": "Delivering product", + "Delivery door closed": "Delivery door closed", + "Delivery door close error": "Delivery door close error", + "Hopper empty": "Hopper empty", + "Hopper overheated": "Hopper overheated", + "Hopper heating timeout": "Hopper heating timeout", + "Hopper error (0424)": "Hopper error (0424)", + "Microwave door opened": "Microwave door opened", + "Microwave door error": "Microwave door error", + "Dispense stopped": "Dispense stopped", + "Slot normal": "Slot normal", + "Product empty": "Product empty", + "Slot empty": "Slot empty", + "Slot not closed": "Slot not closed", + "Slot motor error (0207)": "Slot motor error (0207)", + "Slot motor error (0208)": "Slot motor error (0208)", + "Slot motor error (0209)": "Slot motor error (0209)", + "Hopper empty (0212)": "Hopper empty (0212)", + "Machine normal": "Machine normal", + "Elevator sensor error": "Elevator sensor error", + "Pickup door not closed": "Pickup door not closed", + "Elevator failure": "Elevator failure", + "Slot": "Slot", + "Page 0": "Offline", + "Page 1": "Home", + "Page 2": "Vending", + "Page 3": "Admin", + "Page 4": "Restock", + "Page 5": "Tutorial", + "Page 6": "Purchasing", + "Page 7": "Locked", + "Page 60": "Dispense Success", + "Page 61": "Slot Test", + "Page 62": "Payment Selection", + "Page 63": "Waiting for Payment", + "Page 64": "Dispensing", + "Page 65": "Receipt", + "Page 66": "Passcode", + "Page 67": "Pickup Code", + "Page 68": "Message", + "Page 69": "Purchase Cancelled", + "Page 610": "Purchase Ended", + "Page 611": "Store Gift", + "Page 612": "Dispense Failed", + "Door Opened": "Door Opened", + "Door Closed": "Door Closed", + "Firmware updated to :version": "Firmware updated to :version", + "Model changed to :model": "Model changed to :model", + "User logged in: :name": "User logged in: :name", + "Login failed: :account": "Login failed: :account", + "Unauthorized login attempt: :account": "Unauthorized login attempt: :account" } \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index 57aa852..ce6787d 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -17,7 +17,7 @@ "Account updated successfully.": "アカウントが正常に更新されました。", "Account:": "アカウント:", "accounts": "アカウント管理", - "Accounts / Machines": "アカウント / 機体", + "Accounts \/ Machines": "アカウント \/ 機体", "Action": "操作", "Actions": "操作", "Active": "有効", @@ -51,7 +51,7 @@ "Advertisement Management": "広告管理", "Advertisement updated successfully": "広告の更新に成功しました", "Advertisement updated successfully.": "広告の更新に成功しました。", - "Advertisement Video/Image": "広告動画/画像", + "Advertisement Video\/Image": "広告動画\/画像", "Affiliated Company": "所属会社", "Affiliated Unit": "所属会社", "Affiliation": "所属会社", @@ -127,7 +127,7 @@ "Back to List": "リストに戻る", "Badge Settings": "バッジ設定", "Barcode": "バーコード", - "Barcode / Material": "バーコード / 素材", + "Barcode \/ Material": "バーコード \/ 素材", "Basic Information": "基本情報", "Basic Settings": "基本設定", "Basic Specifications": "基本仕様", @@ -164,7 +164,7 @@ "Change": "変更", "Change Stock": "在庫変更", "Channel Limits": "チャネル制限", - "Channel Limits (Track/Spring)": "チャネル上限(トラック/スプリング)", + "Channel Limits (Track\/Spring)": "チャネル上限(トラック\/スプリング)", "Channel Limits Configuration": "チャネル制限設定", "ChannelId": "チャネルID", "ChannelSecret": "チャネルシークレット", @@ -277,7 +277,7 @@ "Dispensing": "払い出し中", "Duration": "再生時間", "Duration (Seconds)": "再生時間(秒)", - "e.g. 500ml / 300g": "例: 500ml / 300g", + "e.g. 500ml \/ 300g": "例: 500ml \/ 300g", "e.g. John Doe": "例:山田太郎", "e.g. johndoe": "例:yamadataro", "e.g. Taiwan Star": "例:Taiwan Star", @@ -315,7 +315,7 @@ "Enable Material Code": "品目コードを有効にする", "Enable Points": "ポイントを有効にする", "Enabled": "有効", - "Enabled/Disabled": "有効/無効", + "Enabled\/Disabled": "有効\/無効", "End Date": "終了日", "Engineer": "エンジニア", "English": "英語", @@ -341,7 +341,7 @@ "Execution Time": "実行時間", "Exp": "有効期限", "Expired": "期限切れ", - "Expired / Disabled": "期限切れ / 無効", + "Expired \/ Disabled": "期限切れ \/ 無効", "Expiring": "期限間近", "Expiry": "期限", "Expiry Date": "有効期限", @@ -569,7 +569,7 @@ "Monthly cumulative revenue overview": "月間累積収益の概要", "Monthly Transactions": "月間取引数", "Multilingual Names": "多言語名称", - "N/A": "N/A", + "N\/A": "N\/A", "Name": "名前", "Name in English": "英語名", "Name in Japanese": "日本語名", @@ -630,7 +630,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.": "アカウントを削除すると、そのすべてのリソースとデータが完全に削除されます。削除する前に、保存しておきたいデータをダウンロードしてください。", @@ -712,7 +712,7 @@ "Position": "位置", "Preview": "プレビュー", "Previous": "前へ", - "Price / Member": "価格 / 会員", + "Price \/ Member": "価格 \/ 会員", "Pricing Information": "価格情報", "Product Count": "商品数", "Product created successfully": "商品が正常に作成されました", @@ -824,7 +824,7 @@ "Scale level and access control": "規模レベルとアクセス制御", "Scan this code to quickly access the maintenance form for this device.": "このコードをスキャンして、端末のメンテナンスフォームに素早くアクセスします。", "Search accounts...": "アカウントを検索...", - "Search by name or S/N...": "名称または製造番号で検索...", + "Search by name or S\/N...": "名称または製造番号で検索...", "Search cargo lane": "貨道を検索", "Search Company Title...": "会社名を検索...", "Search company...": "会社を検索...", @@ -901,7 +901,7 @@ "Start Date": "開始日", "Statistics": "統計", "Status": "ステータス", - "Status / Temp / Sub / Card / Scan": "状態 / 温度 / 子機 / カード / スキャン", + "Status \/ Temp \/ Sub \/ Card \/ Scan": "状態 \/ 温度 \/ 子機 \/ カード \/ スキャン", "Stock": "在庫", "Stock & Expiry": "在庫と消費期限", "Stock & Expiry Management": "在庫・期限管理", @@ -1051,5 +1051,72 @@ "You cannot delete your own account.": "自分自身のアカウントを削除することはできません。", "Your email address is unverified.": "メールアドレスが未認証です。", "Your recent account activity": "最近のアカウントアクティビティ", - "待填寫": "待填寫" + "待填寫": "待填寫", + "Dispensing in progress": "商品搬送中", + "Dispense successful": "搬送成功", + "Slot jammed": "貨道詰まり (K-PDT)", + "Motor not stopped": "モーター停止異常", + "Slot not found": "指定貨道が見つかりません", + "Dispense error (0407)": "搬送異常 (0407)", + "Dispense error (0408)": "搬送異常 (0408)", + "Dispense error (0409)": "搬送異常 (0409)", + "Dispense error (040A)": "搬送異常 (040A)", + "Elevator rising": "昇降機上昇中", + "Elevator descending": "昇降機下降中", + "Elevator rise error": "昇降機上昇異常", + "Elevator descent error": "昇降機下降異常", + "Pickup door closed": "取出口ドア閉鎖", + "Pickup door error": "取出口ドア異常", + "Delivery door opened": "配送口ドア開放", + "Delivery door open error": "配送口ドア開放異常", + "Delivering product": "商品配送中", + "Delivery door closed": "配送口ドア閉鎖", + "Delivery door close error": "配送口ドア閉鎖異常", + "Hopper empty": "ホッパー空", + "Hopper overheated": "ホッパー過熱", + "Hopper heating timeout": "ホッパー加熱タイムアウト", + "Hopper error (0424)": "ホッパー異常 (0424)", + "Microwave door opened": "電子レンジドア開放", + "Microwave door error": "電子レンジドア異常", + "Dispense stopped": "搬送停止", + "Slot normal": "貨道正常", + "Product empty": "品切れ (PDT_EMPTY)", + "Slot empty": "貨道空 (SLOT_EMPTY)", + "Slot not closed": "貨道未閉鎖", + "Slot motor error (0207)": "貨道モーター故障 (0207)", + "Slot motor error (0208)": "貨道モーター故障 (0208)", + "Slot motor error (0209)": "貨道モーター故障 (0209)", + "Hopper empty (0212)": "ホッパー空 (0212)", + "Machine normal": "システム正常", + "Elevator sensor error": "昇降センサー異常", + "Pickup door not closed": "取出口ドア未閉鎖", + "Elevator failure": "昇降システム故障", + "Page 0": "オフライン", + "Page 1": "ホーム", + "Page 2": "販売ページ", + "Page 3": "管理ページ", + "Page 4": "補充ページ", + "Page 5": "チュートリアル", + "Page 6": "購入中", + "Page 7": "ロック中", + "Page 60": "排出成功", + "Page 61": "スロットテスト", + "Page 62": "支払い方法選択", + "Page 63": "支払い待ち", + "Page 64": "排出中", + "Page 65": "レシート", + "Page 66": "パスコード", + "Page 67": "受取コード", + "Page 68": "メッセージ", + "Page 69": "購入キャンセル", + "Page 610": "購入完了", + "Page 611": "来店ギフト", + "Page 612": "排出失敗", + "Door Opened": "機ドア開放", + "Door Closed": "機ドア閉鎖", + "Firmware updated to :version": "ファームウェア更新::version", + "Model changed to :model": "モデル変更::model", + "User logged in: :name": "ユーザーログイン::name", + "Login failed: :account": "ログイン失敗::account", + "Unauthorized login attempt: :account": "不許可のログイン試行::account" } \ No newline at end of file diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 9cc9363..e4a1870 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -17,7 +17,7 @@ "Account updated successfully.": "帳號已成功更新。", "Account:": "帳號:", "accounts": "帳號管理", - "Accounts / Machines": "帳號 / 機台", + "Accounts \/ Machines": "帳號 \/ 機台", "Action": "操作", "Actions": "操作", "Active": "使用中", @@ -51,7 +51,7 @@ "Advertisement Management": "廣告管理", "Advertisement updated successfully": "廣告更新成功", "Advertisement updated successfully.": "廣告更新成功。", - "Advertisement Video/Image": "廣告影片/圖片", + "Advertisement Video\/Image": "廣告影片\/圖片", "Affiliated Company": "公司名稱", "Affiliated Unit": "公司名稱", "Affiliation": "所屬單位", @@ -127,7 +127,7 @@ "Back to List": "返回列表", "Badge Settings": "識別證", "Barcode": "條碼", - "Barcode / Material": "條碼 / 物料編碼", + "Barcode \/ Material": "條碼 \/ 物料編碼", "Basic Information": "基本資訊", "Basic Settings": "基本設定", "Basic Specifications": "基本規格", @@ -164,7 +164,7 @@ "Change": "更換", "Change Stock": "零錢庫存", "Channel Limits": "貨道上限", - "Channel Limits (Track/Spring)": "貨道上限 (履帶/彈簧)", + "Channel Limits (Track\/Spring)": "貨道上限 (履帶\/彈簧)", "Channel Limits Configuration": "貨道上限配置", "ChannelId": "ChannelId", "ChannelSecret": "ChannelSecret", @@ -277,7 +277,7 @@ "Dispensing": "出貨", "Duration": "時長", "Duration (Seconds)": "播放秒數", - "e.g. 500ml / 300g": "例如:500ml / 300g", + "e.g. 500ml \/ 300g": "例如:500ml \/ 300g", "e.g. John Doe": "例如:張曉明", "e.g. johndoe": "例如:xiaoming", "e.g. Taiwan Star": "例如:台灣之星", @@ -315,7 +315,7 @@ "Enable Material Code": "啟用物料編號", "Enable Points": "啟用點數規則", "Enabled": "已啟用", - "Enabled/Disabled": "啟用/停用", + "Enabled\/Disabled": "啟用\/停用", "End Date": "截止日", "Engineer": "維修人員", "English": "英文", @@ -341,7 +341,7 @@ "Execution Time": "執行時間", "Exp": "效期", "Expired": "已過期", - "Expired / Disabled": "已過期 / 停用", + "Expired \/ Disabled": "已過期 \/ 停用", "Expiring": "效期將屆", "Expiry": "效期", "Expiry Date": "有效日期", @@ -570,7 +570,7 @@ "Monthly cumulative revenue overview": "本月累計營收概況", "Monthly Transactions": "本月交易統計", "Multilingual Names": "多語系名稱", - "N/A": "不適用", + "N\/A": "不適用", "Name": "名稱", "Name in English": "英文名稱", "Name in Japanese": "日文名稱", @@ -713,7 +713,7 @@ "Position": "投放位置", "Preview": "預覽", "Previous": "上一頁", - "Price / Member": "售價 / 會員價", + "Price \/ Member": "售價 \/ 會員價", "Pricing Information": "價格資訊", "Product Count": "商品數量", "Product created successfully": "商品已成功建立", @@ -825,7 +825,7 @@ "Scale level and access control": "層級與存取控制", "Scan this code to quickly access the maintenance form for this device.": "掃描此 QR Code 即可快速進入此設備的維修單填寫頁面。", "Search accounts...": "搜尋帳號...", - "Search by name or S/N...": "搜尋名稱或序號...", + "Search by name or S\/N...": "搜尋名稱或序號...", "Search cargo lane": "搜尋貨道編號或商品名稱", "Search Company Title...": "搜尋公司名稱...", "Search company...": "搜尋公司...", @@ -880,7 +880,6 @@ "Showing :from to :to of :total items": "顯示第 :from 到 :to 項,共 :total 項", "Sign in to your account": "隨時隨地掌控您的業務。", "Signed in as": "登入身份", - "Slot": "貨道", "Slot Mechanism (default: Conveyor, check for Spring)": "貨道機制 (預設履帶,勾選為彈簧)", "Slot No": "貨道編號", "Slot Status": "貨道效期", @@ -902,7 +901,7 @@ "Start Date": "起始日", "Statistics": "數據統計", "Status": "狀態", - "Status / Temp / Sub / Card / Scan": "狀態 / 溫度 / 下位機 / 刷卡機 / 掃碼機", + "Status \/ Temp \/ Sub \/ Card \/ Scan": "狀態 \/ 溫度 \/ 下位機 \/ 刷卡機 \/ 掃碼機", "Stock": "庫存", "Stock & Expiry": "庫存與效期", "Stock & Expiry Management": "庫存與效期管理", @@ -1052,5 +1051,73 @@ "You cannot delete your own account.": "您無法刪除自己的帳號。", "Your email address is unverified.": "您的電子郵件地址尚未驗證。", "Your recent account activity": "最近的帳號活動", - "待填寫": "待填寫" + "待填寫": "待填寫", + "Dispensing in progress": "正在出貨中", + "Dispense successful": "出貨成功", + "Slot jammed": "貨道卡貨 (K-PDT)", + "Motor not stopped": "電機未停止", + "Slot not found": "找不到指定貨道", + "Dispense error (0407)": "出貨過程異常 (0407)", + "Dispense error (0408)": "出貨過程異常 (0408)", + "Dispense error (0409)": "出貨過程異常 (0409)", + "Dispense error (040A)": "出貨過程異常 (040A)", + "Elevator rising": "升降平台上升中", + "Elevator descending": "升降平台下降中", + "Elevator rise error": "升降平台上升異常", + "Elevator descent error": "升降平台下降異常", + "Pickup door closed": "取貨門已關閉", + "Pickup door error": "取貨門運作異常", + "Delivery door opened": "送貨門開啟", + "Delivery door open error": "送貨門開啟異常", + "Delivering product": "正在送出商品", + "Delivery door closed": "送貨門關閉", + "Delivery door close error": "送貨門關閉異常", + "Hopper empty": "料斗箱空", + "Hopper overheated": "料斗箱過熱", + "Hopper heating timeout": "料斗箱加熱逾時", + "Hopper error (0424)": "料斗箱異常 (0424)", + "Microwave door opened": "微波爐門開啟", + "Microwave door error": "微波爐門異常", + "Dispense stopped": "出貨停止", + "Slot normal": "貨道正常", + "Product empty": "貨道缺貨 (PDT_EMPTY)", + "Slot empty": "貨道空 (SLOT_EMPTY)", + "Slot not closed": "貨道未關閉", + "Slot motor error (0207)": "貨道電機故障 (0207)", + "Slot motor error (0208)": "貨道電機故障 (0208)", + "Slot motor error (0209)": "貨道電機故障 (0209)", + "Hopper empty (0212)": "料斗空 (0212)", + "Machine normal": "機台系統正常", + "Elevator sensor error": "升降箱感測異常", + "Pickup door not closed": "取貨門未關閉", + "Elevator failure": "升降系統故障", + "Slot": "貨道", + "Page 0": "離線", + "Page 1": "主頁面", + "Page 2": "販賣頁", + "Page 3": "管理頁", + "Page 4": "補貨頁", + "Page 5": "教學頁", + "Page 6": "購買中", + "Page 7": "鎖定頁", + "Page 60": "出貨成功", + "Page 61": "貨道測試", + "Page 62": "付款選擇", + "Page 63": "等待付款", + "Page 64": "出貨", + "Page 65": "收據簽單", + "Page 66": "通行碼", + "Page 67": "取貨碼", + "Page 68": "訊息顯示", + "Page 69": "取消購買", + "Page 610": "購買結束", + "Page 611": "來店禮", + "Page 612": "出貨失敗", + "Door Opened": "機門已開啟", + "Door Closed": "機門已關閉", + "Firmware updated to :version": "韌體版本更新::version", + "Model changed to :model": "型號變更::model", + "User logged in: :name": "使用者登入::name", + "Login failed: :account": "登入失敗::account", + "Unauthorized login attempt: :account": "越權登入嘗試::account" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a2d7912..6870d10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "flatpickr": "^4.6.13", "pptxgenjs": "^4.0.1" }, "devDependencies": { @@ -1506,6 +1507,12 @@ "node": ">=8" } }, + "node_modules/flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", diff --git a/package.json b/package.json index 4705b28..5864fad 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "vite": "^5.0.0" }, "dependencies": { + "flatpickr": "^4.6.13", "pptxgenjs": "^4.0.1" } } diff --git a/resources/css/app.css b/resources/css/app.css index 5e2eac6..76be933 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,3 +1,5 @@ +@import 'flatpickr/dist/flatpickr.min.css'; + @tailwind base; @tailwind components; @tailwind utilities; @@ -155,6 +157,17 @@ [x-cloak] { display: none !important; } + + /* Hide scrollbar for Chrome, Safari and Opera */ + .hide-scrollbar::-webkit-scrollbar { + display: none; + } + + /* Hide scrollbar for IE, Edge and Firefox */ + .hide-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } } @layer components { @@ -317,4 +330,204 @@ .luxury-select-sm .hs-select-toggle { @apply py-2.5 text-sm !important; } -} \ No newline at end of file +} + /* Flatpickr Luxury Theme Overrides */ + .flatpickr-calendar { + border: 1px solid #e2e8f0 !important; + border-radius: 1.25rem !important; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25) !important; + background: #ffffff !important; + padding: 4px !important; + } + + .dark .flatpickr-calendar { + background: #1e293b !important; + border-color: #334155 !important; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5) !important; + } + + .flatpickr-day { + color: #475569 !important; + border-radius: 12px !important; + font-weight: 500 !important; + } + + .dark .flatpickr-day { + color: #cbd5e1 !important; + } + + .flatpickr-day.prevMonthDay, + .flatpickr-day.nextMonthDay { + color: #cbd5e1 !important; + opacity: 0.3; + } + + .dark .flatpickr-day.prevMonthDay, + .dark .flatpickr-day.nextMonthDay { + color: #475569 !important; + opacity: 0.8; + } + + .flatpickr-day.today { + border-color: #06b6d4 !important; + color: #06b6d4 !important; + } + + .flatpickr-day.selected { + 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; + } + + .flatpickr-day:not(.selected):hover { + background: #f1f5f9 !important; + } + + .dark .flatpickr-day:not(.selected):hover { + background: #334155 !important; + } + + /* Weekdays & Header */ + .flatpickr-weekday { + color: #94a3b8 !important; + font-weight: 800 !important; + font-size: 11px !important; + } + + .dark .flatpickr-weekday { + color: #475569 !important; + } + + .flatpickr-months .flatpickr-month { + color: #1e293b !important; + fill: currentColor !important; + } + + .dark .flatpickr-months .flatpickr-month { + color: #f8fafc !important; + } + + .flatpickr-prev-month, + .flatpickr-next-month { + color: #1e293b !important; + fill: currentColor !important; + @apply transition-colors duration-200 !important; + } + + .dark .flatpickr-prev-month, + .dark .flatpickr-next-month { + color: #cbd5e1 !important; + } + + .flatpickr-prev-month:hover, + .flatpickr-next-month:hover { + color: #06b6d4 !important; + } + + .flatpickr-current-month .flatpickr-monthDropdown-months { + font-weight: 800 !important; + @apply bg-transparent dark:bg-slate-800 text-slate-900 dark:text-slate-100 !important; + border: none !important; + border-radius: 6px !important; + padding: 2px 4px !important; + cursor: pointer !important; + } + + .flatpickr-monthDropdown-month { + @apply bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 !important; + } + + /* Time Section */ + .flatpickr-time { + border-top: 1px solid #f1f5f9 !important; + margin-top: 8px !important; + padding-top: 8px !important; + } + + .dark .flatpickr-time { + border-top-color: #334155 !important; + } + + .flatpickr-time input { + color: #1e293b !important; + font-weight: 800 !important; + border-radius: 8px !important; + } + + .dark .flatpickr-time input { + color: #f8fafc !important; + } + + .flatpickr-time input:hover, + .flatpickr-time input:focus { + background: #f1f5f9 !important; + } + + .dark .flatpickr-time input:hover, + .dark .flatpickr-time input:focus { + background: #334155 !important; + } + + .dark .flatpickr-time .flatpickr-am-pm { + color: #f8fafc !important; + } + + /* Time Stepper Arrows & Separator */ + .flatpickr-time .numInputWrapper span.arrowUp:after { + border-bottom-color: #94a3b8 !important; + } + + .flatpickr-time .numInputWrapper span.arrowUp:hover:after { + border-bottom-color: #06b6d4 !important; + } + + .flatpickr-time .numInputWrapper span.arrowDown:after { + border-top-color: #94a3b8 !important; + } + + .flatpickr-time .numInputWrapper span.arrowDown:hover:after { + border-top-color: #06b6d4 !important; + } + + .dark .flatpickr-time .numInputWrapper span.arrowUp:after { + border-bottom-color: #64748b !important; + } + + .dark .flatpickr-time .numInputWrapper span.arrowUp:hover:after { + border-bottom-color: #06b6d4 !important; + } + + .dark .flatpickr-time .numInputWrapper span.arrowDown:after { + border-top-color: #64748b !important; + } + + .dark .flatpickr-time .numInputWrapper span.arrowDown:hover:after { + border-top-color: #06b6d4 !important; + } + + .flatpickr-time .numInputWrapper span { + border-color: #f1f5f9 !important; + } + + .dark .flatpickr-time .numInputWrapper span { + border-color: #334155 !important; + background: transparent !important; + } + + .dark .flatpickr-time .numInputWrapper span:hover { + background: #334155 !important; + } + + .flatpickr-time .flatpickr-time-separator { + color: #94a3b8 !important; + } + + .dark .flatpickr-time .flatpickr-time-separator { + color: #64748b !important; + } + + .flatpickr-weekdays { + background: transparent !important; + } + diff --git a/resources/js/app.js b/resources/js/app.js index 94872e7..f668452 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -3,11 +3,32 @@ import './bootstrap'; import Alpine from 'alpinejs'; import collapse from '@alpinejs/collapse'; +// 初始化 Preline UI +import 'preline'; + +// 引入 Flatpickr 與語系 +import flatpickr from "flatpickr"; +import { MandarinTraditional } from "flatpickr/dist/l10n/zh-tw.js"; +import { Japanese } from "flatpickr/dist/l10n/ja.js"; + +const docLang = document.documentElement.lang.toLowerCase(); + +if (docLang.includes('zh')) { + flatpickr.localize(MandarinTraditional); + window.flatpickrLocale = MandarinTraditional; +} else if (docLang.includes('ja')) { + flatpickr.localize(Japanese); + window.flatpickrLocale = Japanese; +} else { + // English is the default in flatpickr + window.flatpickrLocale = 'default'; +} + +window.flatpickr = flatpickr; + Alpine.plugin(collapse); window.Alpine = Alpine; +// 確保其他套件都初始化完成後再啟動 Alpine Alpine.start(); - -// 初始化 Preline UI -import 'preline'; diff --git a/resources/views/admin/data-config/accounts.blade.php b/resources/views/admin/data-config/accounts.blade.php index 9c03969..49baa44 100644 --- a/resources/views/admin/data-config/accounts.blade.php +++ b/resources/views/admin/data-config/accounts.blade.php @@ -95,7 +95,10 @@ $roleSelectConfig = [ - + @if(auth()->user()->isSystemAdmin()) @@ -106,6 +109,7 @@ $roleSelectConfig = [ @endif +
@@ -211,7 +215,8 @@ $roleSelectConfig = [ + placeholder="{{ __('Search users...') }}" + @keydown.enter="$el.form.submit()">
@if(auth()->user()->isSystemAdmin()) @@ -222,6 +227,7 @@ $roleSelectConfig = [ @endif +
diff --git a/resources/views/admin/machines/index.blade.php b/resources/views/admin/machines/index.blade.php index 436a10e..d1af3b0 100644 --- a/resources/views/admin/machines/index.blade.php +++ b/resources/views/admin/machines/index.blade.php @@ -24,17 +24,17 @@ selectedMachine: null, slots: [], inventorySlots: [], + currentPage: 1, + lastPage: 1, init() { - const d = new Date(); - const today = [ - d.getFullYear(), - String(d.getMonth() + 1).padStart(2, '0'), - String(d.getDate()).padStart(2, '0') - ].join('-'); - this.startDate = today; - this.endDate = today; - this.$watch('activeTab', () => this.fetchLogs()); + const now = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + const formatDate = (date, time) => `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${time}`; + + this.startDate = formatDate(now, '00:00'); + this.endDate = formatDate(now, '23:59'); + this.$watch('activeTab', () => this.fetchLogs(1)); }, async openLogPanel(id, sn, name) { @@ -54,15 +54,21 @@ }, - async fetchLogs() { + async fetchLogs(page = 1) { this.loading = true; + this.currentPage = page; try { - let url = '/admin/machines/' + this.currentMachineId + '/logs-ajax?type=' + this.activeTab; + let url = '/admin/machines/' + this.currentMachineId + '/logs-ajax?type=' + this.activeTab + '&page=' + page; if (this.startDate) url += '&start_date=' + this.startDate; if (this.endDate) url += '&end_date=' + this.endDate; + const res = await fetch(url); const data = await res.json(); - if (data.success) this.logs = data.data.data || data.data || []; + if (data.success) { + this.logs = data.data || []; + this.currentPage = data.pagination.current_page; + this.lastPage = data.pagination.last_page; + } } catch (e) { console.error('fetchLogs error:', e); } finally { this.loading = false; } }, @@ -100,6 +106,13 @@ finally { this.inventoryLoading = false; } }, + formatDateTime(dateStr) { + if (!dateStr) return '--'; + const d = new Date(dateStr); + const pad = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; + }, + getSlotColorClass(slot) { if (!slot.expiry_date) return 'bg-slate-50/50 dark:bg-slate-800/50 text-slate-400 border-slate-200/60 dark:border-slate-700/50'; const todayStr = new Date().toISOString().split('T')[0]; @@ -390,19 +403,35 @@ - +
- +
-
@@ -554,6 +583,33 @@ + + +
+
+ + +
+ + / + +
+ + +
+
diff --git a/resources/views/admin/permission/roles.blade.php b/resources/views/admin/permission/roles.blade.php index 4b00f96..6dea08d 100644 --- a/resources/views/admin/permission/roles.blade.php +++ b/resources/views/admin/permission/roles.blade.php @@ -61,7 +61,10 @@ - + @if(auth()->user()->isSystemAdmin()) @@ -79,6 +82,7 @@ @endif + diff --git a/routes/api.php b/routes/api.php index 0619da4..e066d5f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -68,6 +68,9 @@ Route::prefix('v1')->middleware(['throttle:api'])->group(function () { // 統一商品主檔 API (B012 整合版) Route::match(['get', 'patch'], 'machine/products/B012', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getProducts']); + // 機台故障與異常上報 (B013) + Route::post('machine/error/B013', [App\Http\Controllers\Api\V1\App\MachineController::class, 'reportError']); + // 交易、發票與出貨 (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']);