From af78c48d8e7e06c3869dd5adae4dda5c85034d68 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Wed, 24 Jun 2026 14:25:33 +0800 Subject: [PATCH 1/3] =?UTF-8?q?[FEAT]=20=E8=B2=A8=E9=81=93=E6=95=88?= =?UTF-8?q?=E6=9C=9F+=E9=8E=96=E5=AE=9A=20App=E2=86=94=E5=BE=8C=E5=8F=B0?= =?UTF-8?q?=E9=9B=99=E5=90=91=E5=90=8C=E6=AD=A5=EF=BC=88LWW=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. machine_slots 新增 is_locked / last_app_lock_rev / last_app_expiry_rev(migration);MachineSlot model fillable+casts。 2. syncSlots(B009):新增 per-field 常態 LWW(App rev 較大才採用);換商品(product_id 變更)時清空舊 expiry/batch 安全網;空值一律歸一化為 null。 3. updateSlot:下行 update_inventory 一律帶 is_locked + lock_rev + expiry_rev,後台編輯只更新「帶上的欄位」(鎖定切換不會清效期),對應 rev +1 成為權威值。 4. 新增 admin slots/lock endpoint(toggleSlotLock)+ route;remote/stock.blade.php 貨道編輯彈窗加「販售狀態/鎖定·解鎖」toggle。 5. 不含 app_uuid/重灌/B017 概念(精簡版);既有現場鎖定靠 App 端升級遷移(lock_rev=1)走常態 LWW 上雲。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Controllers/Admin/MachineController.php | 26 +++ .../Api/V1/App/MachineController.php | 6 + app/Models/Machine/MachineSlot.php | 7 + app/Services/Machine/MachineService.php | 161 ++++++++++++++++-- ...0000_add_slot_lock_and_lww_rev_columns.php | 31 ++++ resources/views/admin/remote/stock.blade.php | 54 ++++++ routes/web.php | 1 + 7 files changed, 268 insertions(+), 18 deletions(-) create mode 100644 database/migrations/2026_06_24_120000_add_slot_lock_and_lww_rev_columns.php diff --git a/app/Http/Controllers/Admin/MachineController.php b/app/Http/Controllers/Admin/MachineController.php index 6386ce4..4a371f5 100644 --- a/app/Http/Controllers/Admin/MachineController.php +++ b/app/Http/Controllers/Admin/MachineController.php @@ -265,6 +265,32 @@ class MachineController extends AdminController } } + /** + * AJAX: 遠端鎖定/解鎖單一貨道(暫停販售)。 + * 與 updateSlotExpiry 共用 updateSlot 下發 update_inventory;只帶 is_locked,不動效期/庫存。 + */ + public function toggleSlotLock(Request $request, Machine $machine) + { + $validated = $request->validate([ + 'slot_no' => 'required|integer', + 'is_locked' => 'required|boolean', + ]); + + try { + $this->machineService->updateSlot($machine, $validated, auth()->id()); + + return response()->json([ + 'success' => true, + 'message' => $validated['is_locked'] ? __('Slot locked.') : __('Slot unlocked.'), + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage() + ], 422); + } + } + /** * 機台專屬定價頁:列出該機台「所屬公司的完整商品目錄」(非僅貨道商品), * 可在上貨前先把機台價/會員價訂好。未填=沿用全域 products 價。 diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index 18996a4..7a99353 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -371,6 +371,12 @@ class MachineController extends Controller 'product_id' => $item['t060v00'] ?? null, 'stock' => isset($item['num']) ? (int) $item['num'] : 0, 'type' => isset($item['type']) ? (int) $item['type'] : null, + // 雙向 LWW:鎖定與效期/批號 + 各自的編輯序號(App 端 store 編輯才遞增) + 'is_locked' => isset($item['is_locked']) ? (bool) $item['is_locked'] : false, + 'lock_rev' => isset($item['lock_rev']) ? (int) $item['lock_rev'] : 0, + 'expiry_date' => $item['expiry_date'] ?? null, + 'batch_no' => $item['batch_no'] ?? null, + 'expiry_rev' => isset($item['expiry_rev']) ? (int) $item['expiry_rev'] : 0, ]; }, $legacyData); diff --git a/app/Models/Machine/MachineSlot.php b/app/Models/Machine/MachineSlot.php index c8e184a..9867c38 100644 --- a/app/Models/Machine/MachineSlot.php +++ b/app/Models/Machine/MachineSlot.php @@ -20,12 +20,19 @@ class MachineSlot extends Model 'expiry_date', 'batch_no', 'is_active', + 'is_locked', + 'last_app_lock_rev', + 'last_app_expiry_rev', ]; protected $casts = [ 'price' => 'decimal:2', 'last_restocked_at' => 'datetime', 'expiry_date' => 'date:Y-m-d', + 'is_active' => 'boolean', + 'is_locked' => 'boolean', + 'last_app_lock_rev' => 'integer', + 'last_app_expiry_rev' => 'integer', ]; public function machine() diff --git a/app/Services/Machine/MachineService.php b/app/Services/Machine/MachineService.php index d93b21d..12a9509 100644 --- a/app/Services/Machine/MachineService.php +++ b/app/Services/Machine/MachineService.php @@ -338,17 +338,31 @@ class MachineService 'is_active' => true, ]; + // ── 雙向 LWW:鎖定(lock) 與 效期/批號(expiry/batch),各自獨立 rev ── + $updateData = array_merge( + $updateData, + $this->resolveSlotLockLww($existingSlot, $slotData), + $this->resolveSlotExpiryLww($existingSlot, $slotData, $actualProductId) + ); + if ($existingSlot) { $oldStock = (int) $existingSlot->stock; $oldProductId = $existingSlot->product_id; - + // 檢查是否有實質變動 (Check for actual changes) $isStockChanged = ($oldStock !== $newStock); $isProductChanged = ($oldProductId != $actualProductId); // 這裡用鬆散比對以處理 null/0 的情況 $isSlotConfigChanged = (string) $existingSlot->type !== (string) $updateData['type'] || (int) $existingSlot->max_stock !== (int) $updateData['max_stock']; + // lock/expiry/rev 也算實質變動,確保 LWW 結果會被寫回(即使 stock/product 沒變) + $curExpiryStr = $existingSlot->expiry_date ? $existingSlot->expiry_date->format('Y-m-d') : null; + $isLwwChanged = ((bool) $existingSlot->is_locked !== (bool) $updateData['is_locked']) + || ((int) $existingSlot->last_app_lock_rev !== (int) $updateData['last_app_lock_rev']) + || ((int) $existingSlot->last_app_expiry_rev !== (int) $updateData['last_app_expiry_rev']) + || ($curExpiryStr !== $updateData['expiry_date']) + || ($existingSlot->batch_no !== $updateData['batch_no']); - if ($isStockChanged || $isProductChanged || $isSlotConfigChanged) { + if ($isStockChanged || $isProductChanged || $isSlotConfigChanged || $isLwwChanged) { $existingSlot->update($updateData); $existingSlot->refresh(); @@ -420,6 +434,88 @@ class MachineService }); } + /** + * 空字串/"null" 一律歸一化為 null(避免 MySQL date 欄位寫入空字串解析爆)。 + */ + private function blankToNull($value) + { + if (is_string($value)) { + $value = trim($value); + } + if ($value === '' || $value === null || (is_string($value) && strtolower($value) === 'null')) { + return null; + } + return $value; + } + + /** + * 取日期字串的 yyyy-MM-dd 部分(容錯後台/機台帶了時間),空值回 null。 + */ + private function normalizeDate($value) + { + $value = $this->blankToNull($value); + if ($value === null) { + return null; + } + return substr((string) $value, 0, 10); + } + + /** + * 貨道鎖定(lock)的雙向 LWW 解析,回傳要寫入的 is_locked / last_app_lock_rev。 + * - 新貨道 → 採 App 現場值。 + * - 既有貨道 → 常態 LWW:App rev 較大才採用,否則保留後台值。 + * (既有機台升級時,App 端 onUpgrade 已把現場既有鎖定的 lock_rev 設為 1,故能 1>0 自動遷移上雲。) + */ + private function resolveSlotLockLww(?MachineSlot $existingSlot, array $slotData): array + { + $appLocked = (bool) ($slotData['is_locked'] ?? false); + $appRev = (int) ($slotData['lock_rev'] ?? 0); + + if (!$existingSlot) { + $newRev = $appRev > 0 ? $appRev : ($appLocked ? 1 : 0); + return ['is_locked' => $appLocked, 'last_app_lock_rev' => $newRev]; + } + $curLocked = (bool) $existingSlot->is_locked; + $curRev = (int) $existingSlot->last_app_lock_rev; + if ($appRev > $curRev) { + return ['is_locked' => $appLocked, 'last_app_lock_rev' => $appRev]; + } + return ['is_locked' => $curLocked, 'last_app_lock_rev' => $curRev]; + } + + /** + * 效期/批號(expiry/batch)的雙向 LWW 解析,回傳要寫入的 expiry_date / batch_no / last_app_expiry_rev。 + * 換商品(product_id 變更)時預設清空舊效期(安全網),除非 App 同筆帶了較新的新效期。 + */ + private function resolveSlotExpiryLww(?MachineSlot $existingSlot, array $slotData, $actualProductId): array + { + $appExpiry = $this->normalizeDate($slotData['expiry_date'] ?? null); + $appBatch = $this->blankToNull($slotData['batch_no'] ?? null); + $appRev = (int) ($slotData['expiry_rev'] ?? 0); + + if (!$existingSlot) { + $newRev = $appRev > 0 ? $appRev : ($appExpiry !== null ? 1 : 0); + return ['expiry_date' => $appExpiry, 'batch_no' => $appBatch, 'last_app_expiry_rev' => $newRev]; + } + $curExpiry = $existingSlot->expiry_date ? $existingSlot->expiry_date->format('Y-m-d') : null; + $curBatch = $this->blankToNull($existingSlot->batch_no); + $curRev = (int) $existingSlot->last_app_expiry_rev; + + // 換商品安全網:預設清空,除非 App 同筆帶了較新的新效期 + $isProductChanged = ((string) ($existingSlot->product_id) !== (string) $actualProductId); + if ($isProductChanged) { + if ($appExpiry !== null && $appRev > $curRev) { + return ['expiry_date' => $appExpiry, 'batch_no' => $appBatch, 'last_app_expiry_rev' => $appRev]; + } + return ['expiry_date' => null, 'batch_no' => null, 'last_app_expiry_rev' => max($curRev, $appRev)]; + } + + if ($appRev > $curRev) { + return ['expiry_date' => $appExpiry, 'batch_no' => $appBatch, 'last_app_expiry_rev' => $appRev]; + } + return ['expiry_date' => $curExpiry, 'batch_no' => $curBatch, 'last_app_expiry_rev' => $curRev]; + } + public function syncProductSlotMaxStock(Product $product): void { MachineSlot::query() @@ -521,19 +617,42 @@ class MachineService } // 紀錄舊數據以供回滾使用 + $oldExpiry = $slot->expiry_date ? Carbon::parse($slot->expiry_date)->toDateString() : null; $oldData = [ 'stock' => $slot->stock, - 'expiry_date' => $slot->expiry_date ? Carbon::parse($slot->expiry_date)->toDateString() : null, + 'expiry_date' => $oldExpiry, 'batch_no' => $slot->batch_no, + 'is_locked' => (bool) $slot->is_locked, ]; - // 2. 執行樂觀更新 (Optimistic Update) - $updateData = [ - 'expiry_date' => $expiryDate, - 'batch_no' => $batchNo, - ]; - if ($stock !== null) $updateData['stock'] = (int)$stock; - $slot->update($updateData); + // 2. 執行樂觀更新 (Optimistic Update):只更新「明確帶上的欄位」,避免鎖定切換誤清效期 + $updateData = []; + $normExpiry = $oldExpiry; + if (array_key_exists('expiry_date', $data)) { + $normExpiry = $this->normalizeDate($expiryDate); + $updateData['expiry_date'] = $normExpiry; + } + $normBatch = $slot->batch_no; + if (array_key_exists('batch_no', $data)) { + $normBatch = $this->blankToNull($batchNo); + $updateData['batch_no'] = $normBatch; + } + if ($stock !== null) $updateData['stock'] = (int) $stock; + if (array_key_exists('is_locked', $data)) { + $updateData['is_locked'] = (bool) $data['is_locked']; + } + + // 後台編輯效期/批號或鎖定 → 遞增對應 rev(成為權威值,下行帶給 App 對齊 synced_rev) + $expiryEdited = ($normExpiry !== $oldExpiry) || ($normBatch !== $slot->batch_no); + if ($expiryEdited) { + $updateData['last_app_expiry_rev'] = (int) $slot->last_app_expiry_rev + 1; + } + $lockEdited = array_key_exists('is_locked', $data) && ((bool) $data['is_locked'] !== (bool) $slot->is_locked); + if ($lockEdited) { + $updateData['last_app_lock_rev'] = (int) $slot->last_app_lock_rev + 1; + } + + if (!empty($updateData)) $slot->update($updateData); // 3. 若庫存數值有異動,記錄 adjustment 流水帳 if ($stock !== null && (int)$stock !== $oldData['stock']) { @@ -549,6 +668,8 @@ class MachineService ); } + $slot->refresh(); + // 3. 建立遠端指令紀錄 $command = RemoteCommand::create([ 'machine_id' => $machine->id, @@ -559,19 +680,23 @@ class MachineService 'slot_no' => (string)$slotNo, 'old' => $oldData, 'new' => [ - 'stock' => $stock !== null ? (int)$stock : $oldData['stock'], - 'expiry_date' => $expiryDate ?: null, - 'batch_no' => $batchNo ?: null, + 'stock' => (int) $slot->stock, + 'expiry_date' => $slot->expiry_date ? $slot->expiry_date->format('Y-m-d') : null, + 'batch_no' => $slot->batch_no, + 'is_locked' => (bool) $slot->is_locked, ] ] ]); - // 4. 推播 MQTT + // 4. 推播 MQTT(一律帶上目前權威的 is_locked 與雙 rev,讓 App 端對齊 synced_rev) $mqttPayload = [ - 'slot_no' => $command->payload['slot_no'], - 'stock' => $command->payload['new']['stock'], - 'expiry_date' => $command->payload['new']['expiry_date'], - 'batch_no' => $command->payload['new']['batch_no'] ?? null, + 'slot_no' => (string) $slotNo, + 'stock' => (int) $slot->stock, + 'expiry_date' => $slot->expiry_date ? $slot->expiry_date->format('Y-m-d') : null, + 'batch_no' => $slot->batch_no, + 'is_locked' => (bool) $slot->is_locked, + 'lock_rev' => (int) $slot->last_app_lock_rev, + 'expiry_rev' => (int) $slot->last_app_expiry_rev, ]; app(\App\Services\Machine\MqttService::class)->pushCommand( diff --git a/database/migrations/2026_06_24_120000_add_slot_lock_and_lww_rev_columns.php b/database/migrations/2026_06_24_120000_add_slot_lock_and_lww_rev_columns.php new file mode 100644 index 0000000..b09a62f --- /dev/null +++ b/database/migrations/2026_06_24_120000_add_slot_lock_and_lww_rev_columns.php @@ -0,0 +1,31 @@ +boolean('is_locked')->default(false)->after('is_active'); + $table->unsignedBigInteger('last_app_lock_rev')->default(0)->after('is_locked'); + $table->unsignedBigInteger('last_app_expiry_rev')->default(0)->after('last_app_lock_rev'); + }); + } + + public function down(): void + { + Schema::table('machine_slots', function (Blueprint $table) { + $table->dropColumn(['is_locked', 'last_app_lock_rev', 'last_app_expiry_rev']); + }); + } +}; diff --git a/resources/views/admin/remote/stock.blade.php b/resources/views/admin/remote/stock.blade.php index 5e239cc..3ed8ea3 100644 --- a/resources/views/admin/remote/stock.blade.php +++ b/resources/views/admin/remote/stock.blade.php @@ -251,6 +251,42 @@ } }, + async toggleLock() { + this.updating = true; + try { + const csrf = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + const res = await fetch(`/admin/machines/${this.selectedMachine.id}/slots/lock`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRF-TOKEN': csrf + }, + body: JSON.stringify({ + slot_no: this.selectedSlot.slot_no, + is_locked: !this.selectedSlot.is_locked + }) + }); + const data = await res.json(); + if (data.success) { + this.showEditModal = false; + window.location.href = "{{ route('admin.remote.stock') }}"; + } else { + throw new Error(data.message || 'Unknown error'); + } + } catch (e) { + window.dispatchEvent(new CustomEvent('toast', { + detail: { + message: '{{ __("Save error:") }} ' + e.message, + type: 'error' + } + })); + console.error('Lock toggle error:', e); + } finally { + this.updating = false; + } + }, + getSlotColorClass(slot) { const today = new Date().toISOString().split('T')[0]; if (slot.expiry_date && slot.expiry_date < today) { @@ -966,6 +1002,24 @@ + +
+
+
{{ + __('Sale Status') }}
+
+
+
+ +
+ diff --git a/routes/web.php b/routes/web.php index 37e2903..97af95d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -68,6 +68,7 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix Route::get('/utilization-ajax/{id?}', [App\Http\Controllers\Admin\MachineController::class, 'utilizationData'])->name('utilization-ajax'); Route::get('/{machine}/slots-ajax', [App\Http\Controllers\Admin\MachineController::class, 'slotsAjax'])->name('slots-ajax'); Route::post('/{machine}/slots/expiry', [App\Http\Controllers\Admin\MachineController::class, 'updateSlotExpiry'])->name('slots.expiry.update'); + Route::post('/{machine}/slots/lock', [App\Http\Controllers\Admin\MachineController::class, 'toggleSlotLock'])->name('slots.lock.toggle'); // 機台專屬定價(獨立頁,列全公司目錄,可上貨前先定價) Route::get('/{machine}/pricing', [App\Http\Controllers\Admin\MachineController::class, 'pricing'])->name('pricing'); Route::post('/{machine}/pricing', [App\Http\Controllers\Admin\MachineController::class, 'updatePricing'])->name('pricing.update'); From 6ce4c2c8edfad9ed7a37d65c5565fd23d935cfeb Mon Sep 17 00:00:00 2001 From: sky121113 Date: Wed, 24 Jun 2026 14:36:20 +0800 Subject: [PATCH 2/3] =?UTF-8?q?[FIX]=20=E8=B2=A8=E9=81=93=E9=8E=96?= =?UTF-8?q?=E5=AE=9A=E6=93=8D=E4=BD=9C=E7=B4=80=E9=8C=84=E9=A1=AF=E7=A4=BA?= =?UTF-8?q?=E8=88=87=E5=A4=9A=E8=AA=9E=E7=B3=BB=E8=A3=9C=E9=BD=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 操作紀錄徽章補上鎖定/解鎖切換顯示,修正僅 is_locked 變動時出現空白「貨道 N:」 2. 補齊鎖定彈窗 4 個語系 key(Sale Status / On Sale / Locked (Sale Paused) / Lock (Pause Sale))三語系對齊 Co-Authored-By: Claude Opus 4.8 (1M context) --- lang/en.json | 4 ++++ lang/ja.json | 4 ++++ lang/zh_TW.json | 4 ++++ resources/views/admin/remote/stock.blade.php | 8 ++++++++ 4 files changed, 20 insertions(+) diff --git a/lang/en.json b/lang/en.json index d14532b..9124034 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1039,11 +1039,13 @@ "Location found!": "Location found!", "Location not found": "Location not found", "Lock": "Lock", + "Lock (Pause Sale)": "Lock (Pause Sale)", "Lock Now": "Lock Now", "Lock Page": "Lock Page", "Lock Page Lock": "Lock Page Lock", "Lock Page Unlock": "Lock Page Unlock", "Locked": "Locked", + "Locked (Sale Paused)": "Locked (Sale Paused)", "Locked Page": "Locked Page", "Log Details": "Log Details", "Log Time": "Log Time", @@ -1409,6 +1411,7 @@ "Offline Machines": "Offline Machines", "Old": "Old", "Old Value": "Old Value", + "On Sale": "On Sale", "Once": "Once", "Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.", "Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.", @@ -1806,6 +1809,7 @@ "SYSTEM": "SYSTEM", "Safety Stock": "Safety Stock", "Sale Price": "Sale Price", + "Sale Status": "Sale Status", "Sales": "Sales", "Sales Activity": "Sales Activity", "Sales Amount": "Sales Amount", diff --git a/lang/ja.json b/lang/ja.json index befd334..93c00fd 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1039,11 +1039,13 @@ "Location found!": "座標を取得しました!", "Location not found": "場所が見つかりません。ランドマーク名(例:花蓮県庁)を使用してみてください", "Lock": "ロック", + "Lock (Pause Sale)": "ロック(販売停止)", "Lock Now": "今すぐロック", "Lock Page": "ページロック", "Lock Page Lock": "機器アプリロック", "Lock Page Unlock": "機器アプリロック解除", "Locked": "ロック中", + "Locked (Sale Paused)": "ロック中(販売停止)", "Locked Page": "ロックページ", "Log Details": "Log Details", "Log Time": "記録時間", @@ -1409,6 +1411,7 @@ "Offline Machines": "オフライン機器", "Old": "Old", "Old Value": "Old Value", + "On Sale": "販売中", "Once": "一回のみ", "Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "アカウントが削除されると、すべてのリソースとデータは完全に削除されます。アカウントを削除する前に、保持したいデータや情報をダウンロードしてください。", "Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "アカウントが削除されると、すべてのデータは永久に削除されます。完全に削除することを確認するためにパスワードを入力してください。", @@ -1806,6 +1809,7 @@ "SYSTEM": "システムレベル", "Safety Stock": "安全在庫", "Sale Price": "販売価格", + "Sale Status": "販売状態", "Sales": "販売管理", "Sales Activity": "販売活動", "Sales Amount": "売上金額", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index f4b9749..1ad52e0 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -1039,11 +1039,13 @@ "Location found!": "已成功獲取座標!", "Location not found": "找不到位置,您可以嘗試使用地標名稱(例如:花蓮縣政府)", "Lock": "鎖定", + "Lock (Pause Sale)": "鎖定(暫停販售)", "Lock Now": "立即鎖定", "Lock Page": "頁面鎖定", "Lock Page Lock": "機台 APP 鎖定", "Lock Page Unlock": "機台 APP 解鎖", "Locked": "尚未解鎖", + "Locked (Sale Paused)": "已鎖定(暫停販售)", "Locked Page": "鎖定頁", "Log Details": "操作紀錄詳情", "Log Time": "記錄時間", @@ -1409,6 +1411,7 @@ "Offline Machines": "離線機台", "Old": "舊值", "Old Value": "舊資料", + "On Sale": "販售中", "Once": "單次使用", "Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "一旦您的帳號被刪除,其所有資源和數據將被永久刪除。在刪除帳號之前,請下載您希望保留的任何數據或資訊。", "Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "帳號一旦刪除,所有關連數據將被永久移除。請輸入您的密碼以確認您希望永久刪除此帳號。", @@ -1806,6 +1809,7 @@ "SYSTEM": "系統層級", "Safety Stock": "安全庫存", "Sale Price": "售價", + "Sale Status": "銷售狀態", "Sales": "銷售管理", "Sales Activity": "銷售活動", "Sales Amount": "銷售金額", diff --git a/resources/views/admin/remote/stock.blade.php b/resources/views/admin/remote/stock.blade.php index 3ed8ea3..73afda6 100644 --- a/resources/views/admin/remote/stock.blade.php +++ b/resources/views/admin/remote/stock.blade.php @@ -388,6 +388,14 @@ details += `{{ __('Batch') }} ${p.old.batch_no || 'N/A'} → ${p.new.batch_no || 'N/A'}`; } + // 鎖定/解鎖(暫停販售)切換:old/new 僅 is_locked 不同時,避免顯示空白徽章 + if ((p.old.is_locked ?? false) !== (p.new.is_locked ?? false)) { + if (details !== `{{ __('Slot') }} ${p.slot_no}: `) details += ', '; + details += p.new.is_locked + ? `{{ __('Lock (Pause Sale)') }}` + : `{{ __('Unlock') }}`; + } + return details; } return ''; From 1bd89713100fac9040075c3b629286aec47d678e Mon Sep 17 00:00:00 2001 From: sky121113 Date: Wed, 24 Jun 2026 14:49:59 +0800 Subject: [PATCH 3/3] =?UTF-8?q?[FEAT]=20=E5=BA=AB=E5=AD=98=E6=B8=85?= =?UTF-8?q?=E5=96=AE=E8=88=87=E6=A9=9F=E5=8F=B0=E6=A6=82=E8=A6=BD=E9=A1=AF?= =?UTF-8?q?=E7=A4=BA=E8=B2=A8=E9=81=93=E9=8E=96=E5=AE=9A=E7=8B=80=E6=85=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 庫存與效期管理頁(列表/矩陣/手機卡片)商品旁顯示鎖定徽章,使用者可在清單直接辨識暫停販售貨道 2. 機台庫存概覽頁(格狀/表格/手機卡片)同步顯示鎖定徽章 3. 新增 Sale Paused 語系 key 三語系對齊(暫停販售 / Sale Paused / 販売停止) Co-Authored-By: Claude Opus 4.8 (1M context) --- lang/en.json | 1 + lang/ja.json | 1 + lang/zh_TW.json | 1 + resources/views/admin/remote/stock.blade.php | 40 ++++++++++++++----- .../warehouses/machine-inventory.blade.php | 24 ++++++++++- 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/lang/en.json b/lang/en.json index 9124034..2cea620 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1808,6 +1808,7 @@ "Running Status": "Running Status", "SYSTEM": "SYSTEM", "Safety Stock": "Safety Stock", + "Sale Paused": "Sale Paused", "Sale Price": "Sale Price", "Sale Status": "Sale Status", "Sales": "Sales", diff --git a/lang/ja.json b/lang/ja.json index 93c00fd..e3fbd1c 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1808,6 +1808,7 @@ "Running Status": "稼働ステータス", "SYSTEM": "システムレベル", "Safety Stock": "安全在庫", + "Sale Paused": "販売停止", "Sale Price": "販売価格", "Sale Status": "販売状態", "Sales": "販売管理", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 1ad52e0..93fddc4 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -1808,6 +1808,7 @@ "Running Status": "運行狀態", "SYSTEM": "系統層級", "Safety Stock": "安全庫存", + "Sale Paused": "暫停販售", "Sale Price": "售價", "Sale Status": "銷售狀態", "Sales": "銷售管理", diff --git a/resources/views/admin/remote/stock.blade.php b/resources/views/admin/remote/stock.blade.php index 73afda6..1e36ebb 100644 --- a/resources/views/admin/remote/stock.blade.php +++ b/resources/views/admin/remote/stock.blade.php @@ -710,12 +710,21 @@ class="text-xs font-black uppercase tracking-tighter text-slate-800 dark:text-white" x-text="slot.slot_no"> - +
+ + +
@@ -821,8 +830,16 @@
-
+
+ + +