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/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php
index e328e4c..ff536d3 100644
--- a/app/Http/Controllers/Admin/SalesController.php
+++ b/app/Http/Controllers/Admin/SalesController.php
@@ -194,7 +194,10 @@ class SalesController extends Controller
// 1. 取貨碼列表 (list)
if (!$isAjax || $tab === 'list') {
- $query = PickupCode::with(['machine.slots.product', 'creator', 'order'])->latest();
+ // 僅顯示目前帳號可存取機台的取貨碼(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope)
+ $query = PickupCode::with(['machine.slots.product', 'creator', 'order'])
+ ->whereHas('machine')
+ ->latest();
if ($request->search) {
$query->where(function ($q) use ($request) {
@@ -219,7 +222,12 @@ class SalesController extends Controller
// 2. 操作紀錄 (logs)
if (!$isAjax || $tab === 'logs') {
- $logQuery = PickupCodeLog::with(['user:id,name', 'machine:id,name,serial_no', 'pickupCode', 'order']);
+ // 同上:操作紀錄只顯示可存取機台的資料(machine_id 為 null 的非機台紀錄仍保留)
+ $logQuery = PickupCodeLog::with(['user:id,name', 'machine:id,name,serial_no', 'pickupCode', 'order'])
+ ->where(function ($q) {
+ $q->whereNull('machine_id')
+ ->orWhereHas('machine');
+ });
if ($request->filled('search_log')) {
$search = $request->input('search_log');
@@ -362,6 +370,11 @@ class SalesController extends Controller
*/
public function destroyPickupCode(PickupCode $pickupCode)
{
+ // 機台授權以 machine_user 為準:非系統管理員若無法存取該取貨碼所屬機台則拒絕
+ if (!Auth::user()->isSystemAdmin() && !Machine::whereKey($pickupCode->machine_id)->exists()) {
+ abort(403);
+ }
+
$oldValues = $pickupCode->toArray();
$pickupCode->update(['status' => 'cancelled']);
@@ -411,7 +424,10 @@ class SalesController extends Controller
// 1. 通行碼列表 (list)
if (!$isAjax || $tab === 'list') {
- $query = PassCode::with(['machine', 'creator'])->latest();
+ // 僅顯示目前帳號可存取機台的通行碼(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope)
+ $query = PassCode::with(['machine', 'creator'])
+ ->whereHas('machine')
+ ->latest();
if ($request->search) {
$query->where(function ($q) use ($request) {
@@ -447,7 +463,9 @@ class SalesController extends Controller
// 2. 操作紀錄 (logs)
if (!$isAjax || $tab === 'logs') {
- $logQuery = PassCodeLog::with(['user:id,name', 'machine:id,name,serial_no', 'passCode']);
+ // 同上:操作紀錄只顯示可存取機台的資料(pass_code_logs.machine_id 為 NOT NULL)
+ $logQuery = PassCodeLog::with(['user:id,name', 'machine:id,name,serial_no', 'passCode'])
+ ->whereHas('machine');
if ($request->filled('search_log')) {
$search = $request->input('search_log');
@@ -587,6 +605,11 @@ class SalesController extends Controller
*/
public function destroyPassCode(PassCode $passCode)
{
+ // 機台授權以 machine_user 為準:非系統管理員若無法存取該通行碼所屬機台則拒絕
+ if (!Auth::user()->isSystemAdmin() && !Machine::whereKey($passCode->machine_id)->exists()) {
+ abort(403);
+ }
+
$oldValues = $passCode->toArray();
$passCode->update(['status' => 'disabled']);
@@ -618,7 +641,10 @@ class SalesController extends Controller
// 1. 來店禮列表 (list)
if (!$isAjax || $tab === 'list') {
- $query = WelcomeGift::with(['machine', 'creator'])->latest();
+ // 僅顯示目前帳號可存取機台的來店禮(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope)
+ $query = WelcomeGift::with(['machine', 'creator'])
+ ->whereHas('machine')
+ ->latest();
if ($request->search) {
$query->where(function ($q) use ($request) {
@@ -654,7 +680,9 @@ class SalesController extends Controller
// 2. 操作紀錄 (logs)
if (!$isAjax || $tab === 'logs') {
- $logQuery = WelcomeGiftLog::with(['machine:id,name,serial_no', 'welcomeGift', 'order:id,order_no']);
+ // 同上:操作紀錄只顯示可存取機台的資料(welcome_gift_logs.machine_id 為 NOT NULL)
+ $logQuery = WelcomeGiftLog::with(['machine:id,name,serial_no', 'welcomeGift', 'order:id,order_no'])
+ ->whereHas('machine');
if ($request->filled('search_log')) {
$search = $request->input('search_log');
@@ -846,6 +874,11 @@ class SalesController extends Controller
*/
public function destroyWelcomeGift(WelcomeGift $welcomeGift)
{
+ // 機台授權以 machine_user 為準:非系統管理員若無法存取該來店禮所屬機台則拒絕
+ if (!Auth::user()->isSystemAdmin() && !Machine::whereKey($welcomeGift->machine_id)->exists()) {
+ abort(403);
+ }
+
$oldValues = $welcomeGift->toArray();
$welcomeGift->update(['status' => 'disabled']);
diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php
index 6a8053b..79e2c6a 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 abda173..a350338 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();
@@ -422,6 +436,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()
@@ -523,19 +619,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']) {
@@ -551,6 +670,8 @@ class MachineService
);
}
+ $slot->refresh();
+
// 3. 建立遠端指令紀錄
$command = RemoteCommand::create([
'machine_id' => $machine->id,
@@ -561,19 +682,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/lang/en.json b/lang/en.json
index 9cce327..2f91f32 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -1050,11 +1050,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",
@@ -1423,6 +1425,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.",
@@ -1826,7 +1829,9 @@
"Running Status": "Running Status",
"SYSTEM": "SYSTEM",
"Safety Stock": "Safety Stock",
+ "Sale Paused": "Sale Paused",
"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 92d9939..d0fb4aa 100644
--- a/lang/ja.json
+++ b/lang/ja.json
@@ -1050,11 +1050,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": "記録時間",
@@ -1423,6 +1425,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.": "アカウントが削除されると、すべてのデータは永久に削除されます。完全に削除することを確認するためにパスワードを入力してください。",
@@ -1826,7 +1829,9 @@
"Running Status": "稼働ステータス",
"SYSTEM": "システムレベル",
"Safety Stock": "安全在庫",
+ "Sale Paused": "販売停止",
"Sale Price": "販売価格",
+ "Sale Status": "販売状態",
"Sales": "販売管理",
"Sales Activity": "販売活動",
"Sales Amount": "売上金額",
diff --git a/lang/zh_TW.json b/lang/zh_TW.json
index d377ea5..3430ff7 100644
--- a/lang/zh_TW.json
+++ b/lang/zh_TW.json
@@ -1050,11 +1050,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": "記錄時間",
@@ -1423,6 +1425,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.": "帳號一旦刪除,所有關連數據將被永久移除。請輸入您的密碼以確認您希望永久刪除此帳號。",
@@ -1826,7 +1829,9 @@
"Running Status": "運行狀態",
"SYSTEM": "系統層級",
"Safety Stock": "安全庫存",
+ "Sale Paused": "暫停販售",
"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 5e239cc..1e36ebb 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) {
@@ -352,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 '';
@@ -666,12 +710,21 @@
class="text-xs font-black uppercase tracking-tighter text-slate-800 dark:text-white"
x-text="slot.slot_no">
-
-
{{ - $code->machine->name }}
+ $code->machine?->name ?? '-' }}
- {{ $code->machine->name }} + {{ $code->machine?->name ?? '-' }}
{{ $code->slot_no }} @php - $slotMobile = $code->machine->slots->where('slot_no', + $slotMobile = $code->machine?->slots->where('slot_no', $code->slot_no)->first(); $prodMobile = $slotMobile ? ($slotMobile->product?->name ?? __('Empty')) : __('Empty'); diff --git a/resources/views/admin/sales/welcome-gifts/partials/tab-list.blade.php b/resources/views/admin/sales/welcome-gifts/partials/tab-list.blade.php index 9655d27..84dbd89 100644 --- a/resources/views/admin/sales/welcome-gifts/partials/tab-list.blade.php +++ b/resources/views/admin/sales/welcome-gifts/partials/tab-list.blade.php @@ -108,7 +108,7 @@
{{ __('Machine') }}
- {{ $gift->machine->name }}
+ {{ $gift->machine?->name ?? '-' }}diff --git a/resources/views/admin/warehouses/machine-inventory.blade.php b/resources/views/admin/warehouses/machine-inventory.blade.php index 805c739..f95b20d 100644 --- a/resources/views/admin/warehouses/machine-inventory.blade.php +++ b/resources/views/admin/warehouses/machine-inventory.blade.php @@ -296,6 +296,12 @@