[FEAT] 實作封閉迴圈交易審核與 API 規格標準化

1. 新增 B680 員工卡驗證 API 並更新相關技術文件 (SKILL.md, api-docs.php)。
2. 重構取貨碼與通行碼之日誌追蹤系統,新增 PickupCodeLog 與 PassCodeLog 模型及資料表遷移。
3. 修正 B660 API 路由方法,統一使用 POST 進行驗證。
4. 優化後台銷售中心介面,將「紀錄」頁籤統一為「使用紀錄」並優化狀態顯示組件。
5. 更新多語系 zh_TW.json 以符合新的 UI 命名規範。
6. 調整 Order 模型與 TransactionService 以支援更精確的封閉迴圈交易審核。
This commit is contained in:
sky121113 2026-05-05 17:50:26 +08:00
parent 15b975efac
commit fc93a0fb8c
33 changed files with 896 additions and 388 deletions

View File

@ -150,14 +150,6 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
> **此 REST API 已廢棄並移除**。機台異常上報已全面遷移至 **MQTT** 即時通訊架構。
> 詳細錯誤代碼請參閱:[MQTT 通訊規範 - B013 硬體代碼對照表](file:///home/mama/projects/star-cloud/.agents/skills/mqtt-communication-specs/SKILL.md#32-異常與錯誤上報-b013)
### 3.12 B010: [DEPRECATED / REMOVED] 心跳上報與狀態同步
> [!WARNING]
> **此 REST API 已廢棄並移除**。機台心跳上報與指令下發已全面遷移至 **MQTT** 架構。
> (註:溫度數據已標準化為 **Integer**,且雲端採「變動即記錄」規則)。
> 詳細格式請參閱:[MQTT 通訊規範 - B010 心跳上報](file:///home/mama/projects/star-cloud/.agents/skills/mqtt-communication-specs/SKILL.md#31-心跳上報-b010)
---
### 3.7 B014: 機台參數與金鑰下載 (Config Download)
用於機台引導階段 (Provisioning),向雲端請求支付金鑰、發票設定及機台正式 API Token。
@ -218,39 +210,23 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
---
### 3.9 B660: 取貨碼驗證與消耗 (Pickup Code)
處理機台端的代碼取貨流程。採用兩階段流程以確保庫存準確性。
### 3.9 B660: 取貨碼驗證 (Pickup Code)
處理機台端的 8 位數代碼取貨流程。
#### [B660] 取貨碼驗證與消耗 (兩階段)
* **Path**: `/api/v1/app/machine/pickup/verify/B660`
* **Method**: `POST` (驗證) / `PUT` (消耗)
* **Description**: 驗證 8 位數取貨碼,並於出貨完成後回報消耗以扣除庫存。
- **URL**: POST /api/v1/app/machine/pickup/verify/B660
- **Authentication**: Bearer Token (Header)
- **運作模式**:
- **POST (驗證)**: 提交 `code` 驗證。成功後回傳 `pickup_code_id` 與建議 `slot_no`
- **消耗機制**: 出貨完成後,由機台發送 MQTT `finalize` 訊息,並在 `payment_type` 帶入 `6` (取貨碼) 完成扣減。
#### [B670] 通行碼驗證 (維護專用)
* **Path**: `/api/v1/app/machine/passcode/verify/B670`
* **Method**: `POST`
* **Description**: 工程人員現場測試專用通行碼,不涉及庫存消耗。
| 參數 | 類型 | 必填 | 說明 | 範例 |
| :--- | :--- | :--- | :--- | :--- |
| code | String | 是 | 使用者輸入的 8 位數取貨碼 | "12345678" |
**Response (Success 200):**
- `pickup_code_id`: 代碼內部 ID供第二階段 PUT 使用。
- `slot_no`: 建議出貨貨道。
#### B. 消耗階段 (PUT)
當機台確認出貨成功後發送,雲端此時才正式執行 `decrement` 庫存與標記代碼已使用。
| 參數 | 類型 | 必填 | 說明 | 範例 |
| :--- | :--- | :--- | :--- | :--- |
| pickup_code_id | Integer | 是 | 驗證階段取得的 ID | 45 |
| status | Integer | 是 | 出貨結果 (1: 成功, 0: 失敗) | 1 |
| 參數 | 類型 | 說明 | 範例 |
| :--- | :--- | :--- | :--- |
| code | String | 8 位數取貨碼 | "12345678" |
---
### 3.10 B670: 通行碼驗證 (Pass Code)
專供維修或工程人員現場測試使用的通行碼。
專供維修或工程人員現場測試使用的通行碼。僅做身分確認,不涉及庫存消耗。
- **URL**: POST /api/v1/app/machine/passcode/verify/B670
- **Authentication**: Bearer Token (Header)
@ -264,23 +240,41 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
- `name`: 持有者姓名 (如:維護部 A 員)。
- `status`: "active"。
---
### 3.11 B680: 員工卡身分驗證 (Staff Card Verification)
機台端感應員工卡 (RFID/NFC) 後驗證卡片 UID。
- **URL**: POST /api/v1/app/machine/staff/verify/B680
- **Authentication**: Bearer Token (Header)
- **Request Body:**
| 參數 | 類型 | 必填 | 說明 | 範例 |
| :--- | :--- | :--- | :--- | :--- |
| code | String | 是 | 卡片 UID 碼 | "A1B2C3D4" |
**Response (Success 200):**
- `name`: 員工姓名。
- `code_id`: 員工卡 ID (用於 MQTT 交易關聯)。
- `status`: "active"。
> [!WARNING]
> **鎖定機制**B660 與 B670 若連續錯誤 5 次,該機台將被鎖定驗證功能 1 分鐘,以防暴力破解。
> **鎖定機制**B660, B670 與 B680 若連續錯誤 5 次,該機台將被鎖定驗證功能 1 分鐘。
---
### 3.10 B027: 贈品碼/優惠券驗證與消耗回報 (Free Gift Verify & Report)
### 3.12 B027: 贈品碼/優惠券驗證 (Free Gift Verify)
用於處理行銷贈品券、0 元購活動或特定的優惠券核銷流程。
- **URL**: POST|PUT /api/v1/app/sell/free-gift/B027
- **URL**: POST /api/v1/app/sell/free-gift/B027
- **Authentication**: Bearer Token (Header)
- **運作模式**:
- **POST**: 提交 `passCode` 驗證該贈品券是否有效。成功後雲端會回傳對應活動 ID 與商品配置。
- **PUT**: 當該贈品0 元商品)出貨完成後,向雲端回報消耗,確保優惠券不會重複核銷。
- **POST**: 提交 `passCode` 驗證。成功後雲端會回傳對應活動 ID 與商品配置。
- **消耗機制**: 出貨完成後,由機台發送 MQTT `finalize` 訊息進行消耗
---
### 3.11 B650: 會員身分驗證 (Member Verification)
### 3.13 B650: 會員身分驗證 (Member Verification)
機台端掃碼後驗證會員身分。
- **URL**: POST /api/v1/app/machine/member/verify/B650
@ -293,7 +287,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
---
### 3.12 B710: 機台時間同步 (Timer Sync)
### 3.14 B710: 機台時間同步 (Timer Sync)
校準機台 RTC 時間。
- **URL**: POST /api/v1/app/machine/timer/B710
@ -307,8 +301,8 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
| 原 API 代碼 | 名稱 | MQTT Topic | 備註 |
| :--- | :--- | :--- | :--- |
| B010 | 心跳上報 | `machine/{serial_no}/heartbeat` | [廢棄](#312-b010-deprecated--removed-心跳上報與狀態同步) |
| B013 | 異常上報 | `machine/{serial_no}/error` | [廢棄](#36-b013-deprecated--removed-機台故障與異常狀態上報) |
| B010 | 心跳上報 | `machine/{serial_no}/heartbeat` | [遷移](#312-b010-deprecated--removed-心跳上報與狀態同步) |
| B013 | 異常上報 | `machine/{serial_no}/error` | [遷移](#36-b013-deprecated--removed-機台故障與異常狀態上報) |
| B600 | 交易建立 | `machine/{serial_no}/transaction` | `action: create` |
| B601 | 發票紀錄 | `machine/{serial_no}/transaction` | `action: invoice` |
| B602 | 出貨結果 | `machine/{serial_no}/transaction` | `action: dispense` |
@ -329,8 +323,8 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
| `2` | 電子票證 | 包含悠遊卡、一卡通等 |
| `3` | 掃碼支付 | 一般掃碼支付 |
| `4` | 投幣機支付 | 實體紙鈔機 |
| `5` | 通行碼 | 原「通關密碼」 |
| `6` | 取貨碼 | |
| `5` | 通行碼 | 現場維護人員測試使用 |
| `6` | 取貨碼 | 包含 8 位數取貨碼與 0 元贈品碼 |
| `7` | 來店禮 | |
| `8` | 問卷 | |
| `9` | 零錢 | 硬幣投幣機 |
@ -341,7 +335,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
| `33` | TapPay - Pi 拍錢包 | |
| `34` | TapPay - 全盈+PAY | |
| `40` | 會員驗證取貨商品 | |
| `41` | 員工卡 | |
| `41` | 員工卡 | RFID / NFC 刷卡 |
| `50` ~ `54` | 線下付款 + TapPay | `50`=線下+30, `51`=線下+31... `54`=線下+34 |
| `60` | 點數 / 優惠卷全額折抵 | 點數全額折抵出貨 或 優惠卷全額折抵出貨 |
| `61` ~ `69` | 會員 + X | `61`=會員+1, `62`=會員+2... `69`=會員+9 |

BIN
Star_Cloud_April_Report.pdf Normal file

Binary file not shown.

Binary file not shown.

View File

@ -10,7 +10,9 @@ use App\Models\Transaction\DispenseRecord;
use App\Models\Transaction\PaymentType;
use App\Models\Transaction\OrderItem;
use App\Models\Transaction\PickupCode;
use App\Models\Transaction\PickupCodeLog;
use App\Models\Transaction\PassCode;
use App\Models\Transaction\PassCodeLog;
use App\Models\Machine\Machine;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
@ -151,7 +153,7 @@ class SalesController extends Controller
// 1. 取貨碼列表 (list)
if (!$isAjax || $tab === 'list') {
$query = PickupCode::with(['machine.slots.product', 'creator'])->latest();
$query = PickupCode::with(['machine.slots.product', 'creator', 'order'])->latest();
if ($request->search) {
$query->where(function ($q) use ($request) {
@ -176,15 +178,15 @@ class SalesController extends Controller
// 2. 操作紀錄 (logs)
if (!$isAjax || $tab === 'logs') {
$logQuery = SystemOperationLog::with('user:id,name')
->where('module', 'pickup_code');
$logQuery = PickupCodeLog::with(['user:id,name', 'machine:id,name,serial_no', 'pickupCode', 'order']);
if ($request->filled('search_log')) {
$search = $request->input('search_log');
$logQuery->where(function ($q) use ($search) {
$q->where('target_id', 'like', "%{$search}%")
->orWhere('new_values', 'like', "%{$search}%")
->orWhere('old_values', 'like', "%{$search}%");
$q->where('remark', 'like', "%{$search}%")
->orWhereHas('pickupCode', function($pq) use ($search) {
$pq->where('code', 'like', "%{$search}%");
});
});
}
@ -386,15 +388,16 @@ class SalesController extends Controller
// 2. 操作紀錄 (logs)
if (!$isAjax || $tab === 'logs') {
$logQuery = SystemOperationLog::with('user:id,name')
->where('module', 'pass_code');
$logQuery = PassCodeLog::with(['user:id,name', 'machine:id,name,serial_no', 'passCode']);
if ($request->filled('search_log')) {
$search = $request->input('search_log');
$logQuery->where(function ($q) use ($search) {
$q->where('target_id', 'like', "%{$search}%")
->orWhere('new_values', 'like', "%{$search}%")
->orWhere('old_values', 'like', "%{$search}%");
$q->where('remark', 'like', "%{$search}%")
->orWhereHas('passCode', function($pq) use ($search) {
$pq->where('code', 'like', "%{$search}%")
->orWhere('name', 'like', "%{$search}%");
});
});
}

View File

@ -16,7 +16,11 @@ use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Cache;
use App\Models\Machine\MachineStockMovement;
use App\Models\Transaction\PickupCode;
use App\Models\Transaction\PickupCodeLog;
use App\Models\Transaction\PassCode;
use App\Models\StaffCard;
use App\Models\StaffCardLog;
use App\Models\Transaction\PassCodeLog;
use App\Models\System\SystemOperationLog;
use App\Services\Machine\MachineService;
@ -505,16 +509,7 @@ class MachineController extends Controller
*/
public function verifyPickupCode(Request $request)
{
$machine = $request->get('machine');
$method = $request->method();
if ($method === 'POST') {
return $this->handlePickupVerify($request, $machine);
} elseif ($method === 'PUT') {
return $this->handlePickupConsume($request, $machine);
}
return response()->json(['success' => false, 'message' => 'Method not allowed'], 405);
return $this->handlePickupVerify($request, $request->get('machine'));
}
private function handlePickupVerify(Request $request, $machine)
@ -551,97 +546,35 @@ class MachineController extends Controller
Cache::forget("pickup_fails:{$machine->id}");
// 記錄到系統操作日誌供後台顯示
SystemOperationLog::create([
// 建立取貨碼專屬日誌 (驗證成功)
PickupCodeLog::create([
'company_id' => $machine->company_id,
'module' => 'pickup_code',
'machine_id' => $machine->id,
'pickup_code_id' => $pickupCode->id,
'action' => 'verify_success',
'target_id' => $pickupCode->id,
'target_type' => PickupCode::class,
'new_values' => ['code' => $pickupCode->code, 'machine_sn' => $machine->serial_no],
'note' => "log.pickup.verify_success_note",
'remark' => "log.pickup.verify_success",
'raw_data' => [
'machine_sn' => $machine->serial_no,
'code' => $pickupCode->code,
'slot' => $pickupCode->slot_no
]
]);
// 移除 StaffCardLog 重複紀錄,因為取貨碼本身有 status 和 order_id 可以追蹤
// 憑證的「核銷」將由 MQTT finalizeTransaction 流程處理,不再需要 B660 PUT
return response()->json([
'success' => true,
'code' => 200,
'data' => [
'slot_no' => $pickupCode->slot_no,
'pickup_code_id' => $pickupCode->id,
'code_id' => $pickupCode->id, // 統一回傳 code_id
'status' => 'active'
]
]);
}
private function handlePickupConsume(Request $request, $machine)
{
$pickupCodeId = $request->input('pickup_code_id');
$status = $request->input('status'); // 1: success, 0: failed
$pickupCode = PickupCode::where('machine_id', $machine->id)
->where('id', $pickupCodeId)
->where('status', 'active')
->first();
if (!$pickupCode) {
return response()->json(['success' => false, 'message' => 'Pickup code not found or already used'], 404);
}
if ($status == '1') {
// 扣除庫存
$slot = $machine->slots()->where('slot_no', $pickupCode->slot_no)->first();
if ($slot && $slot->stock > 0) {
$oldStock = $slot->stock;
$slot->decrement('stock');
$slot->refresh();
// 記錄取貨碼 pickup 庫存異動流水帳
app(MachineService::class)->recordStockMovement(
$slot,
-1,
MachineStockMovement::TYPE_PICKUP,
$pickupCode,
"取貨碼核銷消耗,碼: {$pickupCode->code}"
);
$pickupCode->update(['status' => 'used', 'used_at' => now()]);
ProcessStateLog::dispatch($machine->id, $machine->company_id, "log.pickup.consume_success", 'info', ['code' => $pickupCode->code, 'slot' => $slot->slot_no]);
SystemOperationLog::create([
'company_id' => $machine->company_id,
'module' => 'pickup_code',
'action' => 'consume',
'target_id' => $pickupCode->id,
'target_type'=> PickupCode::class,
'new_values' => [
'code' => $pickupCode->code,
'machine_sn' => $machine->serial_no,
'slot' => $slot->slot_no,
'old' => $oldStock,
'new' => ($oldStock - 1)
],
'note' => "log.pickup.consume_success_note",
]);
} else {
return response()->json(['success' => false, 'message' => 'Out of stock'], 400);
}
} else {
ProcessStateLog::dispatch($machine->id, $machine->company_id, "log.pickup.dispense_failed", 'error', ['code' => $pickupCode->code]);
SystemOperationLog::create([
'company_id' => $machine->company_id,
'module' => 'pickup_code',
'action' => 'consume_failed',
'target_id' => $pickupCode->id,
'target_type' => PickupCode::class,
'new_values' => ['code' => $pickupCode->code, 'machine_sn' => $machine->serial_no],
'note' => "log.pickup.dispense_failed_note",
]);
}
return response()->json(['success' => true, 'code' => 200]);
}
/**
* B670: Verify Pass Code (For Technicians)
@ -671,21 +604,73 @@ class MachineController extends Controller
return response()->json(['success' => false, 'code' => 404], 404);
}
SystemOperationLog::create([
// 建立通行碼專屬日誌 (驗證成功)
PassCodeLog::create([
'company_id' => $machine->company_id,
'module' => 'pass_code',
'machine_id' => $machine->id,
'pass_code_id' => $passCode->id,
'status' => 'verified',
'action' => 'verify_success',
'target_id' => $passCode->id,
'target_type' => PassCode::class,
'new_values' => ['code' => $passCode->code, 'machine_sn' => $machine->serial_no],
'note' => "log.passcode.verify_success_note",
'raw_data' => ['machine_sn' => $machine->serial_no, 'code' => $passCode->code]
]);
return response()->json([
'success' => true,
'code' => 200,
'data' => [
'name' => $passCode->name,
'code_id' => $passCode->id, // 統一回傳 code_id
'status' => 'active'
]
]);
}
/**
* B680: Verify Staff Card (New)
*/
public function verifyStaffCard(Request $request)
{
$machine = $request->get('machine');
$validator = Validator::make($request->all(), [
'code' => 'required|string', // 卡片 UID
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'code' => 400], 400);
}
$uid = $request->input('code');
// 搜尋該公司的有效員工卡 (遵循租戶隔離)
$staffCard = StaffCard::where('company_id', $machine->company_id)
->where('card_uid', $uid)
->where('status', 'active')
->first();
if (!$staffCard) {
ProcessStateLog::dispatch($machine->id, $machine->company_id, "[StaffCard] Verification failed: $uid", 'warning');
return response()->json(['success' => false, 'code' => 404, 'message' => 'Card not found or inactive'], 404);
}
// 建立刷卡日誌 (作為閉環起點)
StaffCardLog::create([
'company_id' => $machine->company_id,
'staff_card_id' => $staffCard->id,
'machine_id' => $machine->id,
'payment_type' => 41, // Staff Card
'code_id' => $staffCard->id,
'action' => 'verify_success',
'raw_data' => ['machine_sn' => $machine->serial_no, 'uid' => $uid]
]);
return response()->json([
'success' => true,
'code' => 200,
'data' => [
'name' => $staffCard->name,
'code_id' => $staffCard->id, // 統一回傳 code_id
'status' => 'active'
]
]);

View File

@ -17,7 +17,11 @@ class StaffCardLog extends Model
'company_id',
'staff_card_id',
'machine_id',
'action_type',
'payment_type',
'code_id',
'order_id',
'action',
'remark',
'raw_data',
];

View File

@ -31,6 +31,7 @@ class Order extends Model
'payment_response',
'payment_at',
'member_barcode',
'code_id',
'invoice_info',
'machine_time',
'status',

View File

@ -0,0 +1,47 @@
<?php
namespace App\Models\Transaction;
use App\Models\Machine\Machine;
use App\Traits\TenantScoped;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PassCodeLog extends Model
{
use TenantScoped;
protected $fillable = [
'company_id',
'machine_id',
'pass_code_id',
'action',
'user_id',
'order_id',
'raw_data',
];
protected $casts = [
'raw_data' => 'array',
];
public function passCode(): BelongsTo
{
return $this->belongsTo(PassCode::class);
}
public function machine(): BelongsTo
{
return $this->belongsTo(Machine::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(\App\Models\System\User::class);
}
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
}

View File

@ -22,6 +22,7 @@ class PickupCode extends Model
'used_at',
'status',
'created_by',
'order_id',
];
/**
@ -63,6 +64,14 @@ class PickupCode extends Model
return $this->belongsTo(User::class, 'created_by');
}
/**
* 關聯訂單
*/
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
/**
* 判斷是否可用
*/

View File

@ -0,0 +1,48 @@
<?php
namespace App\Models\Transaction;
use Illuminate\Database\Eloquent\Model;
use App\Models\Machine\Machine;
use App\Models\System\User;
use App\Traits\TenantScoped;
class PickupCodeLog extends Model
{
use TenantScoped;
protected $fillable = [
'company_id',
'machine_id',
'pickup_code_id',
'order_id',
'action',
'user_id', // Optional, for admin actions recorded here
'remark',
'raw_data'
];
protected $casts = [
'raw_data' => 'array'
];
public function machine()
{
return $this->belongsTo(Machine::class);
}
public function pickupCode()
{
return $this->belongsTo(PickupCode::class);
}
public function order()
{
return $this->belongsTo(Order::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -8,6 +8,10 @@ use App\Models\Transaction\Invoice;
use App\Models\Transaction\DispenseRecord;
use Illuminate\Support\Facades\DB;
use App\Models\Machine\Machine;
use App\Models\StaffCardLog;
use App\Models\Transaction\PickupCode;
use App\Models\Transaction\PickupCodeLog;
use App\Models\Transaction\PassCodeLog;
use Illuminate\Support\Facades\Log;
class TransactionService
@ -49,6 +53,7 @@ class TransactionService
'payment_request' => $data['payment_request'] ?? null,
'payment_response' => $data['payment_response'] ?? null,
'member_barcode' => $data['member_barcode'] ?? null,
'code_id' => $data['code_id'] ?? null,
'invoice_info' => $data['invoice_info'] ?? null,
'machine_time' => $data['machine_time'] ?? now(),
'payment_at' => now(),
@ -162,7 +167,8 @@ class TransactionService
*/
public function finalizeTransaction(array $data): Order
{
$flowId = $data['order']['flow_id'] ?? null;
// 優先從 Root 讀取,再從 order 內讀取
$flowId = $data['flow_id'] ?? ($data['order']['flow_id'] ?? null);
// 冪等性檢查:如果 flow_id 已經存在,直接回傳既有訂單
if ($flowId) {
@ -175,14 +181,15 @@ class TransactionService
}
}
return DB::transaction(function () use ($data) {
return DB::transaction(function () use ($data, $flowId) {
$serialNo = $data['serial_no'];
// 1. Process Order (B600)
$orderData = $data['order'];
$orderData['serial_no'] = $serialNo;
// 確保提取 payment_type (可能在 root 或 order 內)
// 確保提取 payment_type 與 flow_id (可能在 root 或 order 內)
$orderData['flow_id'] = $flowId;
if (!isset($orderData['payment_type']) && isset($data['payment_type'])) {
$orderData['payment_type'] = $data['payment_type'];
}
@ -214,6 +221,68 @@ class TransactionService
}
}
// 4. 閉環勾稽 (Closed Loop Check)
$codeId = $data['order']['code_id'] ?? ($data['code_id'] ?? null);
$paymentType = (int)$order->payment_type;
if ($codeId) {
switch ($paymentType) {
case 41: // 員工卡 (Staff Card)
// 建立員工卡核銷日誌
StaffCardLog::create([
'company_id' => $order->company_id,
'machine_id' => $order->machine_id,
'staff_card_id' => $codeId,
'payment_type' => 41,
'code_id' => $codeId,
'order_id' => $order->id,
'action' => 'consume',
'remark' => "MQTT 交易完成自動核銷,訂單: #{$order->id}"
]);
break;
case 6: // 取貨碼 (Pickup Code)
$pickupCode = PickupCode::where('machine_id', $order->machine_id)
->where('id', $codeId)
->where('status', 'active')
->first();
if ($pickupCode) {
$pickupCode->update([
'order_id' => $order->id,
'status' => 'used',
'used_at' => now()
]);
// 建立核銷日誌 (閉環最後一哩路)
PickupCodeLog::create([
'company_id' => $order->company_id,
'machine_id' => $order->machine_id,
'pickup_code_id' => $pickupCode->id,
'order_id' => $order->id,
'action' => 'consume',
'remark' => "MQTT 交易完成自動核銷,訂單: #{$order->id}",
'raw_data' => [
'slot' => $pickupCode->slot_no,
'code' => $pickupCode->code
]
]);
}
break;
case 5: // 通行碼 (Pass Code)
// 建立通行碼核銷日誌
PassCodeLog::create([
'company_id' => $order->company_id,
'machine_id' => $order->machine_id,
'pass_code_id' => $codeId,
'order_id' => $order->id,
'action' => 'completed',
'remark' => "MQTT 交易完成自動紀錄取貨,訂單: #{$order->id}"
]);
break;
}
}
return $order;
});
}

View File

@ -326,11 +326,11 @@ return [
],
],
[
'name' => 'B660: 取貨碼驗證與消耗 (Pickup Code)',
'name' => 'B660: 取貨碼驗證 (Pickup Code)',
'slug' => 'b660-pickup-code',
'method' => 'POST / PUT',
'method' => 'POST',
'path' => '/api/v1/app/machine/pickup/verify/B660',
'description' => '處理兩階段代碼取貨。POST 驗證有效性PUT 回報出貨成功後正式扣庫存。',
'description' => '處理代碼取貨驗證。成功後回傳對應資訊,正式消耗改由 MQTT finalize 處理。',
'headers' => [
'Authorization' => 'Bearer <api_token>',
'Content-Type' => 'application/json',
@ -338,19 +338,9 @@ return [
'parameters' => [
'code' => [
'type' => 'string',
'description' => '8 位數取貨碼 (僅限 POST)',
'description' => '8 位數取貨碼',
'example' => '12345678'
],
'pickup_code_id' => [
'type' => 'integer',
'description' => '代碼內部 ID (僅限 PUT)',
'example' => 45
],
'status' => [
'type' => 'integer',
'description' => '出貨狀態 (僅限 PUT: 1:成功, 0:失敗)',
'example' => 1
],
],
'response' => [
'success' => true,
@ -360,7 +350,7 @@ return [
'status' => 'active'
]
],
'notes' => '安全性:連續錯誤 5 次將鎖定該機台驗證功能 1 分鐘。'
'notes' => '安全性:連續錯誤 5 次將鎖定該機台驗證功能 1 分鐘。消耗流程已遷移至 MQTT。'
],
[
'name' => 'B670: 通行碼驗證 (Pass Code)',
@ -389,6 +379,34 @@ return [
],
'notes' => '僅供現場測試使用,錯誤過多同樣會觸發鎖定。'
],
[
'name' => 'B680: 員工卡身分驗證 (Staff Card Verification)',
'slug' => 'b680-staff-card-verify',
'method' => 'POST',
'path' => '/api/v1/app/machine/staff/verify/B680',
'description' => '機台端感應員工卡 (RFID/NFC) 後驗證卡片 UID。',
'headers' => [
'Authorization' => 'Bearer <api_token>',
'Content-Type' => 'application/json',
],
'parameters' => [
'code' => [
'type' => 'string',
'required' => true,
'description' => '卡片 UID 碼',
'example' => 'A1B2C3D4'
]
],
'response' => [
'success' => true,
'data' => [
'name' => '員工姓名',
'code_id' => 12,
'status' => 'active'
]
],
'notes' => '驗證成功後會建立刷卡日誌。連續錯誤同樣會觸發鎖定。'
],
[
'name' => 'B710: 機台時間同步 (Timer Sync)',
'slug' => 'b710-timer-sync',
@ -406,11 +424,11 @@ return [
]
],
[
'name' => 'B027: 贈品碼/優惠券驗證與消耗 (Free Gift Verify & Consume)',
'name' => 'B027: 贈品碼/優惠券驗證 (Free Gift Verify)',
'slug' => 'b027-freebie-code',
'method' => 'POST / PUT',
'method' => 'POST',
'path' => '/api/v1/app/sell/free-gift/B027',
'description' => 'POST 用於驗證碼有效性PUT 用於回報出貨成功並消耗代碼。',
'description' => '驗證贈品券有效性。成功後回傳對應資訊,正式消耗改由 MQTT finalize 處理。',
'headers' => [
'Authorization' => 'Bearer <api_token>',
'Content-Type' => 'application/json',
@ -418,28 +436,18 @@ return [
'parameters' => [
'passCode' => [
'type' => 'string',
'description' => '贈品碼 (POST)',
'description' => '贈品碼/優惠券代碼',
'example' => 'FREE888'
],
'accessCodeId' => [
'type' => 'string',
'description' => '代碼 ID (PUT)',
],
'status' => [
'type' => 'string',
'description' => '出貨狀態 (PUT: 1:成功, 0:失敗)',
],
],
'response_parameters' => [
'success' => ['type' => 'boolean', 'description' => '驗證結果'],
'res1' => ['type' => 'string', 'description' => '雲端關聯 ID'],
],
'request' => [
'passCode' => 'FREE888'
],
'response' => [
'success' => true,
'message' => 'Free gift verified'
'data' => [
'res1' => 'cloud_id_123',
'message' => 'Free gift verified'
]
],
'notes' => '消耗流程已遷移至 MQTT。'
],
],
],

View File

@ -0,0 +1,35 @@
<?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('staff_card_logs', function (Blueprint $table) {
$table->integer('payment_type')->after('machine_id')->nullable()->comment('支付/通行類型: 5:Pass, 11:Pickup, 41:Staff');
$table->unsignedBigInteger('code_id')->after('payment_type')->nullable()->comment('對應資料表的ID');
$table->unsignedBigInteger('order_id')->after('code_id')->nullable()->index()->comment('關聯訂單ID');
$table->string('status')->after('order_id')->default('verified')->comment('狀態: verified, completed');
// 讓原本的欄位可空,因為之後統一用 code_id
$table->unsignedBigInteger('staff_card_id')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('staff_card_logs', function (Blueprint $table) {
$table->dropColumn(['payment_type', 'code_id', 'order_id', 'status']);
$table->unsignedBigInteger('staff_card_id')->nullable(false)->change();
});
}
};

View File

@ -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('orders', function (Blueprint $table) {
$table->string('code_id')->nullable()->after('member_barcode')->index()->comment('驗證碼 ID (閉環追蹤用)');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('code_id');
});
}
};

View File

@ -0,0 +1,44 @@
<?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
{
// 1. 取貨碼表增加 order_id 欄位
Schema::table('pickup_codes', function (Blueprint $table) {
$table->unsignedBigInteger('order_id')->after('created_by')->nullable()->index()->comment('關聯訂單ID');
});
// 2. 建立通行碼專用的使用紀錄表 (各自的紀錄)
Schema::create('pass_code_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('company_id')->constrained('companies')->onDelete('cascade');
$table->foreignId('machine_id')->constrained('machines')->onDelete('cascade');
$table->foreignId('pass_code_id')->constrained('pass_codes')->onDelete('cascade');
$table->unsignedBigInteger('order_id')->nullable()->index()->comment('關聯訂單ID');
$table->string('status')->default('verified')->comment('狀態: verified, completed');
$table->json('raw_data')->nullable();
$table->timestamps();
$table->index(['company_id', 'machine_id', 'pass_code_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('pass_code_logs');
Schema::table('pickup_codes', function (Blueprint $table) {
$table->dropColumn('order_id');
});
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('pickup_code_logs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('company_id')->nullable()->index();
$table->unsignedBigInteger('machine_id')->nullable()->index();
$table->unsignedBigInteger('pickup_code_id')->index();
$table->unsignedBigInteger('order_id')->nullable()->index();
$table->unsignedBigInteger('user_id')->nullable()->index();
$table->string('action')->index(); // create, update, verify_success, verify_failed, used
$table->string('status')->nullable();
$table->text('remark')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('pickup_code_logs');
}
};

View File

@ -0,0 +1,29 @@
<?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('pass_code_logs', function (Blueprint $table) {
$table->string('action')->after('pass_code_id')->nullable()->index()->comment('動作類型');
$table->unsignedBigInteger('user_id')->nullable()->after('action')->index()->comment('操作人員ID');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('pass_code_logs', function (Blueprint $table) {
$table->dropColumn(['action', 'user_id']);
});
}
};

View File

@ -0,0 +1,36 @@
<?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('pickup_code_logs', function (Blueprint $table) {
$table->dropColumn('status');
});
Schema::table('pass_code_logs', function (Blueprint $table) {
$table->dropColumn('status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('pickup_code_logs', function (Blueprint $table) {
$table->string('status')->nullable()->after('action');
});
Schema::table('pass_code_logs', function (Blueprint $table) {
$table->string('status')->nullable()->after('action');
});
}
};

View File

@ -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('pickup_code_logs', function (Blueprint $table) {
$table->json('raw_data')->nullable()->after('remark');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('pickup_code_logs', function (Blueprint $table) {
$table->dropColumn('raw_data');
});
}
};

View File

@ -0,0 +1,43 @@
<?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('staff_card_logs', function (Blueprint $table) {
$table->string('action')->nullable()->after('order_id')->comment('執行動作');
$table->string('remark')->nullable()->after('action')->comment('備註');
});
// 資料遷移
DB::table('staff_card_logs')->where('status', 'verified')->where('action_type', 'swipe')->update(['action' => 'verify_success']);
DB::table('staff_card_logs')->where('status', 'completed')->where('action_type', 'consume')->update(['action' => 'consume']);
Schema::table('staff_card_logs', function (Blueprint $table) {
$table->dropColumn(['status', 'action_type']);
});
}
public function down(): void
{
Schema::table('staff_card_logs', function (Blueprint $table) {
$table->string('status')->nullable()->after('order_id');
$table->string('action_type')->nullable()->after('status');
});
// 恢復資料
DB::table('staff_card_logs')->where('action', 'verify_success')->update(['status' => 'verified', 'action_type' => 'swipe']);
DB::table('staff_card_logs')->where('action', 'consume')->update(['status' => 'completed', 'action_type' => 'consume']);
Schema::table('staff_card_logs', function (Blueprint $table) {
$table->dropColumn(['action', 'remark']);
});
}
};

View File

@ -47,6 +47,7 @@
"Add Pickup Code": "新增取貨碼",
"Add Product": "新增商品",
"Add Role": "新增角色",
"Add Staff Card": "新增員工識別卡",
"Add Warehouse": "新增倉庫",
"Add new inventory items to your warehouse": "將新商品入庫至您的倉庫",
"Address": "地址",
@ -71,7 +72,7 @@
"Advertisement deleted successfully.": "廣告刪除成功。",
"Advertisement updated successfully": "廣告更新成功",
"Advertisement updated successfully.": "廣告更新成功。",
"Affiliated Company": "公司名稱",
"Affiliated Company": "所屬公司",
"Affiliated Unit": "公司名稱",
"Affiliation": "所屬單位",
"After Qty": "異動後數量",
@ -171,30 +172,6 @@
"Back to History": "返回紀錄",
"Back to List": "返回列表",
"Badge Settings": "識別證",
"Staff Identification Management": "員工識別管理",
"Staff List": "員工列表",
"Usage Logs": "使用日誌",
"Employee ID": "員工編號",
"IC Card UID": "IC 卡號",
"Last Used": "最後使用",
"Manage employee IC cards and track usage logs.": "管理員工 IC 卡並追蹤使用日誌。",
"Search by ID or Name...": "搜尋員工編號或姓名...",
"Search by ID, Name or UID...": "搜尋員工編號、姓名或卡號...",
"No staff cards found": "找不到員工識別卡",
"No usage logs found": "找不到使用日誌",
"Time": "時間",
"Employee": "員工",
"Action": "操作",
"Machine": "機台",
"Add Staff Card": "新增員工識別卡",
"Edit Staff Card": "編輯員工識別卡",
"Staff Name": "員工姓名",
"Never Used": "從未使用",
"No staff cards found.": "找不到員工識別卡。",
"No usage logs found.": "找不到使用日誌。",
"Staff card created successfully": "員工識別卡建立成功",
"Staff card updated successfully": "員工識別卡更新成功",
"Staff card deleted successfully": "員工識別卡刪除成功",
"Barcode": "條碼",
"Barcode / Material": "條碼 / 物料編碼",
"Basic Information": "基本資訊",
@ -475,6 +452,7 @@
"Edit Role Permissions": "編輯角色權限",
"Edit Settings": "編輯設定",
"Edit Slot": "編輯貨道",
"Edit Staff Card": "編輯員工識別卡",
"Edit Sub Account Role": "編輯子帳號角色",
"Edit Warehouse": "編輯倉庫",
"Electronic Invoice": "電子發票",
@ -487,6 +465,8 @@
"Elevator sensor error": "升降箱感測異常",
"Email": "電子郵件",
"Emblem / Label": "標章 / 標籤",
"Employee": "員工",
"Employee ID": "員工編號",
"Empty": "空白",
"Empty Slot": "空貨道",
"Empty Slots": "空貨道",
@ -683,6 +663,7 @@
"Hopper heating timeout": "料斗箱加熱逾時",
"Hopper overheated": "料斗箱過熱",
"Hrs": "小時",
"IC Card UID": "IC 卡號",
"ITEM": "ITEM",
"Identity & Codes": "識別與代碼",
"Image": "圖片",
@ -708,8 +689,8 @@
"Invoice Date": "開立日期",
"Invoice Information": "發票資訊",
"Invoice Number": "發票號碼",
"Invoice Number / Time": "發票號碼 / 時間",
"Invoice Number / Flow ID": "發票號碼 / 流水號",
"Invoice Number / Time": "發票號碼 / 時間",
"Invoice Status": "發票開立狀態",
"Issued At": "開立時間",
"Item List": "商品清單",
@ -732,6 +713,7 @@
"Last Sync": "最後通訊",
"Last Time": "最後時間",
"Last Updated": "最後更新日期",
"Last Used": "最後使用",
"Latitude": "緯度",
"Lease": "租賃",
"Leave empty for permanent code": "留空表示永久有效",
@ -777,14 +759,15 @@
"Loyalty & Features": "行銷與點數",
"Machine": "機台",
"Machine / Date": "機台 / 日期",
"Machine / Pass Code": "機台 / 通行碼",
"Machine / Slot": "機台 / 貨道",
"Machine Flow ID": "機台流水號",
"Machine Flow ID / Time": "機台流水號 / 時間",
"Machine Advertisement Settings": "機台廣告設置",
"Machine Count": "機台數量",
"Machine Details": "機台詳情",
"Machine Distribution": "機台分布",
"Machine Distribution Map": "機台分布地圖",
"Machine Flow ID": "機台流水號",
"Machine Flow ID / Time": "機台流水號 / 時間",
"Machine Images": "機台照片",
"Machine Info": "機台資訊",
"Machine Information": "機台資訊",
@ -846,6 +829,7 @@
"Manage ad materials and machine playback settings": "管理廣告素材與機台播放設定",
"Manage administrative and tenant accounts": "管理系統管理者與客戶帳號",
"Manage all tenant accounts and validity": "管理所有客戶帳號及其效期",
"Manage employee IC cards and track usage logs.": "管理員工 IC 卡並追蹤使用日誌。",
"Manage multi-use authorization codes for testing and maintenance": "管理用於測試與維修的多用途授權碼",
"Manage stock levels, stock-in orders, and movement history": "追蹤庫存水位、進貨單與異動紀錄",
"Manage stock transfers between warehouses and machine returns": "建立倉庫間調撥或機台退庫單",
@ -913,6 +897,7 @@
"Name in Traditional Chinese": "繁體中文名稱",
"Needs replenishment": "需要補貨",
"Never Connected": "從未連線",
"Never Used": "從未使用",
"New Command": "新增指令",
"New Machine Name": "新機台名稱",
"New Password": "新密碼",
@ -975,10 +960,14 @@
"No slot data available": "尚無貨道資料",
"No slots found": "未找到貨道資訊",
"No slots need replenishment": "無需補貨的貨道",
"No staff cards found": "找不到員工識別卡",
"No staff cards found.": "找不到員工識別卡。",
"No stock data found": "查無庫存資料",
"No stock-in orders found": "查無進貨單紀錄",
"No transaction orders found": "查無交易訂單",
"No transfer orders found": "查無調撥單紀錄",
"No usage logs found": "找不到使用日誌",
"No usage logs found.": "找不到使用日誌。",
"No users found": "找不到用戶資料",
"No warehouses found": "未找到倉庫",
"None": "無",
@ -1017,6 +1006,7 @@
"Optimized Performance": "效能最佳化",
"Optimized for display. Supported formats: JPG, PNG, WebP.": "已針對顯示進行優化。支援格式JPG, PNG, WebP。",
"Optional": "選填",
"Optional notes...": "備註內容(選填)...",
"Optional remarks": "選填備註",
"Optional remarks...": "選填備註...",
"Order Details": "訂單詳情",
@ -1288,6 +1278,7 @@
"Sales Activity": "銷售活動",
"Sales Management": "銷售管理",
"Sales Permissions": "銷售管理權限",
"Sales Record": "銷售紀錄",
"Sales Records": "銷售紀錄",
"Save": "儲存",
"Save Changes": "儲存變更",
@ -1315,6 +1306,10 @@
"Search Product": "搜尋商品",
"Search Product / Slot...": "搜尋商品名稱 / 貨道...",
"Search accounts...": "搜尋帳號...",
"Search by ID or Name": "搜尋員工編號或姓名",
"Search by ID or Name...": "搜尋員工編號或姓名...",
"Search by ID, Name or UID": "搜尋員工編號、姓名或卡號",
"Search by ID, Name or UID...": "搜尋員工編號、姓名或卡號...",
"Search by code, machine name or serial...": "搜尋代碼、機台名稱或序號...",
"Search by code, name or machine...": "搜尋代碼、名稱或機台...",
"Search by name or S/N...": "搜尋名稱或序號...",
@ -1344,7 +1339,7 @@
"Select All": "全選",
"Select Cargo Lane": "選擇貨道",
"Select Category": "選擇類別",
"Select Company": "選擇公司名稱",
"Select Company": "選擇公司",
"Select Company (Default: System)": "選擇公司 (預設:系統)",
"Select Date Range": "選擇建立日期區間",
"Select Machine": "選擇機台",
@ -1426,7 +1421,13 @@
"Spring": "彈簧",
"Spring Channel Limit": "彈簧貨道上限",
"Spring Limit": "彈簧貨道上限",
"Staff Identification Management": "員工識別管理",
"Staff List": "員工列表",
"Staff Name": "員工姓名",
"Staff Stock": "人員庫存",
"Staff card created successfully": "員工識別卡建立成功",
"Staff card deleted successfully": "員工識別卡刪除成功",
"Staff card updated successfully": "員工識別卡更新成功",
"Standby": "待機廣告",
"Standby Ad": "待機廣告",
"Start Date": "起始日",
@ -1600,6 +1601,7 @@
"Unified Operational Timeline": "整合式營運時序圖",
"Units": "台",
"Unknown": "未知",
"Unknown Machine": "未知機台",
"Unknown Product": "未知商品",
"Unknown error": "未知的錯誤",
"Unlimited": "無限期",
@ -1622,6 +1624,10 @@
"Upload New Images": "上傳新照片",
"Upload Video": "上傳影片",
"Uploading new images will replace all existing images.": "上傳新照片將會取代所有現有照片。",
"Usage Logs": "使用紀錄",
"Verified": "驗證成功",
"Consumed": "取貨完成",
"Usage Completed": "使用完成",
"Used": "已使用",
"User": "一般用戶",
"User Info": "用戶資訊",
@ -1632,14 +1638,13 @@
"Utilization Timeline": "稼動時序",
"Utilization, OEE and Operational Intelligence": "稼動率、OEE 與營運情報",
"Utilized Time": "稼動持續時間",
"Valid": "有效",
"Valid Ticket": "有效憑證",
"Valid Until": "有效期限至",
"Validation Error": "驗證錯誤",
"Validity Period": "有效期限",
"Validity Period (Days)": "有效期限 (天)",
"Validity Period (Hours)": "有效期限 (小時)",
"Valid": "有效",
"Void": "作廢",
"Vending": "販賣頁",
"Vending Page": "販賣頁",
"Venue Management": "場地管理",
@ -1656,6 +1661,7 @@
"View slot-level inventory for each machine": "查看各機台貨道庫存",
"Visit Gift": "來店禮",
"Visual overview of all machine locations": "所有機台位置的視覺化概覽",
"Void": "作廢",
"Void Invoice": "作廢發票",
"Waiting": "等待中",
"Waiting for Payment": "等待付款",
@ -1724,8 +1730,10 @@
"command.reload_stock": "同步庫存",
"command.unlock": "解鎖",
"companies": "客戶管理",
"completed": "取貨完成",
"consume": "取貨成功",
"consume_failed": "取貨失敗",
"consumed": "核銷完成",
"create": "建立",
"data-config": "資料設定",
"data-config.sub-account-roles": "子帳號角色",
@ -1738,9 +1746,12 @@
"e.g. Taiwan Star": "例如:台灣之星",
"e.g. Test Code for Maintenance": "例如:維修測試代碼",
"e.g. johndoe": "例如xiaoming",
"e.g., 12345678": "例12345678",
"e.g., Beverage": "例如:飲料",
"e.g., Company Standard Pay": "例如:公司標準支付",
"e.g., Drinks": "例如Drinks",
"e.g., John Doe": "例:王小明",
"e.g., S001": "例S001",
"e.g., Taipei Station": "例如:台北車站",
"e.g., お飲み物": "例如:お飲み物",
"enabled": "啟用",
@ -1840,29 +1851,11 @@
"used": "已使用",
"user": "一般用戶",
"vending": "販賣頁",
"verified": "驗證成功",
"verify_success": "驗證成功",
"video": "影片",
"visit_gift": "來店禮",
"vs Yesterday": "較昨日",
"warehouses": "倉庫管理",
"待填寫": "待填寫",
"Manage employee IC cards and track usage logs.": "管理員工 IC 卡並追蹤使用日誌。",
"Search by ID or Name": "搜尋員工編號或姓名",
"Search by ID, Name or UID": "搜尋員工編號、姓名或卡號",
"Search by ID or Name...": "搜尋員工編號或姓名...",
"Search by ID, Name or UID...": "搜尋員工編號、姓名或卡號...",
"No staff cards found": "找不到員工識別卡",
"No usage logs found": "找不到使用日誌",
"Affiliated Company": "所屬公司",
"All Companies": "所有公司",
"Select Company": "選擇公司",
"Unknown Machine": "未知機台",
"Time": "時間",
"Employee": "員工",
"Action": "操作",
"Unknown": "未知",
"e.g., S001": "例S001",
"e.g., John Doe": "例:王小明",
"e.g., 12345678": "例12345678",
"Optional notes...": "備註內容(選填)..."
"待填寫": "待填寫"
}

View File

@ -76,6 +76,9 @@
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Action') }}
</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Status') }}
</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@ -107,7 +110,32 @@
</div>
</td>
<td class="px-6 py-6">
<x-status-badge :status="$log->action_type" size="sm" />
<x-status-badge :status="$log->action" size="sm" />
</td>
<td class="px-6 py-6">
<div class="flex items-center gap-2">
@if($log->action === 'consume')
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black uppercase tracking-widest bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 shadow-sm shadow-emerald-500/5">
<svg class="w-3 h-3 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
{{ __('Usage Completed') }}
</span>
@if($log->order_id)
<a href="{{ route('admin.sales.orders.show', $log->order_id) }}"
class="p-1.5 rounded-lg bg-slate-100 dark:bg-white/5 text-slate-400 hover:text-cyan-500 transition-colors"
title="{{ __('View Order') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
@endif
@else
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black uppercase tracking-widest bg-slate-500/10 text-slate-500 border border-slate-500/10">
{{ __('Verified Only') }}
</span>
@endif
</div>
</td>
</tr>
@empty
@ -139,30 +167,33 @@
</div>
</div>
<x-status-badge :status="$log->action_type" size="sm" />
<x-status-badge :status="$log->action" size="sm" />
</div>
<!-- Info Grid -->
<div class="grid grid-cols-1 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
{{ __('Machine') }}
{{ __('Status') }}
</p>
<div class="flex items-center gap-2">
<span class="text-sm font-bold text-slate-700 dark:text-slate-300">
{{ $log->machine?->name ?? __('Unknown Machine') }}
</span>
<span class="text-xs font-mono font-bold text-slate-400">({{ $log->machine?->serial_no ?? '-' }})</span>
<div class="flex items-center gap-3">
@if($log->action === 'consume')
<span class="text-xs font-bold text-emerald-500 flex items-center">
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
{{ __('Usage Completed') }}
</span>
@if($log->order_id)
<a href="{{ route('admin.sales.orders.show', $log->order_id) }}" class="text-xs font-black text-cyan-500 hover:underline tracking-widest">
#{{ $log->order_id }}
</a>
@endif
@else
<span class="text-xs font-bold text-slate-500">
{{ __('Verified Only') }}
</span>
@endif
</div>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
{{ __('IC Card UID') }}
</p>
<p class="text-sm font-mono font-bold text-slate-700 dark:text-slate-300 truncate">
{{ $log->staffCard?->card_uid ?? '-' }}
</p>
</div>
</div>
</div>
@empty

View File

@ -14,7 +14,7 @@
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ $filters['search'] }}"
<input type="text" name="search" value="{{ $tab === 'dispense' ? $filters['search'] : '' }}"
class="py-2.5 pl-12 pr-6 block w-full luxury-input"
placeholder="{{ __('Search Product / Slot...') }}">
</div>

View File

@ -14,7 +14,7 @@
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ $filters['search'] }}"
<input type="text" name="search" value="{{ $tab === 'invoices' ? $filters['search'] : '' }}"
class="py-2.5 pl-12 pr-6 block w-full luxury-input"
placeholder="{{ __('Search Invoice No / Flow ID...') }}">
</div>

View File

@ -12,7 +12,7 @@
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ $filters['search'] }}"
<input type="text" name="search" value="{{ $tab === 'orders' ? $filters['search'] : '' }}"
class="py-2.5 pl-12 pr-6 block w-full luxury-input"
placeholder="{{ __('Search Order No / Flow ID / Invoice...') }}">
</div>

View File

@ -214,7 +214,7 @@
{{-- Tabs --}}
<x-tab-nav model="activeTab">
<x-tab-nav-item value="list" :label="__('Pass Codes')" model="activeTab" />
<x-tab-nav-item value="logs" :label="__('Operation Records')" model="activeTab" />
<x-tab-nav-item value="logs" :label="__('Usage Logs')" model="activeTab" />
</x-tab-nav>
{{-- Main Content --}}

View File

@ -103,13 +103,16 @@
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Action / Target') }}</th>
{{ __('Machine / Pass Code') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Action') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Pass Code') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Sales Record') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Time') }}</th>
@ -125,7 +128,7 @@
<td class="px-6 py-5">
<div class="flex items-center gap-x-4">
<div
class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-indigo-500 group-hover:text-white transition-all duration-300">
class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor"
viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
@ -134,40 +137,35 @@
</div>
<div class="flex flex-col">
<span
class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
{{ __('Pass Code') }}
class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">
{{ $log->machine->name ?? $log->machine->serial_no ?? '-' }}
</span>
<span
class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wide mt-0.5">
ID: {{ $log->target_id }}
{{ __('Code') }}: {{ $log->passCode->name ?? '-' }}
</span>
</div>
</div>
</td>
<td class="px-6 py-5 text-center whitespace-nowrap">
@php
$actionColor = match($log->action) {
'create' => 'cyan',
'cancel' => 'rose',
'update' => 'indigo',
'verify_success' => 'emerald',
default => 'slate'
};
@endphp
<span
class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-{{ $actionColor }}-500/10 text-{{ $actionColor }}-500 border border-{{ $actionColor }}-500/20 tracking-widest uppercase shadow-sm shadow-{{ $actionColor }}-500/5">
{{ __($log->action) }}
</span>
<x-status-badge :status="$log->action" size="sm" />
</td>
<td
class="px-6 py-5 text-center font-mono font-extrabold text-slate-700 dark:text-slate-200">
@if($log->new_values && isset($log->new_values['code']))
<span
class="px-2 py-1 rounded-lg bg-slate-100 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50 tracking-tighter">
{{ $log->new_values['code'] }}
{{ $log->passCode->code ?? '-' }}
</span>
</td>
<td class="px-6 py-5 text-center whitespace-nowrap">
@if($log->order_id)
<a href="{{ route('admin.sales.index', ['tab' => 'orders', 'search' => $log->order?->order_no ?? $log->order_id]) }}"
class="text-xs font-black text-cyan-600 dark:text-cyan-400 hover:underline flex items-center justify-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
{{ $log->order?->order_no ?? '#' . $log->order_id }}
</a>
@else
<span class="text-slate-300 dark:text-slate-700">-</span>
<span class="text-slate-300 dark:text-slate-700">-</span>
@endif
</td>
<td class="px-6 py-5 text-center whitespace-nowrap">
@ -178,20 +176,27 @@
</td>
<td class="px-6 py-5 text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-2">
<div
class="w-7 h-7 rounded-full bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center text-indigo-600 dark:text-indigo-400 text-[10px] font-black border border-indigo-200/50 dark:border-indigo-800/50">
{{ Str::substr($log->user->name ?? 'S', 0, 1) }}
</div>
<span
class="text-xs font-bold text-slate-600 dark:text-slate-300 uppercase tracking-widest">
{{ $log->user->name ?? __('System') }}
</span>
@if($log->user)
<div
class="w-7 h-7 rounded-full bg-cyan-100 dark:bg-cyan-900/50 flex items-center justify-center text-cyan-600 dark:text-cyan-400 text-[10px] font-black border border-cyan-200/50 dark:border-cyan-800/50">
{{ Str::substr($log->user->name, 0, 1) }}
</div>
<span
class="text-xs font-bold text-slate-600 dark:text-slate-300 uppercase tracking-widest">
{{ $log->user->name }}
</span>
@else
<span
class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
{{ __('Machine') }}
</span>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-6 py-20 text-center">
<td colspan="6" class="px-6 py-20 text-center">
<div class="flex flex-col items-center justify-center gap-3">
<div class="p-4 rounded-3xl bg-slate-50 dark:bg-slate-800/50">
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" fill="none"
@ -219,36 +224,24 @@
<div class="flex items-start justify-between gap-4 mb-5">
<div class="flex items-center gap-3 min-w-0">
<div
class="w-14 h-14 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-indigo-500 group-hover:text-white transition-all duration-300 shadow-sm shrink-0">
class="w-14 h-14 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 shrink-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</div>
<div class="min-w-0 flex-1">
<h3
class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-indigo-600 transition-colors tracking-tight">
{{ __('Pass Code') }}</h3>
class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 transition-colors tracking-tight">
{{ $log->machine->name ?? $log->machine->serial_no ?? '-' }}</h3>
<p
class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-widest truncate mt-0.5">
ID: {{ $log->target_id }}</p>
{{ __('Code') }}: {{ $log->passCode->name ?? '-' }}</p>
</div>
</div>
<div class="shrink-0">
@php
$actionColor = match($log->action) {
'create' => 'cyan',
'cancel' => 'rose',
'update' => 'indigo',
'verify_success' => 'emerald',
default => 'slate'
};
@endphp
<span
class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-{{ $actionColor }}-500/10 text-{{ $actionColor }}-500 border border-{{ $actionColor }}-500/20 uppercase tracking-widest shadow-sm">
{{ __($log->action) }}
</span>
<x-status-badge :status="$log->action" size="sm" />
</div>
</div>
@ -258,15 +251,27 @@
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{
__('Operator') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">{{
$log->user->name ?? __('System') }}</p>
$log->user->name ?? __('Machine') }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{
__('Pass Code') }}</p>
<p class="text-sm font-mono font-black text-slate-800 dark:text-slate-100">
{{ $log->new_values['code'] ?? '-' }}
{{ $log->passCode->code ?? '-' }}
</p>
</div>
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{
__('Sales Record') }}</p>
@if($log->order_id)
<a href="{{ route('admin.sales.index', ['tab' => 'orders', 'search' => $log->order?->order_no ?? $log->order_id]) }}"
class="text-sm font-black text-cyan-600 dark:text-cyan-400 hover:underline">
{{ $log->order?->order_no ?? '#' . $log->order_id }}
</a>
@else
<p class="text-sm font-bold text-slate-300 dark:text-slate-700">-</p>
@endif
</div>
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{
__('Time') }}</p>

View File

@ -54,7 +54,7 @@ transition-all duration-300",
{{-- Tab Navigation --}}
<x-tab-nav model="activeTab">
<x-tab-nav-item value="list" :label="__('Pickup Codes')" model="activeTab" />
<x-tab-nav-item value="logs" :label="__('Operation Records')" model="activeTab" />
<x-tab-nav-item value="logs" :label="__('Usage Logs')" model="activeTab" />
</x-tab-nav>
{{-- Tab Contents --}}

View File

@ -138,7 +138,16 @@
'expired' :
$code->status;
@endphp
<x-status-badge :status="$displayStatus" />
<div class="flex flex-col items-start gap-1">
<x-status-badge :status="$displayStatus" />
@if($code->status === 'used' && $code->order_id)
<a href="{{ route('admin.sales.index', ['tab' => 'orders', 'search' => $code->order?->order_no]) }}"
class="text-[10px] font-bold text-cyan-600 dark:text-cyan-400 hover:underline flex items-center gap-0.5">
<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.5" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
{{ $code->order?->order_no ?? '#' . $code->order_id }}
</a>
@endif
</div>
</td>
<td class="px-6 py-6 whitespace-nowrap text-right">
<div class="flex items-center justify-end gap-2">
@ -224,7 +233,16 @@
'expired' :
$code->status;
@endphp
<x-status-badge :status="$displayStatus" size="sm" />
<div class="flex flex-col items-end gap-1">
<x-status-badge :status="$displayStatus" size="sm" />
@if($code->status === 'used' && $code->order_id)
<a href="{{ route('admin.sales.index', ['tab' => 'orders', 'search' => $code->order?->order_no]) }}"
class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 hover:underline flex items-center gap-0.5">
<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.5" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
{{ $code->order?->order_no ?? '#' . $code->order_id }}
</a>
@endif
</div>
</div>
{{-- Info Grid --}}

View File

@ -1,4 +1,4 @@
{{-- Filter Form (Simplified to match Inventory style) --}}
{{-- Filter Form --}}
<form action="{{ route('admin.sales.pickup-codes') }}" method="GET"
class="flex flex-wrap items-center gap-3 sm:gap-4 mb-8" @submit.prevent="fetchTabData('logs')">
<input type="hidden" name="tab" value="logs">
@ -17,22 +17,6 @@
placeholder="{{ __('Search logs...') }}">
</div>
{{-- 類型篩選 --}}
<div class="w-full sm:w-64">
<x-searchable-select
name="action"
:selected="request('action')"
:placeholder="__('All Types')"
:hasSearch="false"
>
@foreach($actions as $key => $label)
<option value="{{ $key }}" {{ request('action') === $key ? 'selected' : '' }} data-title="{{ $label }}">
{{ $label }}
</option>
@endforeach
</x-searchable-select>
</div>
{{-- 日期範圍 --}}
<div class="relative group w-full sm:w-72 sm:flex-none"
x-data="{ fp: null }"
@ -78,11 +62,6 @@
</button>
<button type="button" @click="
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
$el.closest('form').querySelectorAll('select').forEach(s => {
s.value = ' ';
const instance = window.HSSelect.getInstance(s);
if (instance) instance.setValue(' ');
});
fetchTabData('logs', '{{ route('admin.sales.pickup-codes') }}?tab=logs');
"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95"
@ -103,13 +82,16 @@
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Action / Target') }}</th>
{{ __('Machine / Slot') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Action') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Pickup Code') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Sales Record') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Time') }}</th>
@ -129,45 +111,40 @@
<svg class="w-5 h-5" fill="none" stroke="currentColor"
viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<div class="flex flex-col">
<span
class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">
{{ __('Pickup Code') }}
{{ $log->machine->name ?? $log->machine->serial_no ?? '-' }}
</span>
<span
class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wide mt-0.5">
ID: {{ $log->target_id }}
{{ __('Slot') }}: {{ $log->raw_data['slot'] ?? ($log->pickupCode->slot_no ?? '-') }}
</span>
</div>
</div>
</td>
<td class="px-6 py-5 text-center whitespace-nowrap">
@php
$actionColor = match($log->action) {
'create' => 'cyan',
'cancel', 'consume_failed' => 'rose',
'used', 'consume' => 'emerald',
'verify_success' => 'indigo',
default => 'slate'
};
@endphp
<span
class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-{{ $actionColor }}-500/10 text-{{ $actionColor }}-500 border border-{{ $actionColor }}-500/20 tracking-widest uppercase shadow-sm shadow-{{ $actionColor }}-500/5">
{{ __($log->action) }}
</span>
<x-status-badge :status="$log->action" size="sm" />
</td>
<td
class="px-6 py-5 text-center font-mono font-extrabold text-slate-700 dark:text-slate-200">
@if($log->new_values && isset($log->new_values['code']))
<span
class="px-2 py-1 rounded-lg bg-slate-100 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50 tracking-tighter">
{{ $log->new_values['code'] }}
{{ $log->pickupCode->code ?? '-' }}
</span>
</td>
<td class="px-6 py-5 text-center whitespace-nowrap">
@if($log->order_id)
<a href="{{ route('admin.sales.index', ['tab' => 'orders', 'search' => $log->order?->order_no]) }}"
class="text-xs font-black text-cyan-600 dark:text-cyan-400 hover:underline flex items-center justify-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
{{ $log->order?->order_no ?? '#' . $log->order_id }}
</a>
@else
<span class="text-slate-300 dark:text-slate-700">-</span>
<span class="text-slate-300 dark:text-slate-700">-</span>
@endif
</td>
<td class="px-6 py-5 text-center whitespace-nowrap">
@ -178,30 +155,38 @@
</td>
<td class="px-6 py-5 text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-2">
<div
class="w-7 h-7 rounded-full bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center text-indigo-600 dark:text-indigo-400 text-[10px] font-black border border-indigo-200/50 dark:border-indigo-800/50">
{{ Str::substr($log->user->name ?? 'S', 0, 1) }}
</div>
<span
class="text-xs font-bold text-slate-600 dark:text-slate-300 uppercase tracking-widest">
{{ $log->user->name ?? __('System') }}
</span>
@if($log->user)
<div
class="w-7 h-7 rounded-full bg-cyan-100 dark:bg-cyan-900/50 flex items-center justify-center text-cyan-600 dark:text-cyan-400 text-[10px] font-black border border-cyan-200/50 dark:border-cyan-800/50">
{{ Str::substr($log->user->name, 0, 1) }}
</div>
<span
class="text-xs font-bold text-slate-600 dark:text-slate-300 uppercase tracking-widest">
{{ $log->user->name }}
</span>
@else
<span
class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
{{ __('Machine') }}
</span>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-6 py-20 text-center">
<td colspan="6" class="px-6 py-20 text-center">
<div class="flex flex-col items-center justify-center gap-3">
<div class="p-4 rounded-3xl bg-slate-50 dark:bg-slate-800/50">
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="1.5"
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<p class="text-slate-400 font-bold uppercase tracking-widest text-xs">{{ __('No records found') }}</p>
<p class="text-slate-400 font-bold uppercase tracking-widest text-xs">{{
__('No records found') }}</p>
</div>
</td>
</tr>
@ -222,32 +207,20 @@
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<div class="min-w-0 flex-1">
<h3
class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 transition-colors tracking-tight">
{{ __('Pickup Code') }}</h3>
{{ $log->machine->name ?? $log->machine->serial_no ?? '-' }}</h3>
<p
class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-widest truncate mt-0.5">
ID: {{ $log->target_id }}</p>
{{ __('Slot') }}: {{ $log->raw_data['slot'] ?? '-' }}</p>
</div>
</div>
<div class="shrink-0">
@php
$actionColor = match($log->action) {
'create' => 'cyan',
'cancel', 'consume_failed' => 'rose',
'used', 'consume' => 'emerald',
'verify_success' => 'indigo',
default => 'slate'
};
@endphp
<span
class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-{{ $actionColor }}-500/10 text-{{ $actionColor }}-500 border border-{{ $actionColor }}-500/20 uppercase tracking-widest shadow-sm">
{{ __($log->action) }}
</span>
<x-status-badge :status="$log->action" size="sm" />
</div>
</div>
@ -257,15 +230,27 @@
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{
__('Operator') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">{{
$log->user->name ?? __('System') }}</p>
$log->user->name ?? __('Machine') }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{
__('Pickup Code') }}</p>
<p class="text-sm font-mono font-black text-slate-800 dark:text-slate-100">
{{ $log->new_values['code'] ?? '-' }}
{{ $log->pickupCode->code ?? '-' }}
</p>
</div>
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{
__('Sales Record') }}</p>
@if($log->order_id)
<a href="{{ route('admin.sales.index', ['tab' => 'orders', 'search' => $log->order?->order_no]) }}"
class="text-sm font-black text-cyan-600 dark:text-cyan-400 hover:underline">
{{ $log->order?->order_no ?? '#' . $log->order_id }}
</a>
@else
<p class="text-sm font-bold text-slate-300 dark:text-slate-700">-</p>
@endif
</div>
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{
__('Time') }}</p>

View File

@ -56,6 +56,8 @@
'used' => ['label' => __('Used'), 'color' => 'indigo'],
'expired' => ['label' => __('Expired'), 'color' => 'rose'],
'cancelled'=> ['label' => __('Cancelled'),'color' => 'slate'],
'verify_success' => ['label' => __('Verified'), 'color' => 'emerald'],
'consume' => ['label' => __('Consumed'), 'color' => 'cyan'],
];
$preset = $presets[$status] ?? ['label' => $status, 'color' => 'slate'];

View File

@ -65,8 +65,9 @@ Route::prefix('v1')->middleware(['throttle:api'])->group(function () {
Route::post('machine/member/verify/B650', [App\Http\Controllers\Api\V1\App\MachineController::class, 'verifyMember']);
// 取貨與通行驗證
Route::match(['post', 'put'], 'machine/pickup/verify/B660', [App\Http\Controllers\Api\V1\App\MachineController::class, 'verifyPickupCode']);
Route::post('machine/pickup/verify/B660', [App\Http\Controllers\Api\V1\App\MachineController::class, 'verifyPickupCode']);
Route::post('machine/passcode/verify/B670', [App\Http\Controllers\Api\V1\App\MachineController::class, 'verifyPassCode']);
Route::post('machine/staff/verify/B680', [App\Http\Controllers\Api\V1\App\MachineController::class, 'verifyStaffCard']);
// 廣告與貨道清單 (B005, B009, B012)
Route::get('machine/ad/B005', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getAdvertisements']);

33
status.txt Normal file
View File

@ -0,0 +1,33 @@
M .agents/skills/api-technical-specs/SKILL.md
M app/Http/Controllers/Admin/SalesController.php
M app/Http/Controllers/Api/V1/App/MachineController.php
M app/Models/StaffCardLog.php
M app/Models/Transaction/Order.php
M app/Models/Transaction/PickupCode.php
M app/Services/Transaction/TransactionService.php
M config/api-docs.php
M lang/zh_TW.json
M resources/views/admin/data-config/staff-cards/partials/tab-usage-logs.blade.php
M resources/views/admin/sales/partials/tab-dispense.blade.php
M resources/views/admin/sales/partials/tab-invoices.blade.php
M resources/views/admin/sales/partials/tab-orders.blade.php
M resources/views/admin/sales/pass-codes/index.blade.php
M resources/views/admin/sales/pass-codes/partials/tab-logs.blade.php
M resources/views/admin/sales/pickup-codes/index.blade.php
M resources/views/admin/sales/pickup-codes/partials/tab-list.blade.php
M resources/views/admin/sales/pickup-codes/partials/tab-logs.blade.php
M resources/views/components/status-badge.blade.php
M routes/api.php
?? Star_Cloud_April_Report.pdf
?? Star_Cloud_April_Report.pptx
?? app/Models/Transaction/PassCodeLog.php
?? app/Models/Transaction/PickupCodeLog.php
?? database/migrations/2026_05_05_142715_add_closed_loop_fields_to_staff_card_logs_table.php
?? database/migrations/2026_05_05_143106_add_code_id_to_orders_table.php
?? database/migrations/2026_05_05_144000_add_closed_loop_to_codes_tables.php
?? database/migrations/2026_05_05_150000_create_pickup_code_logs_table.php
?? database/migrations/2026_05_05_160712_add_user_id_to_pass_code_logs_table.php
?? database/migrations/2026_05_05_163353_drop_status_from_codes_logs_tables.php
?? database/migrations/2026_05_05_164913_add_raw_data_to_pickup_code_logs_table.php
?? database/migrations/2026_05_05_170034_add_action_to_staff_card_logs_table.php
?? status.txt