[REFACTOR] 每機台定價改置於機台主模組(自基本設定移出)
1. 路由 /{machine}/pricing、/{machine}/pricing (POST) 由 BasicSettings\MachineSettingController 移至 MachineController
2. pricing / updatePricing 方法搬遷至 Admin\MachineController
3. 視圖由 basic-settings/machines/pricing.blade.php 移至 machines/pricing.blade.php
4. 機台列表頁 index.blade.php 新增定價入口;移除基本設定 tab-machines.blade.php 內的舊入口
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1685b858f9
commit
3a793978b4
@ -3,17 +3,12 @@
|
|||||||
namespace App\Http\Controllers\Admin\BasicSettings;
|
namespace App\Http\Controllers\Admin\BasicSettings;
|
||||||
|
|
||||||
use App\Http\Controllers\Admin\AdminController;
|
use App\Http\Controllers\Admin\AdminController;
|
||||||
use App\Jobs\Product\SendProductSyncCommandJob;
|
|
||||||
use App\Models\Machine\Machine;
|
use App\Models\Machine\Machine;
|
||||||
use App\Models\Machine\MachineModel;
|
use App\Models\Machine\MachineModel;
|
||||||
use App\Models\Machine\MachineProductPrice;
|
|
||||||
use App\Models\Product\Product;
|
|
||||||
use App\Models\System\Company;
|
use App\Models\System\Company;
|
||||||
use App\Models\System\PaymentConfig;
|
use App\Models\System\PaymentConfig;
|
||||||
use App\Traits\ImageHandler;
|
use App\Traits\ImageHandler;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
@ -202,91 +197,6 @@ class MachineSettingController extends AdminController
|
|||||||
return view('admin.basic-settings.machines.edit', compact('machine', 'models', 'paymentConfigs', 'companies'));
|
return view('admin.basic-settings.machines.edit', compact('machine', 'models', 'paymentConfigs', 'companies'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 機台專屬定價頁:列出該機台「所屬公司的完整商品目錄」(非僅貨道商品),
|
|
||||||
* 可在上貨前先把機台價/會員價訂好。未填=沿用全域 products 價。
|
|
||||||
*/
|
|
||||||
public function pricing(Machine $machine): View
|
|
||||||
{
|
|
||||||
$products = Product::where('company_id', $machine->company_id)
|
|
||||||
->active()
|
|
||||||
->orderBy('name')
|
|
||||||
->get(['id', 'name', 'image_url', 'barcode', 'price', 'member_price']);
|
|
||||||
|
|
||||||
$overrides = $machine->productPrices()->get()->keyBy('product_id');
|
|
||||||
|
|
||||||
$items = $products->map(function (Product $p) use ($overrides) {
|
|
||||||
$ov = $overrides->get($p->id);
|
|
||||||
return [
|
|
||||||
'product_id' => $p->id,
|
|
||||||
'name' => $p->name,
|
|
||||||
'image_url' => $p->image_url,
|
|
||||||
'barcode' => $p->barcode,
|
|
||||||
'global_price' => (float) $p->price,
|
|
||||||
'global_member_price' => $p->member_price !== null ? (float) $p->member_price : null,
|
|
||||||
'override_price' => $ov && $ov->price !== null ? (float) $ov->price : null,
|
|
||||||
'override_member_price' => $ov && $ov->member_price !== null ? (float) $ov->member_price : null,
|
|
||||||
];
|
|
||||||
})->values();
|
|
||||||
|
|
||||||
return view('admin.basic-settings.machines.pricing', compact('machine', 'items'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批次儲存機台專屬定價。
|
|
||||||
*
|
|
||||||
* - price/member_price 皆空 → 刪除覆蓋,回到全域價。
|
|
||||||
* - 任一有值 → 建立/更新覆蓋;會員價於 B012 下發時自動夾為不高於機台售價。
|
|
||||||
* - 用 quiet 寫入避免逐筆觸發 observer 重複推送,最後統一推一次 B012。
|
|
||||||
* - 商品須屬於該機台公司(跨租戶防護)。
|
|
||||||
*/
|
|
||||||
public function updatePricing(Request $request, Machine $machine): \Illuminate\Http\JsonResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'items' => 'required|array',
|
|
||||||
'items.*.product_id' => 'required|integer|exists:products,id',
|
|
||||||
'items.*.price' => 'nullable|numeric|min:1|max:99999999',
|
|
||||||
'items.*.member_price' => 'nullable|numeric|min:1|max:99999999',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$allowedProductIds = Product::where('company_id', $machine->company_id)
|
|
||||||
->pluck('id')
|
|
||||||
->flip();
|
|
||||||
|
|
||||||
DB::transaction(function () use ($validated, $machine, $allowedProductIds) {
|
|
||||||
foreach ($validated['items'] as $item) {
|
|
||||||
$productId = (int) $item['product_id'];
|
|
||||||
if (!$allowedProductIds->has($productId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$price = isset($item['price']) && $item['price'] !== '' ? (float) $item['price'] : null;
|
|
||||||
$memberPrice = isset($item['member_price']) && $item['member_price'] !== '' ? (float) $item['member_price'] : null;
|
|
||||||
|
|
||||||
if ($price === null && $memberPrice === null) {
|
|
||||||
$machine->productPrices()->where('product_id', $productId)->delete();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = MachineProductPrice::firstOrNew([
|
|
||||||
'machine_id' => $machine->id,
|
|
||||||
'product_id' => $productId,
|
|
||||||
]);
|
|
||||||
$row->price = $price;
|
|
||||||
$row->member_price = $memberPrice;
|
|
||||||
$row->saveQuietly();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 統一推一次 B012 同步給該機台(覆蓋於請求當下即時套用,無需失效公司快取)
|
|
||||||
SendProductSyncCommandJob::dispatch($machine->id, __('Machine pricing updated'), Auth::id());
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => __('Machine pricing saved and sync command pushed.'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新機台詳細參數
|
* 更新機台詳細參數
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -2,10 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Jobs\Product\SendProductSyncCommandJob;
|
||||||
use App\Models\Machine\Machine;
|
use App\Models\Machine\Machine;
|
||||||
|
use App\Models\Machine\MachineProductPrice;
|
||||||
|
use App\Models\Product\Product;
|
||||||
use App\Models\System\Company;
|
use App\Models\System\Company;
|
||||||
use App\Services\Machine\MachineService;
|
use App\Services\Machine\MachineService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class MachineController extends AdminController
|
class MachineController extends AdminController
|
||||||
@ -257,6 +262,91 @@ class MachineController extends AdminController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 機台專屬定價頁:列出該機台「所屬公司的完整商品目錄」(非僅貨道商品),
|
||||||
|
* 可在上貨前先把機台價/會員價訂好。未填=沿用全域 products 價。
|
||||||
|
*/
|
||||||
|
public function pricing(Machine $machine): View
|
||||||
|
{
|
||||||
|
$products = Product::where('company_id', $machine->company_id)
|
||||||
|
->active()
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'image_url', 'barcode', 'price', 'member_price']);
|
||||||
|
|
||||||
|
$overrides = $machine->productPrices()->get()->keyBy('product_id');
|
||||||
|
|
||||||
|
$items = $products->map(function (Product $p) use ($overrides) {
|
||||||
|
$ov = $overrides->get($p->id);
|
||||||
|
return [
|
||||||
|
'product_id' => $p->id,
|
||||||
|
'name' => $p->name,
|
||||||
|
'image_url' => $p->image_url,
|
||||||
|
'barcode' => $p->barcode,
|
||||||
|
'global_price' => (float) $p->price,
|
||||||
|
'global_member_price' => $p->member_price !== null ? (float) $p->member_price : null,
|
||||||
|
'override_price' => $ov && $ov->price !== null ? (float) $ov->price : null,
|
||||||
|
'override_member_price' => $ov && $ov->member_price !== null ? (float) $ov->member_price : null,
|
||||||
|
];
|
||||||
|
})->values();
|
||||||
|
|
||||||
|
return view('admin.machines.pricing', compact('machine', 'items'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批次儲存機台專屬定價。
|
||||||
|
*
|
||||||
|
* - price/member_price 皆空 → 刪除覆蓋,回到全域價。
|
||||||
|
* - 任一有值 → 建立/更新覆蓋;會員價於 B012 下發時自動夾為不高於機台售價。
|
||||||
|
* - 用 quiet 寫入避免逐筆觸發 observer 重複推送,最後統一推一次 B012。
|
||||||
|
* - 商品須屬於該機台公司(跨租戶防護)。
|
||||||
|
*/
|
||||||
|
public function updatePricing(Request $request, Machine $machine): \Illuminate\Http\JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'items' => 'required|array',
|
||||||
|
'items.*.product_id' => 'required|integer|exists:products,id',
|
||||||
|
'items.*.price' => 'nullable|numeric|min:1|max:99999999',
|
||||||
|
'items.*.member_price' => 'nullable|numeric|min:1|max:99999999',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$allowedProductIds = Product::where('company_id', $machine->company_id)
|
||||||
|
->pluck('id')
|
||||||
|
->flip();
|
||||||
|
|
||||||
|
DB::transaction(function () use ($validated, $machine, $allowedProductIds) {
|
||||||
|
foreach ($validated['items'] as $item) {
|
||||||
|
$productId = (int) $item['product_id'];
|
||||||
|
if (!$allowedProductIds->has($productId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$price = isset($item['price']) && $item['price'] !== '' ? (float) $item['price'] : null;
|
||||||
|
$memberPrice = isset($item['member_price']) && $item['member_price'] !== '' ? (float) $item['member_price'] : null;
|
||||||
|
|
||||||
|
if ($price === null && $memberPrice === null) {
|
||||||
|
$machine->productPrices()->where('product_id', $productId)->delete();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = MachineProductPrice::firstOrNew([
|
||||||
|
'machine_id' => $machine->id,
|
||||||
|
'product_id' => $productId,
|
||||||
|
]);
|
||||||
|
$row->price = $price;
|
||||||
|
$row->member_price = $memberPrice;
|
||||||
|
$row->saveQuietly();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 統一推一次 B012 同步給該機台(覆蓋於請求當下即時套用,無需失效公司快取)
|
||||||
|
SendProductSyncCommandJob::dispatch($machine->id, __('Machine pricing updated'), Auth::id());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('Machine pricing saved and sync command pushed.'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取得機台統計數據 (AJAX)
|
* 取得機台統計數據 (AJAX)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -149,13 +149,6 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('admin.basic-settings.machines.pricing', $machine) }}"
|
|
||||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 dark:hover:bg-amber-500/10 border border-transparent hover:border-amber-500/20 transition-all"
|
|
||||||
title="{{ __('Machine Pricing') }}">
|
|
||||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<button
|
<button
|
||||||
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls', 'key_no'])))"
|
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls', 'key_no'])))"
|
||||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all"
|
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all"
|
||||||
@ -263,13 +256,6 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('admin.basic-settings.machines.pricing', $machine) }}"
|
|
||||||
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold hover:bg-amber-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50"
|
|
||||||
title="{{ __('Machine Pricing') }}">
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<button
|
<button
|
||||||
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls', 'key_no'])))"
|
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls', 'key_no'])))"
|
||||||
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50"
|
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50"
|
||||||
|
|||||||
@ -720,6 +720,15 @@
|
|||||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
|
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<a href="{{ route('admin.machines.pricing', $machine) }}"
|
||||||
|
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 dark:hover:bg-amber-500/10 border border-transparent hover:border-amber-500/20 transition-all duration-200"
|
||||||
|
title="{{ __('Machine Pricing') }}">
|
||||||
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -854,6 +863,14 @@
|
|||||||
</svg>
|
</svg>
|
||||||
{{ __('Logs') }}
|
{{ __('Logs') }}
|
||||||
</button>
|
</button>
|
||||||
|
<a href="{{ route('admin.machines.pricing', $machine) }}"
|
||||||
|
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold text-xs hover:bg-amber-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||||
|
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
{{ __('Pricing') }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
|
|||||||
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="space-y-6 pb-28"
|
<div class="space-y-6 pb-28"
|
||||||
x-data="machinePricing({{ Js::from($items) }}, '{{ route('admin.basic-settings.machines.pricing.update', $machine) }}')">
|
x-data="machinePricing({{ Js::from($items) }}, '{{ route('admin.machines.pricing.update', $machine) }}')">
|
||||||
|
|
||||||
{{-- Header --}}
|
{{-- Header --}}
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a href="{{ route('admin.basic-settings.machines.index') }}"
|
<a href="{{ route('admin.machines.index') }}"
|
||||||
class="p-2.5 rounded-xl bg-white dark:bg-slate-900 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all border border-slate-200/50 dark:border-slate-700/50 shadow-sm hover:shadow-md active:scale-95">
|
class="p-2.5 rounded-xl bg-white dark:bg-slate-900 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all border border-slate-200/50 dark:border-slate-700/50 shadow-sm hover:shadow-md active:scale-95">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
||||||
@ -14,15 +14,15 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight flex items-center gap-3">
|
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight flex items-center gap-3">
|
||||||
<span class="p-2 rounded-xl bg-amber-500/10 dark:bg-amber-500/20">
|
<span class="p-2 rounded-xl bg-cyan-500/10 dark:bg-cyan-500/20">
|
||||||
<svg class="w-6 h-6 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
<svg class="w-6 h-6 text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
{{ __('Machine Pricing') }}
|
{{ __('Machine Pricing') }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="mt-2 flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest overflow-hidden">
|
<div class="mt-2 flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest overflow-hidden">
|
||||||
<span class="font-mono text-amber-600 dark:text-amber-400 truncate">{{ $machine->serial_no }}</span>
|
<span class="font-mono text-cyan-600 dark:text-cyan-400 truncate">{{ $machine->serial_no }}</span>
|
||||||
<span class="opacity-50">—</span>
|
<span class="opacity-50">—</span>
|
||||||
<span class="truncate">{{ $machine->name }}</span>
|
<span class="truncate">{{ $machine->name }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -36,7 +36,7 @@
|
|||||||
{{ __('Leave blank to use the global product price. Member price will be capped at the machine selling price.') }}
|
{{ __('Leave blank to use the global product price. Member price will be capped at the machine selling price.') }}
|
||||||
</p>
|
</p>
|
||||||
<div class="relative w-full sm:w-72 flex-shrink-0">
|
<div class="relative w-full sm:w-72 flex-shrink-0">
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||||
<svg class="h-4 w-4 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
<svg class="h-4 w-4 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
@ -91,22 +91,36 @@
|
|||||||
</td>
|
</td>
|
||||||
{{-- Machine Price --}}
|
{{-- Machine Price --}}
|
||||||
<td class="py-3 px-3">
|
<td class="py-3 px-3">
|
||||||
<div class="relative">
|
<div class="flex items-center h-12 rounded-xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all overflow-hidden">
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3 text-slate-400 font-bold pointer-events-none">$</span>
|
<button type="button" @click="bump(item, 'override_price', -1)" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||||
<input type="number" min="1" step="1" inputmode="numeric"
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4" /></svg>
|
||||||
x-model="item.override_price"
|
</button>
|
||||||
:placeholder="Math.floor(item.global_price)"
|
<div class="flex-1 min-w-[56px]">
|
||||||
class="luxury-input py-2 pl-7 pr-3 w-full text-sm font-bold text-center">
|
<input type="number" min="1" step="1" inputmode="numeric"
|
||||||
|
x-model="item.override_price"
|
||||||
|
:placeholder="Math.floor(item.global_price)"
|
||||||
|
class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="bump(item, 'override_price', 1)" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||||
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" /></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{{-- Member Price --}}
|
{{-- Member Price --}}
|
||||||
<td class="py-3 px-3">
|
<td class="py-3 px-3">
|
||||||
<div class="relative">
|
<div class="flex items-center h-12 rounded-xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all overflow-hidden">
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3 text-slate-400 font-bold pointer-events-none">$</span>
|
<button type="button" @click="bump(item, 'override_member_price', -1)" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||||
<input type="number" min="1" step="1" inputmode="numeric"
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4" /></svg>
|
||||||
x-model="item.override_member_price"
|
</button>
|
||||||
:placeholder="item.global_member_price !== null ? Math.floor(item.global_member_price) : '—'"
|
<div class="flex-1 min-w-[56px]">
|
||||||
class="luxury-input py-2 pl-7 pr-3 w-full text-sm font-bold text-center">
|
<input type="number" min="1" step="1" inputmode="numeric"
|
||||||
|
x-model="item.override_member_price"
|
||||||
|
:placeholder="item.global_member_price !== null ? Math.floor(item.global_member_price) : '—'"
|
||||||
|
class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="bump(item, 'override_member_price', 1)" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||||
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" /></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -123,12 +137,12 @@
|
|||||||
<div class="fixed bottom-0 inset-x-0 sm:left-auto sm:right-8 sm:bottom-8 z-40 flex justify-end px-4 sm:px-0">
|
<div class="fixed bottom-0 inset-x-0 sm:left-auto sm:right-8 sm:bottom-8 z-40 flex justify-end px-4 sm:px-0">
|
||||||
<div class="w-full sm:w-auto bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl border border-slate-200 dark:border-white/10 rounded-2xl shadow-2xl px-5 py-4 flex items-center justify-end gap-3">
|
<div class="w-full sm:w-auto bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl border border-slate-200 dark:border-white/10 rounded-2xl shadow-2xl px-5 py-4 flex items-center justify-end gap-3">
|
||||||
<span class="text-xs font-bold text-slate-400 uppercase tracking-widest mr-auto sm:mr-2" x-text="overrideCount + ' {{ __('overrides set') }}'"></span>
|
<span class="text-xs font-bold text-slate-400 uppercase tracking-widest mr-auto sm:mr-2" x-text="overrideCount + ' {{ __('overrides set') }}'"></span>
|
||||||
<a href="{{ route('admin.basic-settings.machines.index') }}"
|
<a href="{{ route('admin.machines.index') }}"
|
||||||
class="px-5 py-2.5 rounded-xl text-sm font-black text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 uppercase tracking-widest transition-colors">
|
class="px-5 py-2.5 rounded-xl text-sm font-black text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 uppercase tracking-widest transition-colors">
|
||||||
{{ __('Cancel') }}
|
{{ __('Cancel') }}
|
||||||
</a>
|
</a>
|
||||||
<button type="button" @click="save()" :disabled="saving || items.length === 0"
|
<button type="button" @click="save()" :disabled="saving || items.length === 0"
|
||||||
class="px-6 py-2.5 rounded-xl text-sm font-black bg-amber-500 text-white shadow-lg shadow-amber-500/25 hover:shadow-amber-500/40 hover:-translate-y-0.5 active:translate-y-0 transition-all uppercase tracking-widest disabled:opacity-40 disabled:pointer-events-none flex items-center gap-2">
|
class="px-6 py-2.5 rounded-xl text-sm font-black bg-cyan-500 text-white shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 hover:-translate-y-0.5 active:translate-y-0 transition-all uppercase tracking-widest disabled:opacity-40 disabled:pointer-events-none flex items-center gap-2">
|
||||||
<template x-if="saving">
|
<template x-if="saving">
|
||||||
<span class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></span>
|
<span class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></span>
|
||||||
</template>
|
</template>
|
||||||
@ -159,6 +173,15 @@ document.addEventListener('alpine:init', () => {
|
|||||||
return this.items.filter(i => (i.name || '').toLowerCase().includes(q) || String(i.barcode || '').toLowerCase().includes(q));
|
return this.items.filter(i => (i.name || '').toLowerCase().includes(q) || String(i.barcode || '').toLowerCase().includes(q));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// +/- 步進:欄位為空(沿用全域)時,從全域價起跳;最低 1(與後端 min:1 一致)
|
||||||
|
bump(item, field, delta) {
|
||||||
|
const base = field === 'override_member_price'
|
||||||
|
? (item.global_member_price !== null ? item.global_member_price : item.global_price)
|
||||||
|
: item.global_price;
|
||||||
|
const cur = (item[field] === '' || item[field] === null) ? base : parseInt(item[field]);
|
||||||
|
item[field] = Math.max(1, (parseInt(cur) || base) + delta);
|
||||||
|
},
|
||||||
|
|
||||||
get overrideCount() {
|
get overrideCount() {
|
||||||
return this.items.filter(i =>
|
return this.items.filter(i =>
|
||||||
(i.override_price !== '' && i.override_price !== null) ||
|
(i.override_price !== '' && i.override_price !== null) ||
|
||||||
@ -68,6 +68,9 @@ 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::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::get('/{machine}/logs-ajax', [App\Http\Controllers\Admin\MachineController::class, 'logsAjax'])->name('logs-ajax');
|
Route::get('/{machine}/logs-ajax', [App\Http\Controllers\Admin\MachineController::class, 'logsAjax'])->name('logs-ajax');
|
||||||
Route::get('/{machine}/temperature-ajax', [App\Http\Controllers\Admin\MachineController::class, 'temperatureAjax'])->name('temperature-ajax');
|
Route::get('/{machine}/temperature-ajax', [App\Http\Controllers\Admin\MachineController::class, 'temperatureAjax'])->name('temperature-ajax');
|
||||||
Route::get('/{machine}/ambient-temperature-ajax', [App\Http\Controllers\Admin\MachineController::class, 'ambientTemperatureAjax'])->name('ambient-temperature-ajax');
|
Route::get('/{machine}/ambient-temperature-ajax', [App\Http\Controllers\Admin\MachineController::class, 'ambientTemperatureAjax'])->name('ambient-temperature-ajax');
|
||||||
@ -293,10 +296,6 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix
|
|||||||
Route::patch('/{machine}/update-system-settings', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'updateSystemSettings'])->name('update-system-settings');
|
Route::patch('/{machine}/update-system-settings', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'updateSystemSettings'])->name('update-system-settings');
|
||||||
Route::post('/{machine}/sync-settings', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'syncSettings'])->name('sync-settings');
|
Route::post('/{machine}/sync-settings', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'syncSettings'])->name('sync-settings');
|
||||||
|
|
||||||
// 機台專屬定價(獨立頁,列全公司目錄,可上貨前先定價)
|
|
||||||
Route::get('/{machine}/pricing', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'pricing'])->name('pricing');
|
|
||||||
Route::post('/{machine}/pricing', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'updatePricing'])->name('pricing.update');
|
|
||||||
|
|
||||||
// 地址轉座標 (Geocoding Proxy)
|
// 地址轉座標 (Geocoding Proxy)
|
||||||
Route::post('/geocode', [App\Http\Controllers\Admin\GeocodingController::class, 'resolve'])->name('geocode');
|
Route::post('/geocode', [App\Http\Controllers\Admin\GeocodingController::class, 'resolve'])->name('geocode');
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user