[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:
parent
15b975efac
commit
fc93a0fb8c
@ -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
BIN
Star_Cloud_April_Report.pdf
Normal file
Binary file not shown.
BIN
Star_Cloud_April_Report.pptx
Normal file
BIN
Star_Cloud_April_Report.pptx
Normal file
Binary file not shown.
@ -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}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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'
|
||||
]
|
||||
]);
|
||||
|
||||
@ -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',
|
||||
];
|
||||
|
||||
|
||||
@ -31,6 +31,7 @@ class Order extends Model
|
||||
'payment_response',
|
||||
'payment_at',
|
||||
'member_barcode',
|
||||
'code_id',
|
||||
'invoice_info',
|
||||
'machine_time',
|
||||
'status',
|
||||
|
||||
47
app/Models/Transaction/PassCodeLog.php
Normal file
47
app/Models/Transaction/PassCodeLog.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判斷是否可用
|
||||
*/
|
||||
|
||||
48
app/Models/Transaction/PickupCodeLog.php
Normal file
48
app/Models/Transaction/PickupCodeLog.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@ -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。'
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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...": "備註內容(選填)..."
|
||||
}
|
||||
"待填寫": "待填寫"
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 --}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 --}}
|
||||
|
||||
@ -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 --}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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
33
status.txt
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user