[FIX] 合併取貨碼/通行碼/來店禮租戶機台授權修復
This commit is contained in:
commit
7044d4cefd
@ -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 價。
|
* 可在上貨前先把機台價/會員價訂好。未填=沿用全域 products 價。
|
||||||
|
|||||||
@ -194,7 +194,10 @@ class SalesController extends Controller
|
|||||||
|
|
||||||
// 1. 取貨碼列表 (list)
|
// 1. 取貨碼列表 (list)
|
||||||
if (!$isAjax || $tab === '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) {
|
if ($request->search) {
|
||||||
$query->where(function ($q) use ($request) {
|
$query->where(function ($q) use ($request) {
|
||||||
@ -219,7 +222,12 @@ class SalesController extends Controller
|
|||||||
|
|
||||||
// 2. 操作紀錄 (logs)
|
// 2. 操作紀錄 (logs)
|
||||||
if (!$isAjax || $tab === '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')) {
|
if ($request->filled('search_log')) {
|
||||||
$search = $request->input('search_log');
|
$search = $request->input('search_log');
|
||||||
@ -362,6 +370,11 @@ class SalesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroyPickupCode(PickupCode $pickupCode)
|
public function destroyPickupCode(PickupCode $pickupCode)
|
||||||
{
|
{
|
||||||
|
// 機台授權以 machine_user 為準:非系統管理員若無法存取該取貨碼所屬機台則拒絕
|
||||||
|
if (!Auth::user()->isSystemAdmin() && !Machine::whereKey($pickupCode->machine_id)->exists()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
$oldValues = $pickupCode->toArray();
|
$oldValues = $pickupCode->toArray();
|
||||||
$pickupCode->update(['status' => 'cancelled']);
|
$pickupCode->update(['status' => 'cancelled']);
|
||||||
|
|
||||||
@ -411,7 +424,10 @@ class SalesController extends Controller
|
|||||||
|
|
||||||
// 1. 通行碼列表 (list)
|
// 1. 通行碼列表 (list)
|
||||||
if (!$isAjax || $tab === '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) {
|
if ($request->search) {
|
||||||
$query->where(function ($q) use ($request) {
|
$query->where(function ($q) use ($request) {
|
||||||
@ -447,7 +463,9 @@ class SalesController extends Controller
|
|||||||
|
|
||||||
// 2. 操作紀錄 (logs)
|
// 2. 操作紀錄 (logs)
|
||||||
if (!$isAjax || $tab === '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')) {
|
if ($request->filled('search_log')) {
|
||||||
$search = $request->input('search_log');
|
$search = $request->input('search_log');
|
||||||
@ -587,6 +605,11 @@ class SalesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroyPassCode(PassCode $passCode)
|
public function destroyPassCode(PassCode $passCode)
|
||||||
{
|
{
|
||||||
|
// 機台授權以 machine_user 為準:非系統管理員若無法存取該通行碼所屬機台則拒絕
|
||||||
|
if (!Auth::user()->isSystemAdmin() && !Machine::whereKey($passCode->machine_id)->exists()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
$oldValues = $passCode->toArray();
|
$oldValues = $passCode->toArray();
|
||||||
$passCode->update(['status' => 'disabled']);
|
$passCode->update(['status' => 'disabled']);
|
||||||
|
|
||||||
@ -618,7 +641,10 @@ class SalesController extends Controller
|
|||||||
|
|
||||||
// 1. 來店禮列表 (list)
|
// 1. 來店禮列表 (list)
|
||||||
if (!$isAjax || $tab === '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) {
|
if ($request->search) {
|
||||||
$query->where(function ($q) use ($request) {
|
$query->where(function ($q) use ($request) {
|
||||||
@ -654,7 +680,9 @@ class SalesController extends Controller
|
|||||||
|
|
||||||
// 2. 操作紀錄 (logs)
|
// 2. 操作紀錄 (logs)
|
||||||
if (!$isAjax || $tab === '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')) {
|
if ($request->filled('search_log')) {
|
||||||
$search = $request->input('search_log');
|
$search = $request->input('search_log');
|
||||||
@ -846,6 +874,11 @@ class SalesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroyWelcomeGift(WelcomeGift $welcomeGift)
|
public function destroyWelcomeGift(WelcomeGift $welcomeGift)
|
||||||
{
|
{
|
||||||
|
// 機台授權以 machine_user 為準:非系統管理員若無法存取該來店禮所屬機台則拒絕
|
||||||
|
if (!Auth::user()->isSystemAdmin() && !Machine::whereKey($welcomeGift->machine_id)->exists()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
$oldValues = $welcomeGift->toArray();
|
$oldValues = $welcomeGift->toArray();
|
||||||
$welcomeGift->update(['status' => 'disabled']);
|
$welcomeGift->update(['status' => 'disabled']);
|
||||||
|
|
||||||
|
|||||||
@ -371,6 +371,12 @@ class MachineController extends Controller
|
|||||||
'product_id' => $item['t060v00'] ?? null,
|
'product_id' => $item['t060v00'] ?? null,
|
||||||
'stock' => isset($item['num']) ? (int) $item['num'] : 0,
|
'stock' => isset($item['num']) ? (int) $item['num'] : 0,
|
||||||
'type' => isset($item['type']) ? (int) $item['type'] : null,
|
'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);
|
}, $legacyData);
|
||||||
|
|
||||||
|
|||||||
@ -20,12 +20,19 @@ class MachineSlot extends Model
|
|||||||
'expiry_date',
|
'expiry_date',
|
||||||
'batch_no',
|
'batch_no',
|
||||||
'is_active',
|
'is_active',
|
||||||
|
'is_locked',
|
||||||
|
'last_app_lock_rev',
|
||||||
|
'last_app_expiry_rev',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'price' => 'decimal:2',
|
'price' => 'decimal:2',
|
||||||
'last_restocked_at' => 'datetime',
|
'last_restocked_at' => 'datetime',
|
||||||
'expiry_date' => 'date:Y-m-d',
|
'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()
|
public function machine()
|
||||||
|
|||||||
@ -338,17 +338,31 @@ class MachineService
|
|||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── 雙向 LWW:鎖定(lock) 與 效期/批號(expiry/batch),各自獨立 rev ──
|
||||||
|
$updateData = array_merge(
|
||||||
|
$updateData,
|
||||||
|
$this->resolveSlotLockLww($existingSlot, $slotData),
|
||||||
|
$this->resolveSlotExpiryLww($existingSlot, $slotData, $actualProductId)
|
||||||
|
);
|
||||||
|
|
||||||
if ($existingSlot) {
|
if ($existingSlot) {
|
||||||
$oldStock = (int) $existingSlot->stock;
|
$oldStock = (int) $existingSlot->stock;
|
||||||
$oldProductId = $existingSlot->product_id;
|
$oldProductId = $existingSlot->product_id;
|
||||||
|
|
||||||
// 檢查是否有實質變動 (Check for actual changes)
|
// 檢查是否有實質變動 (Check for actual changes)
|
||||||
$isStockChanged = ($oldStock !== $newStock);
|
$isStockChanged = ($oldStock !== $newStock);
|
||||||
$isProductChanged = ($oldProductId != $actualProductId); // 這裡用鬆散比對以處理 null/0 的情況
|
$isProductChanged = ($oldProductId != $actualProductId); // 這裡用鬆散比對以處理 null/0 的情況
|
||||||
$isSlotConfigChanged = (string) $existingSlot->type !== (string) $updateData['type']
|
$isSlotConfigChanged = (string) $existingSlot->type !== (string) $updateData['type']
|
||||||
|| (int) $existingSlot->max_stock !== (int) $updateData['max_stock'];
|
|| (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->update($updateData);
|
||||||
$existingSlot->refresh();
|
$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
|
public function syncProductSlotMaxStock(Product $product): void
|
||||||
{
|
{
|
||||||
MachineSlot::query()
|
MachineSlot::query()
|
||||||
@ -523,19 +619,42 @@ class MachineService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 紀錄舊數據以供回滾使用
|
// 紀錄舊數據以供回滾使用
|
||||||
|
$oldExpiry = $slot->expiry_date ? Carbon::parse($slot->expiry_date)->toDateString() : null;
|
||||||
$oldData = [
|
$oldData = [
|
||||||
'stock' => $slot->stock,
|
'stock' => $slot->stock,
|
||||||
'expiry_date' => $slot->expiry_date ? Carbon::parse($slot->expiry_date)->toDateString() : null,
|
'expiry_date' => $oldExpiry,
|
||||||
'batch_no' => $slot->batch_no,
|
'batch_no' => $slot->batch_no,
|
||||||
|
'is_locked' => (bool) $slot->is_locked,
|
||||||
];
|
];
|
||||||
|
|
||||||
// 2. 執行樂觀更新 (Optimistic Update)
|
// 2. 執行樂觀更新 (Optimistic Update):只更新「明確帶上的欄位」,避免鎖定切換誤清效期
|
||||||
$updateData = [
|
$updateData = [];
|
||||||
'expiry_date' => $expiryDate,
|
$normExpiry = $oldExpiry;
|
||||||
'batch_no' => $batchNo,
|
if (array_key_exists('expiry_date', $data)) {
|
||||||
];
|
$normExpiry = $this->normalizeDate($expiryDate);
|
||||||
if ($stock !== null) $updateData['stock'] = (int)$stock;
|
$updateData['expiry_date'] = $normExpiry;
|
||||||
$slot->update($updateData);
|
}
|
||||||
|
$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 流水帳
|
// 3. 若庫存數值有異動,記錄 adjustment 流水帳
|
||||||
if ($stock !== null && (int)$stock !== $oldData['stock']) {
|
if ($stock !== null && (int)$stock !== $oldData['stock']) {
|
||||||
@ -551,6 +670,8 @@ class MachineService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$slot->refresh();
|
||||||
|
|
||||||
// 3. 建立遠端指令紀錄
|
// 3. 建立遠端指令紀錄
|
||||||
$command = RemoteCommand::create([
|
$command = RemoteCommand::create([
|
||||||
'machine_id' => $machine->id,
|
'machine_id' => $machine->id,
|
||||||
@ -561,19 +682,23 @@ class MachineService
|
|||||||
'slot_no' => (string)$slotNo,
|
'slot_no' => (string)$slotNo,
|
||||||
'old' => $oldData,
|
'old' => $oldData,
|
||||||
'new' => [
|
'new' => [
|
||||||
'stock' => $stock !== null ? (int)$stock : $oldData['stock'],
|
'stock' => (int) $slot->stock,
|
||||||
'expiry_date' => $expiryDate ?: null,
|
'expiry_date' => $slot->expiry_date ? $slot->expiry_date->format('Y-m-d') : null,
|
||||||
'batch_no' => $batchNo ?: null,
|
'batch_no' => $slot->batch_no,
|
||||||
|
'is_locked' => (bool) $slot->is_locked,
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 4. 推播 MQTT
|
// 4. 推播 MQTT(一律帶上目前權威的 is_locked 與雙 rev,讓 App 端對齊 synced_rev)
|
||||||
$mqttPayload = [
|
$mqttPayload = [
|
||||||
'slot_no' => $command->payload['slot_no'],
|
'slot_no' => (string) $slotNo,
|
||||||
'stock' => $command->payload['new']['stock'],
|
'stock' => (int) $slot->stock,
|
||||||
'expiry_date' => $command->payload['new']['expiry_date'],
|
'expiry_date' => $slot->expiry_date ? $slot->expiry_date->format('Y-m-d') : null,
|
||||||
'batch_no' => $command->payload['new']['batch_no'] ?? 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(
|
app(\App\Services\Machine\MqttService::class)->pushCommand(
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 貨道「效期 + 鎖定」雙向 LWW 同步所需欄位。
|
||||||
|
*
|
||||||
|
* - machine_slots.is_locked : 貨道是否被「鎖定停售」(與 is_active「是否啟用/配置」語意不同,兩者並存)。
|
||||||
|
* - machine_slots.last_app_lock_rev : 後台已採用的 App 鎖定編輯序號(per-field LWW,避免比較機台/後台兩個時鐘)。
|
||||||
|
* - machine_slots.last_app_expiry_rev: 後台已採用的 App 效期/批號編輯序號。
|
||||||
|
*/
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('machine_slots', function (Blueprint $table) {
|
||||||
|
$table->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']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1050,11 +1050,13 @@
|
|||||||
"Location found!": "Location found!",
|
"Location found!": "Location found!",
|
||||||
"Location not found": "Location not found",
|
"Location not found": "Location not found",
|
||||||
"Lock": "Lock",
|
"Lock": "Lock",
|
||||||
|
"Lock (Pause Sale)": "Lock (Pause Sale)",
|
||||||
"Lock Now": "Lock Now",
|
"Lock Now": "Lock Now",
|
||||||
"Lock Page": "Lock Page",
|
"Lock Page": "Lock Page",
|
||||||
"Lock Page Lock": "Lock Page Lock",
|
"Lock Page Lock": "Lock Page Lock",
|
||||||
"Lock Page Unlock": "Lock Page Unlock",
|
"Lock Page Unlock": "Lock Page Unlock",
|
||||||
"Locked": "Locked",
|
"Locked": "Locked",
|
||||||
|
"Locked (Sale Paused)": "Locked (Sale Paused)",
|
||||||
"Locked Page": "Locked Page",
|
"Locked Page": "Locked Page",
|
||||||
"Log Details": "Log Details",
|
"Log Details": "Log Details",
|
||||||
"Log Time": "Log Time",
|
"Log Time": "Log Time",
|
||||||
@ -1423,6 +1425,7 @@
|
|||||||
"Offline Machines": "Offline Machines",
|
"Offline Machines": "Offline Machines",
|
||||||
"Old": "Old",
|
"Old": "Old",
|
||||||
"Old Value": "Old Value",
|
"Old Value": "Old Value",
|
||||||
|
"On Sale": "On Sale",
|
||||||
"Once": "Once",
|
"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. 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.",
|
"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",
|
"Running Status": "Running Status",
|
||||||
"SYSTEM": "SYSTEM",
|
"SYSTEM": "SYSTEM",
|
||||||
"Safety Stock": "Safety Stock",
|
"Safety Stock": "Safety Stock",
|
||||||
|
"Sale Paused": "Sale Paused",
|
||||||
"Sale Price": "Sale Price",
|
"Sale Price": "Sale Price",
|
||||||
|
"Sale Status": "Sale Status",
|
||||||
"Sales": "Sales",
|
"Sales": "Sales",
|
||||||
"Sales Activity": "Sales Activity",
|
"Sales Activity": "Sales Activity",
|
||||||
"Sales Amount": "Sales Amount",
|
"Sales Amount": "Sales Amount",
|
||||||
|
|||||||
@ -1050,11 +1050,13 @@
|
|||||||
"Location found!": "座標を取得しました!",
|
"Location found!": "座標を取得しました!",
|
||||||
"Location not found": "場所が見つかりません。ランドマーク名(例:花蓮県庁)を使用してみてください",
|
"Location not found": "場所が見つかりません。ランドマーク名(例:花蓮県庁)を使用してみてください",
|
||||||
"Lock": "ロック",
|
"Lock": "ロック",
|
||||||
|
"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 Page": "ロックページ",
|
"Locked Page": "ロックページ",
|
||||||
"Log Details": "Log Details",
|
"Log Details": "Log Details",
|
||||||
"Log Time": "記録時間",
|
"Log Time": "記録時間",
|
||||||
@ -1423,6 +1425,7 @@
|
|||||||
"Offline Machines": "オフライン機器",
|
"Offline Machines": "オフライン機器",
|
||||||
"Old": "Old",
|
"Old": "Old",
|
||||||
"Old Value": "Old Value",
|
"Old Value": "Old Value",
|
||||||
|
"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 Price": "販売価格",
|
"Sale Price": "販売価格",
|
||||||
|
"Sale Status": "販売状態",
|
||||||
"Sales": "販売管理",
|
"Sales": "販売管理",
|
||||||
"Sales Activity": "販売活動",
|
"Sales Activity": "販売活動",
|
||||||
"Sales Amount": "売上金額",
|
"Sales Amount": "売上金額",
|
||||||
|
|||||||
@ -1050,11 +1050,13 @@
|
|||||||
"Location found!": "已成功獲取座標!",
|
"Location found!": "已成功獲取座標!",
|
||||||
"Location not found": "找不到位置,您可以嘗試使用地標名稱(例如:花蓮縣政府)",
|
"Location not found": "找不到位置,您可以嘗試使用地標名稱(例如:花蓮縣政府)",
|
||||||
"Lock": "鎖定",
|
"Lock": "鎖定",
|
||||||
|
"Lock (Pause Sale)": "鎖定(暫停販售)",
|
||||||
"Lock Now": "立即鎖定",
|
"Lock Now": "立即鎖定",
|
||||||
"Lock Page": "頁面鎖定",
|
"Lock Page": "頁面鎖定",
|
||||||
"Lock Page Lock": "機台 APP 鎖定",
|
"Lock Page Lock": "機台 APP 鎖定",
|
||||||
"Lock Page Unlock": "機台 APP 解鎖",
|
"Lock Page Unlock": "機台 APP 解鎖",
|
||||||
"Locked": "尚未解鎖",
|
"Locked": "尚未解鎖",
|
||||||
|
"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": "販售中",
|
||||||
"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 Price": "售價",
|
"Sale Price": "售價",
|
||||||
|
"Sale Status": "銷售狀態",
|
||||||
"Sales": "銷售管理",
|
"Sales": "銷售管理",
|
||||||
"Sales Activity": "銷售活動",
|
"Sales Activity": "銷售活動",
|
||||||
"Sales Amount": "銷售金額",
|
"Sales Amount": "銷售金額",
|
||||||
|
|||||||
@ -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) {
|
getSlotColorClass(slot) {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
if (slot.expiry_date && slot.expiry_date < today) {
|
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'}`;
|
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 details;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
@ -666,12 +710,21 @@
|
|||||||
class="text-xs font-black uppercase tracking-tighter text-slate-800 dark:text-white"
|
class="text-xs font-black uppercase tracking-tighter text-slate-800 dark:text-white"
|
||||||
x-text="slot.slot_no"></span>
|
x-text="slot.slot_no"></span>
|
||||||
</div>
|
</div>
|
||||||
<template x-if="slot.max_stock > 0 && slot.stock <= (slot.max_stock * 0.2)">
|
<div class="flex items-center gap-1.5">
|
||||||
<div
|
<template x-if="slot.is_locked">
|
||||||
class="px-2.5 py-1.5 rounded-xl bg-rose-500 text-white text-[9px] font-black uppercase tracking-widest shadow-lg shadow-rose-500/30 animate-pulse whitespace-nowrap select-none">
|
<div
|
||||||
{{ __('Low') }}
|
class="flex items-center gap-1 px-2.5 py-1.5 rounded-xl bg-rose-500 text-white text-[9px] font-black uppercase tracking-widest shadow-lg shadow-rose-500/30 whitespace-nowrap select-none">
|
||||||
</div>
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>
|
||||||
</template>
|
{{ __('Sale Paused') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="slot.max_stock > 0 && slot.stock <= (slot.max_stock * 0.2)">
|
||||||
|
<div
|
||||||
|
class="px-2.5 py-1.5 rounded-xl bg-rose-500 text-white text-[9px] font-black uppercase tracking-widest shadow-lg shadow-rose-500/30 animate-pulse whitespace-nowrap select-none">
|
||||||
|
{{ __('Low') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Product Image -->
|
<!-- Product Image -->
|
||||||
@ -777,8 +830,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base font-black text-slate-800 dark:text-slate-200"
|
<div class="flex items-center gap-2">
|
||||||
x-text="slot.product?.name || '{{ __('Empty Slot') }}'"></div>
|
<span class="text-base font-black text-slate-800 dark:text-slate-200"
|
||||||
|
x-text="slot.product?.name || '{{ __('Empty Slot') }}'"></span>
|
||||||
|
<template x-if="slot.is_locked">
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-rose-500/10 text-rose-500 text-[10px] font-black uppercase tracking-wider whitespace-nowrap">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>
|
||||||
|
{{ __('Locked (Sale Paused)') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<template x-if="slot.product?.id">
|
<template x-if="slot.product?.id">
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<span class="text-xs font-bold text-slate-400 dark:text-slate-500 font-mono tracking-widest uppercase">{{ __('ID') }}: <span x-text="slot.product.id"></span></span>
|
<span class="text-xs font-bold text-slate-400 dark:text-slate-500 font-mono tracking-widest uppercase">{{ __('ID') }}: <span x-text="slot.product.id"></span></span>
|
||||||
@ -843,7 +904,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="shrink-0 text-right">
|
<div class="shrink-0 text-right">
|
||||||
<span class="text-xs font-black px-2.5 py-1.5 rounded-lg bg-slate-900/10 dark:bg-white/10" x-text="slot.slot_no"></span>
|
<template x-if="slot.is_locked">
|
||||||
|
<span class="inline-flex items-center gap-1 text-[10px] font-black px-2.5 py-1.5 rounded-lg bg-rose-500/10 text-rose-500 uppercase tracking-wider whitespace-nowrap">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>
|
||||||
|
{{ __('Sale Paused') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -966,6 +1032,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Slot Lock (暫停販售):遠端鎖定/解鎖此貨道,與機台現場雙向同步 -->
|
||||||
|
<div
|
||||||
|
class="mt-6 flex items-center justify-between rounded-2xl border border-slate-100 dark:border-slate-800/50 px-5 py-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-black text-slate-500 uppercase tracking-widest">{{
|
||||||
|
__('Sale Status') }}</div>
|
||||||
|
<div class="mt-1 text-xs font-bold"
|
||||||
|
:class="selectedSlot?.is_locked ? 'text-rose-500' : 'text-emerald-500'"
|
||||||
|
x-text="selectedSlot?.is_locked ? '{{ __('Locked (Sale Paused)') }}' : '{{ __('On Sale') }}'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="toggleLock()" :disabled="updating"
|
||||||
|
class="px-6 py-3 rounded-xl font-bold transition disabled:opacity-50"
|
||||||
|
:class="selectedSlot?.is_locked ? 'bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20' : 'bg-rose-500/10 text-rose-600 hover:bg-rose-500/20'"
|
||||||
|
x-text="selectedSlot?.is_locked ? '{{ __('Unlock') }}' : '{{ __('Lock (Pause Sale)') }}'">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer Actions -->
|
<!-- Footer Actions -->
|
||||||
|
|||||||
@ -109,7 +109,7 @@
|
|||||||
<div class="text-base font-extrabold text-slate-800 dark:text-slate-100 tracking-tight">
|
<div class="text-base font-extrabold text-slate-800 dark:text-slate-100 tracking-tight">
|
||||||
{{ $code->name }}</div>
|
{{ $code->name }}</div>
|
||||||
<div class="text-xs font-bold text-slate-400 uppercase tracking-widest">{{
|
<div class="text-xs font-bold text-slate-400 uppercase tracking-widest">{{
|
||||||
$code->machine->name }}</div>
|
$code->machine?->name ?? '-' }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6 whitespace-nowrap">
|
<td class="px-6 py-6 whitespace-nowrap">
|
||||||
@if($code->expires_at)
|
@if($code->expires_at)
|
||||||
@ -241,7 +241,7 @@
|
|||||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
|
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
|
||||||
{{ __('Machine') }}</p>
|
{{ __('Machine') }}</p>
|
||||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">{{
|
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">{{
|
||||||
$code->machine->name }}</p>
|
$code->machine?->name ?? '-' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
|
|||||||
@ -117,15 +117,15 @@
|
|||||||
<td class="px-6 py-6">
|
<td class="px-6 py-6">
|
||||||
<div
|
<div
|
||||||
class="text-base font-extrabold text-slate-800 dark:text-slate-100 tracking-tight">
|
class="text-base font-extrabold text-slate-800 dark:text-slate-100 tracking-tight">
|
||||||
{{ $code->machine->name }}</div>
|
{{ $code->machine?->name ?? '-' }}</div>
|
||||||
<div
|
<div
|
||||||
class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em]">
|
class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em]">
|
||||||
{{
|
{{
|
||||||
$code->machine->serial_no }}</div>
|
$code->machine?->serial_no }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6">
|
<td class="px-6 py-6">
|
||||||
@php
|
@php
|
||||||
$slot = $code->machine->slots->where('slot_no', $code->slot_no)->first();
|
$slot = $code->machine?->slots->where('slot_no', $code->slot_no)->first();
|
||||||
$productName = $slot ? ($slot->product?->name ?? __('Empty')) : __('Empty');
|
$productName = $slot ? ($slot->product?->name ?? __('Empty')) : __('Empty');
|
||||||
@endphp
|
@endphp
|
||||||
<div class="text-sm font-black text-cyan-600 dark:text-cyan-400 mb-0.5">{{
|
<div class="text-sm font-black text-cyan-600 dark:text-cyan-400 mb-0.5">{{
|
||||||
@ -251,7 +251,7 @@
|
|||||||
@endif
|
@endif
|
||||||
<p
|
<p
|
||||||
class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">
|
class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">
|
||||||
{{ $code->machine->name }}
|
{{ $code->machine?->name ?? '-' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -287,7 +287,7 @@
|
|||||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">
|
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">
|
||||||
{{ $code->slot_no }}
|
{{ $code->slot_no }}
|
||||||
@php
|
@php
|
||||||
$slotMobile = $code->machine->slots->where('slot_no',
|
$slotMobile = $code->machine?->slots->where('slot_no',
|
||||||
$code->slot_no)->first();
|
$code->slot_no)->first();
|
||||||
$prodMobile = $slotMobile ? ($slotMobile->product?->name ?? __('Empty')) :
|
$prodMobile = $slotMobile ? ($slotMobile->product?->name ?? __('Empty')) :
|
||||||
__('Empty');
|
__('Empty');
|
||||||
|
|||||||
@ -108,7 +108,7 @@
|
|||||||
<div class="text-base font-extrabold text-slate-800 dark:text-slate-100 tracking-tight">
|
<div class="text-base font-extrabold text-slate-800 dark:text-slate-100 tracking-tight">
|
||||||
{{ $gift->name }}</div>
|
{{ $gift->name }}</div>
|
||||||
<div class="text-xs font-bold text-slate-400 uppercase tracking-widest">{{
|
<div class="text-xs font-bold text-slate-400 uppercase tracking-widest">{{
|
||||||
$gift->machine->name }}</div>
|
$gift->machine?->name ?? '-' }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6 whitespace-nowrap">
|
<td class="px-6 py-6 whitespace-nowrap">
|
||||||
<span class="text-sm font-black text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-500/10 px-3 py-1 rounded-lg border border-indigo-100 dark:border-indigo-500/20">
|
<span class="text-sm font-black text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-500/10 px-3 py-1 rounded-lg border border-indigo-100 dark:border-indigo-500/20">
|
||||||
@ -258,7 +258,7 @@
|
|||||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
|
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
|
||||||
{{ __('Machine') }}</p>
|
{{ __('Machine') }}</p>
|
||||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">
|
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">
|
||||||
{{ $gift->machine->name }}</p>
|
{{ $gift->machine?->name ?? '-' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
|
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
|
||||||
|
|||||||
@ -296,6 +296,12 @@
|
|||||||
<span class="text-sm font-black tracking-tighter text-slate-800 dark:text-white"
|
<span class="text-sm font-black tracking-tighter text-slate-800 dark:text-white"
|
||||||
x-text="slot.slot_no"></span>
|
x-text="slot.slot_no"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<template x-if="slot.is_locked">
|
||||||
|
<div class="absolute top-4 right-5 flex items-center gap-1 px-2.5 py-1.5 rounded-xl bg-rose-500 text-white text-[9px] font-black uppercase tracking-widest shadow-lg shadow-rose-500/30 whitespace-nowrap select-none">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>
|
||||||
|
{{ __('Sale Paused') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="relative w-16 h-16 mb-4 mt-2">
|
<div class="relative w-16 h-16 mb-4 mt-2">
|
||||||
<div
|
<div
|
||||||
@ -364,8 +370,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base font-black text-slate-800 dark:text-slate-200"
|
<div class="flex items-center gap-2">
|
||||||
x-text="slot.product?.name || '{{ __('Empty Slot') }}'"></div>
|
<span class="text-base font-black text-slate-800 dark:text-slate-200"
|
||||||
|
x-text="slot.product?.name || '{{ __('Empty Slot') }}'"></span>
|
||||||
|
<template x-if="slot.is_locked">
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-rose-500/10 text-rose-500 text-[10px] font-black uppercase tracking-wider whitespace-nowrap">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>
|
||||||
|
{{ __('Locked (Sale Paused)') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-x-2.5 gap-y-1 mt-1">
|
<div class="flex flex-wrap items-center gap-x-2.5 gap-y-1 mt-1">
|
||||||
<template x-if="slot.product?.id">
|
<template x-if="slot.product?.id">
|
||||||
<span class="text-xs font-bold text-slate-400 dark:text-slate-500 font-mono tracking-widest uppercase">{{ __('ID') }}: <span x-text="slot.product.id"></span></span>
|
<span class="text-xs font-bold text-slate-400 dark:text-slate-500 font-mono tracking-widest uppercase">{{ __('ID') }}: <span x-text="slot.product.id"></span></span>
|
||||||
@ -435,6 +449,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="shrink-0 text-right">
|
<div class="shrink-0 text-right">
|
||||||
<p class="text-base font-black text-emerald-500" x-text="'$' + Math.floor(slot.product?.price || 0)"></p>
|
<p class="text-base font-black text-emerald-500" x-text="'$' + Math.floor(slot.product?.price || 0)"></p>
|
||||||
|
<template x-if="slot.is_locked">
|
||||||
|
<span class="inline-flex items-center gap-1 mt-1 text-[10px] font-black px-2 py-0.5 rounded-md bg-rose-500/10 text-rose-500 uppercase tracking-wider whitespace-nowrap">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>
|
||||||
|
{{ __('Sale Paused') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -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('/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::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/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::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');
|
Route::post('/{machine}/pricing', [App\Http\Controllers\Admin\MachineController::class, 'updatePricing'])->name('pricing.update');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user