From 15b975efac9c5f30a36c01f428b27576c82eb016 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 5 May 2026 14:06:43 +0800 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E6=96=B0=E5=A2=9E=E5=93=A1=E5=B7=A5?= =?UTF-8?q?=E5=8D=A1=E7=AE=A1=E7=90=86=E6=A8=A1=E7=B5=84=E8=88=87=20B012?= =?UTF-8?q?=20API=20=E6=96=87=E6=AA=94=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增員工卡 (staff_cards) 與員工卡感應日誌 (staff_card_logs) 模型與遷移檔。 2. 實作 StaffCardController 與相關管理介面,支援狀態切換與 CRUD。 3. 擴充 routes/web.php 加入員工卡管理路由。 4. 更新 api-docs.php 文檔,補全 B012 (Unified Sync) 的商品同步回應範例。 5. 實作 PaymentTypeSeeder 並整合至 DatabaseSeeder 以標準化支付類型。 6. 優化銷售中心訂單標籤與倉庫機台庫存顯示邏輯。 7. 側邊欄選單加入員工卡管理入口。 8. 重構 TransactionService 以支援員工卡工作階段 (session_token) 驗證。 --- .agents/rules/api-rules.md | 4 +- .agents/skills/api-technical-specs/SKILL.md | 3 +- .agents/skills/iot-communication/SKILL.md | 9 +- .../Controllers/Admin/RemoteController.php | 8 +- .../Controllers/Admin/SalesController.php | 20 +- .../Controllers/Admin/StaffCardController.php | 144 ++++++ .../Controllers/Admin/WarehouseController.php | 8 + app/Models/StaffCard.php | 45 ++ app/Models/StaffCardLog.php | 44 ++ app/Models/Transaction/Order.php | 59 +++ app/Models/Transaction/PaymentType.php | 3 +- .../Transaction/TransactionService.php | 7 +- config/api-docs.php | 28 +- ..._05_05_092136_create_staff_cards_table.php | 35 ++ ...05_092137_create_staff_card_logs_table.php | 31 ++ ...ake_company_id_nullable_in_staff_cards.php | 36 ++ database/seeders/PaymentTypeSeeder.php | 28 ++ lang/zh_TW.json | 47 +- .../basic-settings/machines/index.blade.php | 3 + .../data-config/staff-cards/index.blade.php | 424 ++++++++++++++++ .../partials/tab-staff-list.blade.php | 285 +++++++++++ .../partials/tab-usage-logs.blade.php | 180 +++++++ .../views/admin/products/index.blade.php | 457 ++++++++++-------- resources/views/admin/remote/index.blade.php | 4 +- .../remote/partials/tab-history.blade.php | 289 ++++++----- resources/views/admin/remote/stock.blade.php | 259 +++++----- .../partials/order-detail-panel.blade.php | 2 +- .../admin/sales/partials/tab-orders.blade.php | 2 +- .../warehouses/machine-inventory.blade.php | 228 ++------- .../partials/list-machines.blade.php | 175 +++++++ .../layouts/partials/sidebar-menu.blade.php | 6 +- routes/web.php | 3 +- 32 files changed, 2210 insertions(+), 666 deletions(-) create mode 100644 app/Http/Controllers/Admin/StaffCardController.php create mode 100644 app/Models/StaffCard.php create mode 100644 app/Models/StaffCardLog.php create mode 100644 database/migrations/2026_05_05_092136_create_staff_cards_table.php create mode 100644 database/migrations/2026_05_05_092137_create_staff_card_logs_table.php create mode 100644 database/migrations/2026_05_05_093300_make_company_id_nullable_in_staff_cards.php create mode 100644 database/seeders/PaymentTypeSeeder.php create mode 100644 resources/views/admin/data-config/staff-cards/index.blade.php create mode 100644 resources/views/admin/data-config/staff-cards/partials/tab-staff-list.blade.php create mode 100644 resources/views/admin/data-config/staff-cards/partials/tab-usage-logs.blade.php create mode 100644 resources/views/admin/warehouses/partials/list-machines.blade.php diff --git a/.agents/rules/api-rules.md b/.agents/rules/api-rules.md index 4a88290..c019077 100644 --- a/.agents/rules/api-rules.md +++ b/.agents/rules/api-rules.md @@ -72,8 +72,8 @@ trigger: always_on ### 5.1 MQTT 通訊端點 (高頻與事件驅動) 以下高頻或即時事件,未來將**全面改採 MQTT 協議**,透過 EMQX 與 Go Gateway 橋接: 1. **心跳事件**:機台發布至 `machine/{serial_no}/heartbeat`。 -2. **B013 (錯誤與狀態)**:機台發布至 `machine/{serial_no}/error`。 -3. **B600 / B602 (交易紀錄)**:機台發布至 `machine/{serial_no}/transaction`。 +2. **異常與狀態 (B013)**:機台發布至 `machine/{serial_no}/error`。 +3. **交易紀錄 (B600/B602)**:機台發布至 `machine/{serial_no}/transaction`,推薦使用 `action: finalize` 整合上報。 處理管線: `機台 ➜ EMQX ➜ Go Gateway ➜ Redis List (mqtt_incoming_jobs) ➜ Laravel daemon (mqtt:listen) ➜ Job 異步寫入 DB` diff --git a/.agents/skills/api-technical-specs/SKILL.md b/.agents/skills/api-technical-specs/SKILL.md index 062a9de..e05b282 100644 --- a/.agents/skills/api-technical-specs/SKILL.md +++ b/.agents/skills/api-technical-specs/SKILL.md @@ -329,7 +329,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 | `2` | 電子票證 | 包含悠遊卡、一卡通等 | | `3` | 掃碼支付 | 一般掃碼支付 | | `4` | 投幣機支付 | 實體紙鈔機 | -| `5` | 通關密碼 | | +| `5` | 通行碼 | 原「通關密碼」 | | `6` | 取貨碼 | | | `7` | 來店禮 | | | `8` | 問卷 | | @@ -341,6 +341,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 | `33` | TapPay - Pi 拍錢包 | | | `34` | TapPay - 全盈+PAY | | | `40` | 會員驗證取貨商品 | | +| `41` | 員工卡 | | | `50` ~ `54` | 線下付款 + TapPay | `50`=線下+30, `51`=線下+31... `54`=線下+34 | | `60` | 點數 / 優惠卷全額折抵 | 點數全額折抵出貨 或 優惠卷全額折抵出貨 | | `61` ~ `69` | 會員 + X | `61`=會員+1, `62`=會員+2... `69`=會員+9 | diff --git a/.agents/skills/iot-communication/SKILL.md b/.agents/skills/iot-communication/SKILL.md index f65504f..05f0af0 100644 --- a/.agents/skills/iot-communication/SKILL.md +++ b/.agents/skills/iot-communication/SKILL.md @@ -19,7 +19,7 @@ description: 規範智能販賣機與 Cloud 平台間的高頻通訊處理流程 4. **Model (儲存層)**:執行資料存取。 ### 1.2 MQTT 管線 (高頻/即時操作) -適用於 B010 (心跳), B013 (異常), B600/B601/B602 (交易生命週期) 等高頻或即時性端點: +適用於 `heartbeat` (心跳), `error` (異常), `transaction` (交易生命週期: `create`, `finalize` 等) 等高頻或即時性端點: 1. **Go Gateway (接收層)**:訂閱 EMQX Topic,提取 `serial_no`,包裝成標準 JSON。 2. **Redis List (橋接層)**:Go 執行 `RPUSH starcloud_database_mqtt_incoming_jobs {json}`。 3. **Laravel `mqtt:listen` (消費層)**:常駐指令 `BLPOP` 取出 JSON,根據 `type` 分派至對應 Job。 @@ -90,9 +90,10 @@ public function handle(MachineService $service): void ### 常見端點處理模式 1. **$SYS Events**:由 Broker 自動觸發,走 **MQTT 管線**。用於毫秒級更新機台的 `online/offline` 狀態與 `last_heartbeat_at`。 -2. **B010 (心跳)**:高頻點,走 **MQTT 管線** (`machine/+/heartbeat`)。更新 `last_heartbeat_at` 與感測器快照。 -3. **B013 (異常)**:事件驅動點,走 **MQTT 管線** (`machine/+/error`)。寫入 `machine_logs` 並觸發告警。 -4. **B600/B601/B602 (交易生命週期)**:高價值點,走 **MQTT 管線** (`machine/+/transaction`)。透過 `action` 欄位區分 `create` (交易建立) / `invoice` (發票) / `dispense` (出貨結果)。 +2. **heartbeat (心跳)**:原 B010。高頻點,走 **MQTT 管線** (`machine/+/heartbeat`)。更新 `last_heartbeat_at` 與感測器快照。 +3. **error (異常)**:原 B013。事件驅動點,走 **MQTT 管線** (`machine/+/error`)。寫入 `machine_logs` 並觸發告警。 +4. **transaction (交易生命週期)**:原 B600/B601/B602。高價值點,走 **MQTT 管線** (`machine/+/transaction`)。透過 `action` 欄位區分 `create` (建立) / `finalize` (完成統整) / `dispense` (出貨結果)。 + - **員工卡/通行碼**:應透過 `action: finalize` 且 `amount: 0` 並帶入對應 `payment_type` (如 41: 員工卡) 進行交易錄入。 5. **B012 (商品同步)**:大資料量,走 **HTTP 管線**。應確保 Service 層具備緩存 (Cache) 機制。 6. **B055 (遠端出貨)**:雲端下發指令,走 **MQTT 下行管線** (`machine/{id}/command`)。 diff --git a/app/Http/Controllers/Admin/RemoteController.php b/app/Http/Controllers/Admin/RemoteController.php index de49323..89fa1ce 100644 --- a/app/Http/Controllers/Admin/RemoteController.php +++ b/app/Http/Controllers/Admin/RemoteController.php @@ -30,15 +30,15 @@ class RemoteController extends Controller // --- 1. 機台列表處理 (New Command Tab) --- $machineQuery = Machine::withCount(['slots'])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc'); - + if ($request->filled('search') && $request->input('tab') === 'list') { $search = $request->input('search'); $machineQuery->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") - ->orWhere('serial_no', 'like', "%{$search}%"); + ->orWhere('serial_no', 'like', "%{$search}%"); }); } - + $machines = $machineQuery->paginate($request->input('per_page', 10), ['*'], 'machine_page'); // --- 2. 歷史紀錄處理 (Operation Records Tab) --- @@ -102,7 +102,7 @@ class RemoteController extends Controller if ($request->has('tab')) { $tab = $request->input('tab'); $viewPath = $tab === 'list' ? 'admin.remote.partials.tab-machines-index' : 'admin.remote.partials.tab-history-index'; - + return response()->json([ 'success' => true, 'html' => view($viewPath, [ diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index 817a939..b71fa39 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -47,19 +47,7 @@ class SalesController extends Controller 'end_date' => $endDate, ], 'machines' => Machine::select('id', 'name', 'serial_no')->get(), - 'paymentTypes' => [ - 1 => '信用卡', - 2 => '電子票證', - 3 => '掃碼支付', - 4 => '紙鈔機', - 9 => '零錢', - 30 => 'LINE Pay', - 31 => '街口支付', - 32 => '悠遊付', - 33 => 'Pi 拍錢包', - 34 => '全盈+PAY', - 60 => '點數/優惠券', - ], + 'paymentTypes' => Order::getPaymentTypeLabels(), ]; // 1. 建立基本查詢 (套用共用過濾器:機台、日期) @@ -144,11 +132,7 @@ class SalesController extends Controller 'success' => true, 'html' => view('admin.sales.partials.order-detail-panel', [ 'order' => $order, - 'paymentTypes' => [ - 1 => '信用卡', 2 => '電子票證', 3 => '掃碼支付', 4 => '紙鈔機', 9 => '零錢', - 30 => 'LINE Pay', 31 => '街口支付', 32 => '悠遊付', 33 => 'Pi 拍錢包', 34 => '全盈+PAY', - 60 => '點數/優惠券', - ] + 'paymentTypes' => Order::getPaymentTypeLabels() ])->render() ]); } diff --git a/app/Http/Controllers/Admin/StaffCardController.php b/app/Http/Controllers/Admin/StaffCardController.php new file mode 100644 index 0000000..5707fc1 --- /dev/null +++ b/app/Http/Controllers/Admin/StaffCardController.php @@ -0,0 +1,144 @@ +get('tab', 'staff_list'); + $isSystemAdmin = auth()->user()->isSystemAdmin(); + + $companies = []; + if ($isSystemAdmin) { + $companies = \App\Models\System\Company::active()->orderBy('name')->get(); + } + + $cardsQuery = StaffCard::with('lastMachine') + ->when($request->search, function ($q) use ($request) { + $q->where(function ($sq) use ($request) { + $sq->where('name', 'like', "%{$request->search}%") + ->orWhere('employee_id', 'like', "%{$request->search}%") + ->orWhere('card_uid', 'like', "%{$request->search}%"); + }); + }) + ->when($isSystemAdmin && $request->company_id, function ($q) use ($request) { + $q->where('company_id', $request->company_id); + }); + + $logsQuery = StaffCardLog::with(['staffCard', 'machine']) + ->when($request->log_search, function ($q) use ($request) { + $q->whereHas('staffCard', function ($sq) use ($request) { + $sq->where('name', 'like', "%{$request->log_search}%") + ->orWhere('employee_id', 'like', "%{$request->log_search}%") + ->orWhere('card_uid', 'like', "%{$request->log_search}%"); + }); + }) + ->when($isSystemAdmin && $request->log_company_id, function ($q) use ($request) { + $q->where('company_id', $request->log_company_id); + }); + + $staffPerPage = $request->input('staff_per_page', 10); + $usagePerPage = $request->input('usage_per_page', 10); + + if ($request->ajax() && $request->has('_ajax')) { + if ($tab === 'staff_list') { + $cards = $cardsQuery->latest()->paginate($staffPerPage, ['*'], 'staff_page'); + return response()->json([ + 'success' => true, + 'html' => view('admin.data-config.staff-cards.partials.tab-staff-list', compact('cards', 'companies'))->render() + ]); + } else { + $logs = $logsQuery->latest()->paginate($usagePerPage, ['*'], 'usage_page'); + return response()->json([ + 'success' => true, + 'html' => view('admin.data-config.staff-cards.partials.tab-usage-logs', compact('logs', 'companies'))->render() + ]); + } + } + + $cards = $cardsQuery->latest()->paginate($staffPerPage, ['*'], 'staff_page'); + $logs = $logsQuery->latest()->paginate($usagePerPage, ['*'], 'usage_page'); + + + return view('admin.data-config.staff-cards.index', compact('cards', 'logs', 'tab', 'companies')); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'company_id' => 'nullable|exists:companies,id', + 'employee_id' => 'required|string|max:50', + 'name' => 'required|string|max:100', + 'card_uid' => 'required|string|max:100', + 'status' => 'nullable|string|in:active,inactive', + 'notes' => 'nullable|string', + ]); + + $validated['status'] = $request->get('status', 'active'); + + $card = StaffCard::create($validated); + + if ($request->ajax()) { + return response()->json([ + 'success' => true, + 'message' => __('Staff card created successfully'), + 'data' => $card + ]); + } + + return redirect()->route('admin.data-config.staff-cards.index')->with('success', __('Staff card created successfully')); + } + + public function update(Request $request, StaffCard $staffCard) + { + $validated = $request->validate([ + 'company_id' => 'nullable|exists:companies,id', + 'employee_id' => 'required|string|max:50', + 'name' => 'required|string|max:100', + 'card_uid' => 'required|string|max:100', + 'status' => 'nullable|string|in:active,inactive', + 'notes' => 'nullable|string', + ]); + + $validated['status'] = $request->get('status', $staffCard->status); + + $staffCard->update($validated); + + if ($request->ajax()) { + return response()->json([ + 'success' => true, + 'message' => __('Staff card updated successfully'), + ]); + } + + return redirect()->route('admin.data-config.staff-cards.index')->with('success', __('Staff card updated successfully')); + } + + public function destroy(StaffCard $staffCard) + { + $staffCard->delete(); + + return response()->json([ + 'success' => true, + 'message' => __('Staff card deleted successfully'), + ]); + } + + public function toggleStatus(StaffCard $staffCard) + { + $staffCard->status = $staffCard->status === 'active' ? 'inactive' : 'active'; + $staffCard->save(); + + return response()->json([ + 'success' => true, + 'message' => __('Status updated successfully'), + ]); + } +} diff --git a/app/Http/Controllers/Admin/WarehouseController.php b/app/Http/Controllers/Admin/WarehouseController.php index 15b8007..e7df2c4 100644 --- a/app/Http/Controllers/Admin/WarehouseController.php +++ b/app/Http/Controllers/Admin/WarehouseController.php @@ -586,9 +586,17 @@ class WarehouseController extends Controller ->paginate($request->input('per_page', 10)) ->withQueryString(); + if ($request->ajax()) { + return response()->json([ + 'success' => true, + 'html' => view('admin.warehouses.partials.list-machines', compact('machines'))->render() + ]); + } + return view('admin.warehouses.machine-inventory', compact('machines')); } + /** * AJAX:取得單台機台貨道詳情 */ diff --git a/app/Models/StaffCard.php b/app/Models/StaffCard.php new file mode 100644 index 0000000..5b72304 --- /dev/null +++ b/app/Models/StaffCard.php @@ -0,0 +1,45 @@ + 'datetime', + ]; + + /** + * 取得此卡片的刷卡紀錄 + */ + public function logs(): HasMany + { + return $this->hasMany(StaffCardLog::class); + } + + /** + * 取得最後一次使用的機台 + */ + public function lastMachine(): BelongsTo + { + return $this->belongsTo(Machine::class, 'last_machine_id'); + } +} diff --git a/app/Models/StaffCardLog.php b/app/Models/StaffCardLog.php new file mode 100644 index 0000000..cc0b490 --- /dev/null +++ b/app/Models/StaffCardLog.php @@ -0,0 +1,44 @@ + 'array', + 'created_at' => 'datetime', + ]; + + /** + * 取得關聯的員工卡片 + */ + public function staffCard(): BelongsTo + { + return $this->belongsTo(StaffCard::class); + } + + /** + * 取得刷卡的機台 + */ + public function machine(): BelongsTo + { + return $this->belongsTo(Machine::class); + } +} diff --git a/app/Models/Transaction/Order.php b/app/Models/Transaction/Order.php index 94c002a..646fcba 100644 --- a/app/Models/Transaction/Order.php +++ b/app/Models/Transaction/Order.php @@ -46,8 +46,67 @@ class Order extends Model 'payment_at' => 'datetime', 'machine_time' => 'datetime', 'metadata' => 'array', + 'payment_type' => 'integer', ]; + /** + * 取得所有支付類型的對照標籤 + */ + public static function getPaymentTypeLabels(): array + { + return [ + 1 => __('Credit Card'), + 2 => __('E-Ticket (EasyCard/iPass)'), + 3 => __('QR Code Payment'), + 4 => __('Bill Acceptor'), + 5 => __('Pass Code'), // 使用者要求由「通關密碼」改為「通行碼」 + 6 => __('Pickup Code'), + 7 => __('Welcome Gift'), + 8 => __('Questionnaire'), + 9 => __('Coin Acceptor'), + 21 => __('Offline + 1'), + 22 => __('Offline + 2'), + 23 => __('Offline + 3'), + 24 => __('Offline + 4'), + 25 => __('Offline + 9'), + 30 => 'LINE Pay', + 31 => __('JKO Pay'), + 32 => __('Easy Wallet'), + 33 => 'Pi Pay', + 34 => 'PlusPay', + 40 => __('Member Verify Pickup'), + 41 => __('Staff Card'), + 50 => __('Offline + LINE Pay'), + 51 => __('Offline + JKO Pay'), + 52 => __('Offline + Easy Wallet'), + 53 => __('Offline + Pi Pay'), + 54 => __('Offline + PlusPay'), + 60 => __('Points/Voucher'), + 61 => __('Member + 1'), + 62 => __('Member + 2'), + 63 => __('Member + 3'), + 64 => __('Member + 4'), + 65 => __('Member + 5'), + 66 => __('Member + 6'), + 67 => __('Member + 7'), + 68 => __('Member + 8'), + 69 => __('Member + 9'), + 90 => __('Member + LINE Pay'), + 91 => __('Member + JKO Pay'), + 92 => __('Member + Easy Wallet'), + 93 => __('Member + Pi Pay'), + 94 => __('Member + PlusPay'), + ]; + } + + /** + * 取得目前的支付類型標籤 (Accessor) + */ + public function getPaymentTypeLabelAttribute(): string + { + return self::getPaymentTypeLabels()[$this->payment_type] ?? __('Unknown'); + } + public function machine() { return $this->belongsTo(Machine::class); diff --git a/app/Models/Transaction/PaymentType.php b/app/Models/Transaction/PaymentType.php index 71b5615..26789c4 100644 --- a/app/Models/Transaction/PaymentType.php +++ b/app/Models/Transaction/PaymentType.php @@ -4,11 +4,10 @@ namespace App\Models\Transaction; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\SoftDeletes; class PaymentType extends Model { - use HasFactory, SoftDeletes; + use HasFactory; protected $fillable = [ 'name', diff --git a/app/Services/Transaction/TransactionService.php b/app/Services/Transaction/TransactionService.php index a79818e..1c25787 100644 --- a/app/Services/Transaction/TransactionService.php +++ b/app/Services/Transaction/TransactionService.php @@ -44,7 +44,7 @@ class TransactionService 'change_amount' => $data['change_amount'] ?? 0, 'points_used' => $data['points_used'] ?? 0, 'original_amount' => $data['original_amount'] ?? $data['total_amount'], - 'payment_type' => $data['payment_type'] ?? 0, + 'payment_type' => (int)($data['payment_type'] ?? 0), 'payment_status' => $data['payment_status'] ?? 1, 'payment_request' => $data['payment_request'] ?? null, 'payment_response' => $data['payment_response'] ?? null, @@ -181,6 +181,11 @@ class TransactionService // 1. Process Order (B600) $orderData = $data['order']; $orderData['serial_no'] = $serialNo; + + // 確保提取 payment_type (可能在 root 或 order 內) + if (!isset($orderData['payment_type']) && isset($data['payment_type'])) { + $orderData['payment_type'] = $data['payment_type']; + } $order = $this->processTransaction($orderData); // 2. Record Invoice (B601) - Optional diff --git a/config/api-docs.php b/config/api-docs.php index fbf737f..1ef3a27 100644 --- a/config/api-docs.php +++ b/config/api-docs.php @@ -253,7 +253,7 @@ return [ ], 'data' => [ 'type' => 'array', - 'description' => '商品明細物件陣列', + 'description' => '商品明細物件陣列。欄位包含:t060v00(ID), t060v01(名稱), t060v03(規格), t060v06(圖片), t060v09(售價), t060v11(預設上限), spring_limit(彈簧上限), track_limit(履帶上限)', 'example' => [ [ 't060v00' => '1', @@ -267,7 +267,7 @@ return [ 't060v30' => 20.0, 't063v03' => 25.0, 't060v40' => 'Buy 1 Get 1', - 't060v41' => 'SKU-001', + 't060v41' => 'SKU-COKE-001', 'spring_limit' => 10, 'track_limit' => 15 ] @@ -275,6 +275,28 @@ return [ ] ], 'request' => [], + 'response' => [ + 'success' => true, + 'code' => 200, + 'data' => [ + [ + 't060v00' => '1', + 't060v01' => '可口可樂 330ml', + 't060v01_en' => 'Coca Cola 330ml', + 't060v01_jp' => 'コカコーラ 330ml', + 't060v03' => '經典原味,冰涼好滋味', + 't060v06' => 'https://cloud.star.com/storage/products/coke.png', + 't060v09' => 25.0, + 't060v11' => 10, + 't060v30' => 20.0, + 't063v03' => 25.0, + 't060v40' => '買一送一活動中', + 't060v41' => 'SKU-COKE-001', + 'spring_limit' => 10, + 'track_limit' => 15 + ] + ] + ], ], [ 'name' => 'B650: 會員身分驗證 (Member Verification)', @@ -510,7 +532,7 @@ return [ 'order.original_amount' => ['type' => 'numeric', 'description' => '(選填) 原始標價金額', 'required' => false], 'order.discount_amount' => ['type' => 'numeric', 'description' => '(選填) 折扣金額,預設 0', 'required' => false], 'order.points_used' => ['type' => 'integer', 'description' => '(選填) 使用點數,預設 0', 'required' => false], - 'order.payment_type' => ['type' => 'integer', 'description' => "(必填) 支付類型代碼:\n1:信用卡, 2:悠遊卡/一卡通, 3:掃碼支付, 4:紙鈔機, 5:通關密碼, 6:取貨碼, 7:來店禮, 8:問卷, 9:零錢。\n21~25:線下付款+X (21:線下+1, 22:線下+2, 23:線下+3, 24:線下+4, 25:線下+9)。\n30:TapPay-Line, 31:TapPay-街口, 32:TapPay-悠遊付, 33:TapPay-Pi, 34:TapPay-全盈+。\n40:會員驗證取貨商品。\n50~54:線下付款+TapPay (50:線下+30, 51:線下+31, 52:線下+32, 53:線下+33, 54:線下+34)。\n60:點數/優惠卷全額折抵。61~69:會員+X (61:會員+1...69:會員+9)。\n90~94:會員+TapPay (90:會員+30...94:會員+34)。"], + 'order.payment_type' => ['type' => 'integer', 'description' => "(必填) 支付類型代碼:\n1:信用卡, 2:悠遊卡/一卡通, 3:掃碼支付, 4:紙鈔機, 5:通行碼, 6:取貨碼, 7:來店禮, 8:問卷, 9:零錢, 41:員工卡。\n21~25:線下付款+X (21:線下+1, 22:線下+2, 23:線下+3, 24:線下+4, 25:線下+9)。\n30:LINE Pay, 31:街口, 32:悠遊付, 33:Pi, 34:全盈+。\n40:會員驗證取貨商品。\n50~54:線下付款+TapPay (50:線下+30, 51:線下+31, 52:線下+32, 53:線下+33, 54:線下+34)。\n60:點數/優惠卷全額折抵。61~69:會員+X (61:會員+1...69:會員+9)。\n90~94:會員+TapPay (90:會員+30...94:會員+34)。"], 'order.payment_status' => ['type' => 'integer', 'description' => '(必填) 支付狀態 (1:成功)'], 'order.payment_request' => ['type' => 'string', 'description' => '(選填) 金流請求原始字串', 'required' => false], 'order.payment_response' => ['type' => 'string', 'description' => '(選填) 金流回應原始字串', 'required' => false], diff --git a/database/migrations/2026_05_05_092136_create_staff_cards_table.php b/database/migrations/2026_05_05_092136_create_staff_cards_table.php new file mode 100644 index 0000000..9b0cbd6 --- /dev/null +++ b/database/migrations/2026_05_05_092136_create_staff_cards_table.php @@ -0,0 +1,35 @@ +id(); + $table->unsignedBigInteger('company_id')->index()->comment('租戶ID'); + $table->string('employee_id')->index()->comment('員工編號'); + $table->string('name')->comment('員工姓名'); + $table->string('card_uid')->index()->comment('IC卡物理卡號'); + $table->string('status')->default('active')->comment('狀態: active/inactive'); + $table->text('notes')->nullable()->comment('備註'); + $table->timestamp('last_used_at')->nullable()->comment('最後使用時間'); + $table->unsignedBigInteger('last_machine_id')->nullable()->comment('最後使用機台'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('staff_cards'); + } +}; diff --git a/database/migrations/2026_05_05_092137_create_staff_card_logs_table.php b/database/migrations/2026_05_05_092137_create_staff_card_logs_table.php new file mode 100644 index 0000000..7a3ee1b --- /dev/null +++ b/database/migrations/2026_05_05_092137_create_staff_card_logs_table.php @@ -0,0 +1,31 @@ +id(); + $table->unsignedBigInteger('staff_card_id')->index()->comment('員工卡片ID'); + $table->unsignedBigInteger('machine_id')->index()->nullable()->comment('機台ID'); + $table->string('action_type')->default('swipe')->comment('動作類型'); + $table->json('raw_data')->nullable()->comment('原始API資料'); + $table->timestamp('created_at')->useCurrent()->comment('發生時間'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('staff_card_logs'); + } +}; diff --git a/database/migrations/2026_05_05_093300_make_company_id_nullable_in_staff_cards.php b/database/migrations/2026_05_05_093300_make_company_id_nullable_in_staff_cards.php new file mode 100644 index 0000000..c91608b --- /dev/null +++ b/database/migrations/2026_05_05_093300_make_company_id_nullable_in_staff_cards.php @@ -0,0 +1,36 @@ +unsignedBigInteger('company_id')->nullable()->change(); + }); + + Schema::table('staff_card_logs', function (Blueprint $table) { + $table->unsignedBigInteger('company_id')->nullable()->index()->after('id')->comment('租戶ID'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('staff_card_logs', function (Blueprint $table) { + $table->dropColumn('company_id'); + }); + + Schema::table('staff_cards', function (Blueprint $table) { + $table->unsignedBigInteger('company_id')->nullable(false)->change(); + }); + } +}; diff --git a/database/seeders/PaymentTypeSeeder.php b/database/seeders/PaymentTypeSeeder.php new file mode 100644 index 0000000..93bfa5a --- /dev/null +++ b/database/seeders/PaymentTypeSeeder.php @@ -0,0 +1,28 @@ + $name) { + PaymentType::updateOrCreate( + ['code' => $code], + [ + 'name' => $name, + 'is_active' => true, + ] + ); + } + } +} diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 070673e..8d7477d 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -171,6 +171,30 @@ "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": "基本資訊", @@ -1821,5 +1845,24 @@ "visit_gift": "來店禮", "vs Yesterday": "較昨日", "warehouses": "倉庫管理", - "待填寫": "待填寫" -} \ No newline at end of file + "待填寫": "待填寫", + "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...": "備註內容(選填)..." +} diff --git a/resources/views/admin/basic-settings/machines/index.blade.php b/resources/views/admin/basic-settings/machines/index.blade.php index 8a9deaa..eee816c 100644 --- a/resources/views/admin/basic-settings/machines/index.blade.php +++ b/resources/views/admin/basic-settings/machines/index.blade.php @@ -348,6 +348,8 @@ }, init() { // 觸發頂部進度條 + // Sync top loading bar (Removed for tab/pagination to reduce visual noise as requested) + /* this.$watch('tabLoading', (val) => { const bar = document.getElementById('top-loading-bar'); if (bar) { @@ -355,6 +357,7 @@ else bar.classList.remove('loading'); } }); + */ // 首次載入時綁定每個 Tab 的分頁連結 this.$nextTick(() => { ['machines', 'models', 'permissions', 'system_settings'].forEach(t => { diff --git a/resources/views/admin/data-config/staff-cards/index.blade.php b/resources/views/admin/data-config/staff-cards/index.blade.php new file mode 100644 index 0000000..2401b26 --- /dev/null +++ b/resources/views/admin/data-config/staff-cards/index.blade.php @@ -0,0 +1,424 @@ +@extends('layouts.admin') + +@section('content') +
+ + {{-- Page Header --}} + + + + + {{-- Tab Navigation --}} + + + + + + {{-- Tab Contents --}} + + {{-- Staff List Tab --}} +
+ + {{-- Loading Overlay --}} + + + + + + +
+ @include('admin.data-config.staff-cards.partials.tab-staff-list') +
+
+ + {{-- Usage Logs Tab --}} +
+ + {{-- Loading Overlay --}} + + + + + + +
+ @include('admin.data-config.staff-cards.partials.tab-usage-logs') +
+
+ + {{-- Modals & Slide-overs --}} + +
+
+
+ +
+
+

+

+ +
+ +
+
+ @if(auth()->user()->isSystemAdmin()) +
+ + +
+ @endif + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+ + {{-- Delete Confirm Modal --}} + + + {{-- Status Toggle Modal --}} + + +
+@endsection + +@section('scripts') + +@endsection \ No newline at end of file diff --git a/resources/views/admin/data-config/staff-cards/partials/tab-staff-list.blade.php b/resources/views/admin/data-config/staff-cards/partials/tab-staff-list.blade.php new file mode 100644 index 0000000..33cca9a --- /dev/null +++ b/resources/views/admin/data-config/staff-cards/partials/tab-staff-list.blade.php @@ -0,0 +1,285 @@ +
+ +
+
+
+ + +
+ + + + + + + +
+ + @if(auth()->user()->isSystemAdmin()) +
+ +
+ @endif + +
+ + +
+ + +
+
+
+ + {{-- Data Table --}} + + + +
+ @forelse($cards as $card) +
+ +
+
+
+ + + +
+
+

+ {{ $card->name }} +

+

+ {{ $card->employee_id }} +

+
+
+ + +
+ + +
+
+

+ {{ __('IC Card UID') }} +

+

+ {{ $card->card_uid }} +

+
+ @if($card->notes) +
+

+ {{ __('Notes') }} +

+

+ {{ $card->notes }} +

+
+ @endif +
+

+ {{ __('Last Used') }} +

+

+ @if($card->last_used_at) + {{ $card->last_used_at->format('Y-m-d H:i') }} + @if($card->lastMachine) + ({{ $card->lastMachine->name }}) + @endif + @else + {{ __('Never Used') }} + @endif +

+
+
+ + +
+
+ @if($card->status === 'active') + + @else + + @endif + +
+ + +
+
+ @empty + + @endforelse +
+ + {{-- Pagination --}} +
+ {{ $cards->appends(request()->query())->links('vendor.pagination.luxury', [ + 'per_page_param' => 'staff_per_page', + 'page_param' => 'staff_page', + ]) }} +
+
diff --git a/resources/views/admin/data-config/staff-cards/partials/tab-usage-logs.blade.php b/resources/views/admin/data-config/staff-cards/partials/tab-usage-logs.blade.php new file mode 100644 index 0000000..d7d360f --- /dev/null +++ b/resources/views/admin/data-config/staff-cards/partials/tab-usage-logs.blade.php @@ -0,0 +1,180 @@ +
+ +
+
+
+ + +
+ + + + + + + +
+ + @if(auth()->user()->isSystemAdmin()) +
+ +
+ @endif + +
+ + +
+ + +
+
+
+ + {{-- Data Table --}} + + + +
+ @forelse($logs as $log) +
+ +
+
+
+ + + +
+
+

+ {{ $log->created_at->format('Y-m-d H:i:s') }} +

+

+ {{ $log->staffCard?->name ?? __('Unknown') }} +

+
+
+ + +
+ + +
+
+

+ {{ __('Machine') }} +

+
+ + {{ $log->machine?->name ?? __('Unknown Machine') }} + + ({{ $log->machine?->serial_no ?? '-' }}) +
+
+
+

+ {{ __('IC Card UID') }} +

+

+ {{ $log->staffCard?->card_uid ?? '-' }} +

+
+
+
+ @empty + + @endforelse +
+ + {{-- Pagination --}} +
+ {{ $logs->appends(request()->query())->links('vendor.pagination.luxury', [ + 'per_page_param' => 'usage_per_page', + 'page_param' => 'usage_page', + ]) }} +
+
diff --git a/resources/views/admin/products/index.blade.php b/resources/views/admin/products/index.blade.php index b4ce8ea..6c6a0f4 100644 --- a/resources/views/admin/products/index.blade.php +++ b/resources/views/admin/products/index.blade.php @@ -5,31 +5,28 @@ $routeName = request()->route()->getName(); $baseRoute = 'admin.data-config.products'; $roleSelectConfig = [ - "placeholder" => __('Select Category'), - "hasSearch" => true, - "searchPlaceholder" => __('Search Category...'), - "toggleClasses" => "hs-select-toggle luxury-select-toggle", - "dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-2xl mt-2 z-[100]", - "optionClasses" => "hs-select-option py-2.5 px-3 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-white/5 rounded-lg flex items-center justify-between", +"placeholder" => __('Select Category'), +"hasSearch" => true, +"searchPlaceholder" => __('Search Category...'), +"toggleClasses" => "hs-select-toggle luxury-select-toggle", +"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 +rounded-xl shadow-2xl mt-2 z-[100]", +"optionClasses" => "hs-select-option py-2.5 px-3 text-sm text-slate-800 dark:text-slate-300 cursor-pointer +hover:bg-slate-100 dark:hover:bg-white/5 rounded-lg flex items-center justify-between", ]; @endphp @section('content') -
- +
+ {{-- Page Header --}} - +