From fa6406b9ad1a527414d984a4588a7e6fb81abe6b Mon Sep 17 00:00:00 2001 From: sky121113 Date: Wed, 6 May 2026 16:33:15 +0800 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E5=84=AA=E5=8C=96=E5=8F=96=E8=B2=A8?= =?UTF-8?q?=E7=A2=BC=E7=AE=A1=E7=90=86=E4=BB=8B=E9=9D=A2=E8=88=87=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增取貨碼使用次數上限功能(限制 1-20 次)。 2. 優化取貨碼管理介面,採用極簡奢華風計數器並調整寬度與間距。 3. 整合 Flatpickr 24小時制日期時間選擇器,並實作模式切換時的時間自動帶入邏輯。 4. 取貨碼清單新增「剩餘使用次數 / 總次數」顯示。 5. 完善相關中文化翻譯(如「小時」、「自定義日期」等)。 6. 新增資料庫欄位 migration 並更新相關模型與 Service 邏輯。 7. 更新 API 技術規格文檔以符合最新取貨碼邏輯。 --- .agents/skills/api-technical-specs/SKILL.md | 2 +- .../Controllers/Admin/SalesController.php | 22 +- .../Api/V1/App/MachineController.php | 2 +- app/Models/Transaction/PickupCode.php | 7 +- .../Transaction/TransactionService.php | 16 +- config/api-docs.php | 32 +- ..._add_usage_limit_to_pickup_codes_table.php | 29 ++ lang/en.json | 6 + lang/zh_TW.json | 6 + .../admin/sales/pickup-codes/index.blade.php | 282 +++++++++++++++--- .../pickup-codes/partials/tab-list.blade.php | 36 ++- 11 files changed, 367 insertions(+), 73 deletions(-) create mode 100644 database/migrations/2026_05_06_160342_add_usage_limit_to_pickup_codes_table.php diff --git a/.agents/skills/api-technical-specs/SKILL.md b/.agents/skills/api-technical-specs/SKILL.md index 24e0de7..8f045c0 100644 --- a/.agents/skills/api-technical-specs/SKILL.md +++ b/.agents/skills/api-technical-specs/SKILL.md @@ -162,7 +162,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 | :--- | :--- | :--- | :--- | :--- | | machine | String | 是 | 機台編號 (serial_no) | M-001 | -- **Response Body (Success 200):** +- **Response Body (Success 200 - Object):** | 欄位 (Key) | 說明 | 備註 | | :--- | :--- | :--- | diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index 77bd470..f15802d 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -239,17 +239,25 @@ class SalesController extends Controller $validated = $request->validate([ 'machine_id' => 'required|exists:machines,id', 'slot_no' => 'required|string', - 'expires_hours' => 'nullable|integer|min:1|max:720', // 最長一個月 + 'usage_limit' => 'nullable|integer|min:1|max:20', + 'expires_hours' => 'nullable|integer|min:1|max:720', + 'expires_at' => 'nullable|date|after:now', 'custom_code' => 'nullable|string|min:4|max:12', ]); $machine = Machine::findOrFail($validated['machine_id']); - $expiresAt = now()->addHours((int) ($request->expires_hours ?? 24)); + + // 處理過期時間:優先使用直接傳入的日期,否則使用時數計算 + $expiresAt = $request->filled('expires_at') + ? Carbon::parse($validated['expires_at']) + : now()->addHours((int) ($request->expires_hours ?? 24)); $pickupCode = PickupCode::create([ 'machine_id' => $validated['machine_id'], 'slot_no' => $validated['slot_no'], 'code' => $request->custom_code ?? PickupCode::generateUniqueCode($validated['machine_id']), + 'usage_limit' => $validated['usage_limit'] ?? 1, + 'usage_count' => 0, 'expires_at' => $expiresAt, 'status' => 'active', 'company_id' => $machine->company_id, @@ -275,14 +283,16 @@ class SalesController extends Controller public function updatePickupCode(Request $request, PickupCode $pickupCode) { $validated = $request->validate([ - 'expires_at' => 'required|date|after:now', + 'expires_at' => 'nullable|date|after:now', + 'usage_limit' => 'nullable|integer|min:1|max:20', ]); $oldValues = $pickupCode->toArray(); - $pickupCode->update([ - 'expires_at' => $validated['expires_at'], - ]); + $pickupCode->update(array_filter([ + 'expires_at' => $validated['expires_at'] ?? null, + 'usage_limit' => $validated['usage_limit'] ?? null, + ])); SystemOperationLog::create([ 'company_id' => $pickupCode->company_id, diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index 2fee5b2..1997dae 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -498,7 +498,7 @@ class MachineController extends Controller return response()->json([ 'success' => true, 'code' => 200, - 'data' => [$data] // App 預期的是包含單一物件的陣列 + 'data' => $data ]); } diff --git a/app/Models/Transaction/PickupCode.php b/app/Models/Transaction/PickupCode.php index 4efdb15..f8b3bc4 100644 --- a/app/Models/Transaction/PickupCode.php +++ b/app/Models/Transaction/PickupCode.php @@ -20,7 +20,8 @@ class PickupCode extends Model 'slug', 'expires_at', 'used_at', - 'status', + 'usage_limit', + 'usage_count', 'created_by', 'order_id', ]; @@ -77,7 +78,9 @@ class PickupCode extends Model */ public function isValid(): bool { - return $this->status === 'active' && $this->expires_at->isFuture(); + return $this->status === 'active' + && ($this->usage_count < $this->usage_limit) + && ($this->expires_at->isFuture()); } /** diff --git a/app/Services/Transaction/TransactionService.php b/app/Services/Transaction/TransactionService.php index 863e7c2..8772d89 100644 --- a/app/Services/Transaction/TransactionService.php +++ b/app/Services/Transaction/TransactionService.php @@ -247,10 +247,14 @@ class TransactionService ->where('status', 'active') ->first(); if ($pickupCode) { + $newCount = $pickupCode->usage_count + 1; + $isUsedUp = $newCount >= $pickupCode->usage_limit; + $pickupCode->update([ 'order_id' => $order->id, - 'status' => 'used', - 'used_at' => now() + 'usage_count' => $newCount, + 'status' => $isUsedUp ? 'used' : 'active', + 'used_at' => $isUsedUp ? now() : $pickupCode->used_at ]); // 建立核銷日誌 (閉環最後一哩路) @@ -259,11 +263,13 @@ class TransactionService 'machine_id' => $order->machine_id, 'pickup_code_id' => $pickupCode->id, 'order_id' => $order->id, - 'action' => 'consume', - 'remark' => "MQTT 交易完成自動核銷,訂單: #{$order->id}", + 'action' => $isUsedUp ? 'used' : 'consume', + 'remark' => "MQTT 交易完成自動核銷 (" . ($newCount) . "/" . $pickupCode->usage_limit . "),訂單: #{$order->id}", 'raw_data' => [ 'slot' => $pickupCode->slot_no, - 'code' => $pickupCode->code + 'code' => $pickupCode->code, + 'count' => $newCount, + 'limit' => $pickupCode->usage_limit ] ]); } diff --git a/config/api-docs.php b/config/api-docs.php index ac4dbfe..cf6bc33 100644 --- a/config/api-docs.php +++ b/config/api-docs.php @@ -97,16 +97,14 @@ return [ 'example' => true ], 'data' => [ - 'type' => 'array', - 'description' => '配置物件陣列。包含:t050v01 (序號), api_token (通訊 Token), t050v41~43 (玉山設定), t050v34~38 (發票設定), TP_... (趨勢/手機支付設定)', + 'type' => 'object', + 'description' => '配置物件。包含:t050v01 (序號), api_token (通訊 Token), t050v41~43 (玉山設定), t050v34~38 (發票設定), TP_... (趨勢/手機支付設定)', 'example' => [ - [ - 't050v01' => 'SN202604130001', - 'api_token' => 'mac_token_...', - 't050v41' => '80812345', - 't050v34' => '2000132', - 'TP_APP_ID' => 'GP_001' - ] + 't050v01' => 'SN202604130001', + 'api_token' => 'mac_token_...', + 't050v41' => '80812345', + 't050v34' => '2000132', + 'TP_APP_ID' => 'GP_001' ] ], ], @@ -115,15 +113,13 @@ return [ 'success' => true, 'code' => 200, 'data' => [ - [ - 't050v01' => 'SN202604130001', - 'api_token' => 'mac_token_...', - 't050v41' => '80812345', - 't050v42' => '9001', - 't050v43' => 'hash_key', - 't050v34' => '2000132', - 'TP_APP_ID' => 'GP_001' - ] + 't050v01' => 'SN202604130001', + 'api_token' => 'mac_token_...', + 't050v41' => '80812345', + 't050v42' => '9001', + 't050v43' => 'hash_key', + 't050v34' => '2000132', + 'TP_APP_ID' => 'GP_001' ] ], 'notes' => '此 API 為機台初始化引導用,目前不強制驗證 User Token。' diff --git a/database/migrations/2026_05_06_160342_add_usage_limit_to_pickup_codes_table.php b/database/migrations/2026_05_06_160342_add_usage_limit_to_pickup_codes_table.php new file mode 100644 index 0000000..df635a7 --- /dev/null +++ b/database/migrations/2026_05_06_160342_add_usage_limit_to_pickup_codes_table.php @@ -0,0 +1,29 @@ +integer('usage_limit')->default(1)->after('code')->comment('使用次數上限'); + $table->integer('usage_count')->default(0)->after('usage_limit')->comment('已使用次數'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('pickup_codes', function (Blueprint $table) { + $table->dropColumn(['usage_limit', 'usage_count']); + }); + } +}; diff --git a/lang/en.json b/lang/en.json index 39a2fab..301e3da 100644 --- a/lang/en.json +++ b/lang/en.json @@ -175,6 +175,7 @@ "Basic Information": "Basic Information", "Basic Settings": "Basic Settings", "Basic Specifications": "Basic Specifications", + "Based on Hours": "Based on Hours", "Batch": "Batch", "Batch No": "Batch No", "Batch Number": "Batch Number", @@ -337,6 +338,8 @@ "Current Stocks": "Current Stocks", "Current Type": "Current Type", "Current:": "Cur:", + "Custom Date": "Custom Date", + "Custom Date Set": "Custom Date Set", "Customer Details": "Customer Details", "Customer Info": "Customer Info", "Customer List": "Customer List", @@ -1544,6 +1547,9 @@ "View Slots": "View Slots", "View all warehouses": "View all warehouses", "View slot-level inventory for each machine": "View slot-level inventory for each machine", + "Usage Limit": "Usage Limit", + "Usage Logs": "Usage Logs", + "Usage Progress": "Usage Progress", "Visit Gift": "Visit Gift", "Visual overview of all machine locations": "Visual overview of all machine locations", "Void Invoice": "Void Invoice", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index a27fb66..2a7047a 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -183,6 +183,7 @@ "Batch No": "批號", "Batch Number": "批號", "Before Qty": "異動前數量", + "Based on Hours": "依據時數計算", "Belongs To": "公司名稱", "Belongs To Company": "公司名稱", "Branch": "分倉", @@ -343,6 +344,8 @@ "Current Stocks": "目前庫存", "Current Type": "當前類型", "Current:": "現:", + "Custom Date": "自定義日期", + "Custom Date Set": "已設定自定義日期", "Customer Details": "客戶詳情", "Customer Info": "客戶資訊", "Customer List": "客戶列表", @@ -666,6 +669,7 @@ "Hopper error (0424)": "料斗箱異常 (0424)", "Hopper heating timeout": "料斗箱加熱逾時", "Hopper overheated": "料斗箱過熱", + "Hours": "小時", "Hrs": "小時", "IC Card UID": "IC 卡號", "ITEM": "ITEM", @@ -1632,7 +1636,9 @@ "Upload New Images": "上傳新照片", "Upload Video": "上傳影片", "Uploading new images will replace all existing images.": "上傳新照片將會取代所有現有照片。", + "Usage Limit": "使用次數上限", "Usage Logs": "使用紀錄", + "Usage Progress": "使用進度", "Verified": "驗證成功", "Usage Completed": "使用完成", "Used": "已使用", diff --git a/resources/views/admin/sales/pickup-codes/index.blade.php b/resources/views/admin/sales/pickup-codes/index.blade.php index 4a84991..1772af5 100644 --- a/resources/views/admin/sales/pickup-codes/index.blade.php +++ b/resources/views/admin/sales/pickup-codes/index.blade.php @@ -90,7 +90,115 @@ transition-all duration-300", {{-- End relative min-h --}} {{-- End mt-6 --}} - {{-- Create Modal --}} + {{-- Edit Pickup Code Modal --}} +
+ +
+ +
+ +
+ @csrf + @method('PUT') + +
+
+
+ +
+
+

{{ __('Edit Pickup Code') }}

+

{{ __('Update usage and expiry') }}

+
+
+ +
+ +
+ {{-- Usage Limit --}} +
+ +
+ {{-- Unified Luxury Counter --}} +
+ +
+
+ +
+
+ +
+ + {{-- Quick Select Buttons --}} +
+ +
+
+
+ + {{-- Expiry Date --}} +
+ +
+ + + + +
+
+
+ +
+ + +
+
+
+
+ + {{-- Create Pickup Code Modal --}}
+ +
+ +
+ {{-- Unified Luxury Counter --}} +
+ +
+
+ +
+
+ +
+ + {{-- Quick Select Buttons - Removed scrollbar --}} +
+ +
+
+
{{-- Pickup Code Preview Section --}} @@ -168,43 +311,73 @@ transition-all duration-300", maxlength="12" required> - {{-- Validity Period Section (Compact) --}} + {{-- Validity Period Section (Flexible) --}}
- - -
-
-
- +
+ +
+ + +
+
+ +
+
+
+
+ +
+
+ + {{ __('Hrs') }} +
-
- - {{ - __('Hrs') }} +
+
- + +
+
+ +
+ + + +
+
+
+
{{ __('Expected Expiry') }} - {{ __('Max 24 Hours') }} +
-
- - +
+ + - +
@@ -321,7 +494,10 @@ transition-all duration-300", selectedMachine: '', selectedSlot: '', expiresHours: 24, - maxHours: 24, + maxHours: 168, + expiryMode: 'hours', + customExpiry: '', + usageLimit: 1, showQrModal: false, activeQrCode: '', activeTicketUrl: '', @@ -333,6 +509,12 @@ transition-all duration-300", slotSelectConfig: config, customCode: '', + // Edit Pickup Code State + showEditModal: false, + editPickupCodeId: '', + editUsageLimit: 1, + editExpiresAt: '', + async fetchTabData(tab, url = null) { @@ -397,6 +579,10 @@ transition-all duration-300", calculateExpiry() { const date = new Date(); date.setHours(date.getHours() + parseInt(this.expiresHours)); + return this.formatDate(date); + }, + + formatDate(date) { return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0') + ' ' + @@ -404,6 +590,13 @@ transition-all duration-300", String(date.getMinutes()).padStart(2, '0'); }, + getDisplayExpiry() { + if (this.expiryMode === 'date' && this.customExpiry) { + return this.customExpiry.replace('T', ' '); + } + return this.calculateExpiry(); + }, + async fetchSlots() { if (!this.selectedMachine) { this.slots = []; @@ -426,19 +619,19 @@ transition-all duration-300", window.showToast('{{ __("Please select a machine") }}', 'error'); return; } - if (!this.selectedSlot || this.selectedSlot.toString().trim() === '') { + if (!this.selectedSlot) { window.showToast('{{ __("Please select a slot") }}', 'error'); return; } - if (!this.customCode || this.customCode.toString().trim() === '') { - window.showToast('{{ __("Please enter or generate a pickup code") }}', 'error'); - return; - } - this.$el.submit(); + this.$refs.createForm.submit(); }, - - + openEditModal(id, limit, expiry) { + this.editPickupCodeId = id; + this.editUsageLimit = parseInt(limit); + this.editExpiresAt = expiry; + this.showEditModal = true; + }, copyQrLink() { if (!this.activeTicketUrl) return; navigator.clipboard.writeText(this.activeTicketUrl).then(() => { @@ -571,10 +764,23 @@ transition-all duration-300", this.$watch('selectedSlot', (val) => { // Selection toast removed per user request }); + this.$watch('expiryMode', (mode) => { + if (mode === 'date') { + this.customExpiry = this.calculateExpiry(); + } + }); + this.$watch('showCreateModal', (val) => { if (val) { this.selectedMachine = ''; this.selectedSlot = ''; + this.usageLimit = 1; + this.expiryMode = 'hours'; + this.expiresHours = 24; + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setMinutes(0); + this.customExpiry = this.formatDate(tomorrow); this.slots = []; this.generateRandomCode(); this.updateSlotSelect(); diff --git a/resources/views/admin/sales/pickup-codes/partials/tab-list.blade.php b/resources/views/admin/sales/pickup-codes/partials/tab-list.blade.php index 18d6865..4e419a0 100644 --- a/resources/views/admin/sales/pickup-codes/partials/tab-list.blade.php +++ b/resources/views/admin/sales/pickup-codes/partials/tab-list.blade.php @@ -139,7 +139,12 @@ $code->status; @endphp