[FIX] backport 至 dev:每機台商品定價 + 機台列表 RWD + 多語系對齊
This commit is contained in:
commit
94a941e100
@ -2,10 +2,15 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Jobs\Product\SendProductSyncCommandJob;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\MachineProductPrice;
|
||||
use App\Models\Product\Product;
|
||||
use App\Models\System\Company;
|
||||
use App\Services\Machine\MachineService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
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)
|
||||
*/
|
||||
|
||||
@ -394,8 +394,8 @@ class MachineController extends Controller
|
||||
{
|
||||
$machine = $request->get('machine');
|
||||
|
||||
// Cache First: Get from cache or rebuild if missing
|
||||
$payload = $catalogService->getPayload($machine->company_id);
|
||||
// 公司基底目錄(快取) + 該機台專屬定價覆蓋(即時套用,見 getMachinePayload)
|
||||
$payload = $catalogService->getMachinePayload($machine);
|
||||
|
||||
return response()->json($payload);
|
||||
}
|
||||
|
||||
@ -54,6 +54,14 @@ class Machine extends Model
|
||||
])) {
|
||||
app(\App\Services\Machine\MachineService::class)->syncMachineSlotMaxStock($machine);
|
||||
}
|
||||
|
||||
// 機台轉移公司:不同公司商品 ID 體系不同,舊的機台專屬定價必須清空,
|
||||
// 避免跨租戶價格錯位與資料污染。批次刪除不觸發 model 事件,故補推一次
|
||||
// B012 同步,讓機台重抓新公司目錄與(已清空的)定價。
|
||||
if ($machine->wasChanged('company_id')) {
|
||||
$machine->productPrices()->delete();
|
||||
\App\Jobs\Product\SendProductSyncCommandJob::dispatch($machine->id, __('Machine company changed'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -401,6 +409,11 @@ class Machine extends Model
|
||||
return $this->hasMany(MachineSlot::class);
|
||||
}
|
||||
|
||||
public function productPrices()
|
||||
{
|
||||
return $this->hasMany(MachineProductPrice::class);
|
||||
}
|
||||
|
||||
public function commands()
|
||||
{
|
||||
return $this->hasMany(RemoteCommand::class);
|
||||
|
||||
58
app/Models/Machine/MachineProductPrice.php
Normal file
58
app/Models/Machine/MachineProductPrice.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use App\Jobs\Product\SendProductSyncCommandJob;
|
||||
use App\Models\Product\Product;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* 機台專屬定價:特定機台 × 特定商品的價格覆蓋。
|
||||
*
|
||||
* - price / member_price 為 NULL 代表沿用全域 products 價。
|
||||
* - 任何新增/修改/刪除都會推送 B012 同步指令給「該台」機台,使其即時重抓目錄。
|
||||
* 目錄覆蓋於 B012 請求當下即時套用(見 ProductCatalogService::getMachinePayload),
|
||||
* 不進公司目錄快取,故改價不需失效公司快取。
|
||||
*/
|
||||
class MachineProductPrice extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'machine_id',
|
||||
'product_id',
|
||||
'price',
|
||||
'member_price',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'price' => 'decimal:2',
|
||||
'member_price' => 'decimal:2',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saved(fn (self $row) => $row->dispatchSync());
|
||||
static::deleted(fn (self $row) => $row->dispatchSync());
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送 B012 同步指令給該機台,讓它即時重抓最新定價。
|
||||
* 控制台/離線判定沿用 SendProductSyncCommandJob 既有邏輯。
|
||||
*/
|
||||
protected function dispatchSync(): void
|
||||
{
|
||||
SendProductSyncCommandJob::dispatch($this->machine_id, __('Machine pricing updated'));
|
||||
}
|
||||
|
||||
public function machine()
|
||||
{
|
||||
return $this->belongsTo(Machine::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Services\Product;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\MachineProductPrice;
|
||||
use App\Models\Product\Product;
|
||||
use App\Models\System\Company;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
@ -76,6 +78,54 @@ class ProductCatalogService
|
||||
Cache::forget($this->getCacheKey($companyId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得「該機台」的商品目錄:公司基底目錄(快取) + 機台專屬定價覆蓋。
|
||||
*
|
||||
* 設計取捨:重的 i18n 目錄留在 per-company 快取;機台覆蓋於請求當下即時套用,
|
||||
* 不另設機台快取。覆蓋只是一次 (machine_id) 的索引查詢,極輕量;且改價僅推「該台」
|
||||
* 重抓,不會造成多台群湧。如此完全避開「機台快取失效 / 公司基底變動連帶失效」的複雜度。
|
||||
*
|
||||
* @param Machine $machine
|
||||
* @return array
|
||||
*/
|
||||
public function getMachinePayload(Machine $machine): array
|
||||
{
|
||||
$payload = $this->getPayload($machine->company_id);
|
||||
|
||||
$overrides = MachineProductPrice::where('machine_id', $machine->id)
|
||||
->get(['product_id', 'price', 'member_price'])
|
||||
->keyBy('product_id');
|
||||
|
||||
if ($overrides->isEmpty()) {
|
||||
return $payload;
|
||||
}
|
||||
|
||||
// PHP 陣列為值複製,array_map 產生新陣列,不會污染 Cache 內的公司基底目錄。
|
||||
$payload['data'] = array_map(function ($item) use ($overrides) {
|
||||
$override = $overrides->get((int) ($item['t060v00'] ?? 0));
|
||||
if (!$override) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
// 機台售價(t063v03=App machinePrice):有覆蓋價就用,否則維持全域售價基底。
|
||||
$sellingPrice = $override->price !== null
|
||||
? (float) $override->price
|
||||
: (float) $item['t063v03'];
|
||||
$item['t063v03'] = $sellingPrice;
|
||||
|
||||
// 機台會員價(t060v30):有覆蓋就用,否則沿用全域會員價;
|
||||
// 一律夾為「不高於機台售價」,避免會員買得比一般人貴。
|
||||
$memberPrice = $override->member_price !== null
|
||||
? (float) $override->member_price
|
||||
: (float) $item['t060v30'];
|
||||
$item['t060v30'] = min($memberPrice, $sellingPrice);
|
||||
|
||||
return $item;
|
||||
}, $payload['data']);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a Product model to the catalog format expected by machines.
|
||||
*
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 機台專屬定價:針對「特定機台 × 特定商品」覆蓋全域 products.price / member_price。
|
||||
*
|
||||
* - 無紀錄(或欄位為 NULL)= 沿用全域商品價(products.price / member_price)。
|
||||
* - B012 下發時,命中的商品會把 t063v03(機台價) / t060v30(會員價) 換成此處的值。
|
||||
* - 粒度為 (machine_id, product_id):App 端價格模型是 product-level,同商品占多貨道共用同一價。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('machine_product_prices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('machine_id')->constrained('machines')->onDelete('cascade');
|
||||
$table->foreignId('product_id')->constrained('products')->onDelete('cascade');
|
||||
$table->decimal('price', 10, 2)->nullable()->comment('機台專屬售價;NULL=用全域 products.price');
|
||||
$table->decimal('member_price', 10, 2)->nullable()->comment('機台專屬會員價;NULL=用全域 member_price(並夾為不高於機台售價)');
|
||||
$table->timestamps();
|
||||
|
||||
// 一台機台對同一商品只有一筆覆蓋價
|
||||
$table->unique(['machine_id', 'product_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('machine_product_prices');
|
||||
}
|
||||
};
|
||||
15
lang/en.json
15
lang/en.json
@ -765,6 +765,7 @@
|
||||
"Failed to get invoice print URL": "Failed to get invoice print URL",
|
||||
"Failed to load permissions": "Failed to load permissions",
|
||||
"Failed to load preview": "Failed to load preview",
|
||||
"Failed to load pricing": "Failed to load pricing",
|
||||
"Failed to load tab content": "Failed to load tab content",
|
||||
"Failed to resolve issues.": "Failed to resolve issues.",
|
||||
"Failed to save permissions.": "Failed to save permissions.",
|
||||
@ -897,6 +898,7 @@
|
||||
"Generate Replenishment Order": "Generate Replenishment Order",
|
||||
"Generate and manage one-time pickup codes for customers": "Generate and manage one-time pickup codes for customers",
|
||||
"Gift Definitions": "Gift Definitions",
|
||||
"Global": "Global",
|
||||
"Global roles accessible by all administrators.": "Global roles accessible by all administrators.",
|
||||
"Go Authorize": "Go Authorize",
|
||||
"Go Back": "Go Back",
|
||||
@ -1020,6 +1022,7 @@
|
||||
"Latitude": "Latitude",
|
||||
"Lease": "Lease",
|
||||
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "Leave blank to deploy immediately, or set a future time to schedule a background release.",
|
||||
"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.",
|
||||
"Leave empty for permanent code": "Leave empty for permanent code",
|
||||
"Leave empty or 0 for permanent code": "Leave empty or 0 for permanent code",
|
||||
"Leave empty to inherit company settings": "Leave empty to inherit company settings",
|
||||
@ -1105,6 +1108,8 @@
|
||||
"Machine Model Settings": "Machine Model Settings",
|
||||
"Machine Name": "Machine Name",
|
||||
"Machine Permissions": "Machine Permissions",
|
||||
"Machine Price": "Machine Price",
|
||||
"Machine Pricing": "Machine Pricing",
|
||||
"Machine Reboot": "Machine Reboot",
|
||||
"Machine Registry": "Machine Registry",
|
||||
"Machine Replenishment": "Machine Replenishment",
|
||||
@ -1123,6 +1128,7 @@
|
||||
"Machine Stock Movements": "Machine Stock Movements",
|
||||
"Machine System Settings": "Machine System Settings",
|
||||
"Machine Utilization": "Machine Utilization",
|
||||
"Machine company changed": "Machine company changed",
|
||||
"Machine created successfully.": "Machine created successfully.",
|
||||
"Machine deleted successfully.": "Machine deleted successfully.",
|
||||
"Machine has reconnected to the server and resumed normal heartbeat.": "Machine has reconnected to the server and resumed normal heartbeat.",
|
||||
@ -1135,6 +1141,8 @@
|
||||
"Machine normal": "Machine normal",
|
||||
"Machine not found": "Machine not found",
|
||||
"Machine permissions updated successfully.": "Machine permissions updated successfully.",
|
||||
"Machine pricing saved and sync command pushed.": "Machine pricing saved and sync command pushed.",
|
||||
"Machine pricing updated": "Machine pricing updated",
|
||||
"Machine settings updated successfully.": "Machine settings updated successfully.",
|
||||
"Machine temperature exceeds limit": "Machine temperature exceeds limit, purchase suspended",
|
||||
"Machine to Warehouse": "Machine to Warehouse",
|
||||
@ -1338,6 +1346,7 @@
|
||||
"No maintenance records found": "No maintenance records found",
|
||||
"No matching logs found": "No matching logs found",
|
||||
"No matching machines": "No matching machines",
|
||||
"No matching products": "No matching products",
|
||||
"No materials available": "No materials available",
|
||||
"No movement records found": "No movement records found",
|
||||
"No movements found": "No movements found",
|
||||
@ -1348,9 +1357,11 @@
|
||||
"No pickup codes found": "No pickup codes found",
|
||||
"No product data matching search criteria": "No product data matching search criteria",
|
||||
"No product record found": "No product record found",
|
||||
"No products available for this company": "No products available for this company",
|
||||
"No products found": "No products found",
|
||||
"No products found in this warehouse": "No products found in this warehouse",
|
||||
"No products found matching your criteria.": "No products found matching your criteria.",
|
||||
"No products on this machine yet": "No products on this machine yet",
|
||||
"No records found": "No records found",
|
||||
"No related detail found": "No related detail found",
|
||||
"No remark": "No remark",
|
||||
@ -1827,6 +1838,7 @@
|
||||
"Sales Today": "Sales Today",
|
||||
"Sales revenue minus product cost net value": "Sales revenue minus product cost net value",
|
||||
"Save": "Save",
|
||||
"Save & Sync": "Save & Sync",
|
||||
"Save Changes": "Save Changes",
|
||||
"Save Config": "Save Config",
|
||||
"Save Material": "Save Material",
|
||||
@ -1834,6 +1846,8 @@
|
||||
"Save Settings": "Save Settings",
|
||||
"Save Version": "Save Version",
|
||||
"Save error:": "Save error:",
|
||||
"Save failed": "Save failed",
|
||||
"Saved": "Saved",
|
||||
"Saved.": "Saved.",
|
||||
"Saving...": "Saving...",
|
||||
"Scale level and access control": "Scale level and access control",
|
||||
@ -2590,6 +2604,7 @@
|
||||
"of items": "items",
|
||||
"optional": "optional",
|
||||
"orders": "orders",
|
||||
"overrides set": "overrides set",
|
||||
"pending": "Pending",
|
||||
"permissions": "Permission Settings",
|
||||
"permissions.accounts": "Account Management",
|
||||
|
||||
15
lang/ja.json
15
lang/ja.json
@ -765,6 +765,7 @@
|
||||
"Failed to get invoice print URL": "領収書印刷URLの取得に失敗しました",
|
||||
"Failed to load permissions": "権限の読み込みに失敗しました",
|
||||
"Failed to load preview": "プレビューの読み込みに失敗しました",
|
||||
"Failed to load pricing": "価格設定の読み込みに失敗しました",
|
||||
"Failed to load tab content": "タブ内容の読み込みに失敗しました",
|
||||
"Failed to resolve issues.": "異常の解消に失敗しました。",
|
||||
"Failed to save permissions.": "権限の保存に失敗しました。",
|
||||
@ -897,6 +898,7 @@
|
||||
"Generate Replenishment Order": "補充伝票生成",
|
||||
"Generate and manage one-time pickup codes for customers": "顧客用のワンタイム受取コードを生成・管理",
|
||||
"Gift Definitions": "ギフト設定",
|
||||
"Global": "グローバル価格",
|
||||
"Global roles accessible by all administrators.": "全管理者がアクセス可能なグローバルロール。",
|
||||
"Go Authorize": "先に授権する",
|
||||
"Go Back": "戻る",
|
||||
@ -1020,6 +1022,7 @@
|
||||
"Latitude": "緯度",
|
||||
"Lease": "リース",
|
||||
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "空欄のままにすると即時配信されます。未来の日時を指定すると背景で自動予約配信されます。",
|
||||
"Leave blank to use the global product price. Member price will be capped at the machine selling price.": "空欄の場合はグローバル商品価格を使用します。会員価格はマシン販売価格を上限に自動調整されます。",
|
||||
"Leave empty for permanent code": "空欄で無期限コードになります",
|
||||
"Leave empty or 0 for permanent code": "空欄または0で無期限コードになります",
|
||||
"Leave empty to inherit company settings": "空白のままにすると会社のグローバル設定を継承します",
|
||||
@ -1105,6 +1108,8 @@
|
||||
"Machine Model Settings": "機器モデル設定",
|
||||
"Machine Name": "機器名",
|
||||
"Machine Permissions": "機器権限",
|
||||
"Machine Price": "マシン価格",
|
||||
"Machine Pricing": "マシン価格設定",
|
||||
"Machine Reboot": "機器アプリ再起動",
|
||||
"Machine Registry": "機器リスト",
|
||||
"Machine Replenishment": "機器補充伝票",
|
||||
@ -1123,6 +1128,7 @@
|
||||
"Machine Stock Movements": "機器在庫変動履歴",
|
||||
"Machine System Settings": "機器システム設定",
|
||||
"Machine Utilization": "機器稼働率",
|
||||
"Machine company changed": "マシンの所属会社が変更されました",
|
||||
"Machine created successfully.": "機器が正常に作成されました。",
|
||||
"Machine deleted successfully.": "機器が正常に削除されました。",
|
||||
"Machine has reconnected to the server and resumed normal heartbeat.": "マシンがサーバーに再接続し、通常のハートビートを再開しました。",
|
||||
@ -1135,6 +1141,8 @@
|
||||
"Machine normal": "デバイス正常",
|
||||
"Machine not found": "機器が見つかりません",
|
||||
"Machine permissions updated successfully.": "機器権限が正常に更新されました。",
|
||||
"Machine pricing saved and sync command pushed.": "マシン価格を保存し、同期コマンドを送信しました。",
|
||||
"Machine pricing updated": "マシン価格を更新しました",
|
||||
"Machine settings updated successfully.": "機器設定が正常に更新されました。",
|
||||
"Machine temperature exceeds limit": "設定温度超過のため購入一時停止",
|
||||
"Machine to Warehouse": "機器から倉庫",
|
||||
@ -1338,6 +1346,7 @@
|
||||
"No maintenance records found": "メンテナンス記録が見つかりません",
|
||||
"No matching logs found": "一致するログが見つかりません",
|
||||
"No matching machines": "一致する機器がありません",
|
||||
"No matching products": "一致する商品がありません",
|
||||
"No materials available": "利用可能な素材がありません",
|
||||
"No movement records found": "変動記録が見つかりません",
|
||||
"No movements found": "変動が見つかりません",
|
||||
@ -1348,9 +1357,11 @@
|
||||
"No pickup codes found": "受取コードが見つかりません",
|
||||
"No product data matching search criteria": "検索条件に一致する商品データはありません",
|
||||
"No product record found": "商品記録が見つかりません",
|
||||
"No products available for this company": "この会社で販売可能な商品がありません",
|
||||
"No products found": "商品が見つかりません",
|
||||
"No products found in this warehouse": "この倉庫に商品は見つかりません",
|
||||
"No products found matching your criteria.": "条件に一致する商品が見つかりません。",
|
||||
"No products on this machine yet": "このマシンにはまだ商品がありません",
|
||||
"No records found": "記録が見つかりません",
|
||||
"No related detail found": "関連する詳細が見つかりません",
|
||||
"No remark": "備考なし",
|
||||
@ -1827,6 +1838,7 @@
|
||||
"Sales Today": "本日の販売数",
|
||||
"Sales revenue minus product cost net value": "販売額から商品コストを差し引いた純利益",
|
||||
"Save": "保存",
|
||||
"Save & Sync": "保存して同期",
|
||||
"Save Changes": "変更を保存",
|
||||
"Save Config": "設定を保存",
|
||||
"Save Material": "素材を保存",
|
||||
@ -1834,6 +1846,8 @@
|
||||
"Save Settings": "設定を保存",
|
||||
"Save Version": "Save Version",
|
||||
"Save error:": "保存エラー:",
|
||||
"Save failed": "保存に失敗しました",
|
||||
"Saved": "保存しました",
|
||||
"Saved.": "保存しました。",
|
||||
"Saving...": "保存中...",
|
||||
"Scale level and access control": "スケールレベルとアクセス制御",
|
||||
@ -2590,6 +2604,7 @@
|
||||
"of items": "件中",
|
||||
"optional": "任意",
|
||||
"orders": "件",
|
||||
"overrides set": "件のマシン価格を設定済み",
|
||||
"pending": "機器受取待機中",
|
||||
"permissions": "権限設定",
|
||||
"permissions.accounts": "アカウント管理",
|
||||
|
||||
@ -765,6 +765,7 @@
|
||||
"Failed to get invoice print URL": "取得發票列印網址失敗",
|
||||
"Failed to load permissions": "載入權限失敗",
|
||||
"Failed to load preview": "載入預覽失敗",
|
||||
"Failed to load pricing": "載入定價失敗",
|
||||
"Failed to load tab content": "載入分頁內容失敗",
|
||||
"Failed to resolve issues.": "排除異常失敗。",
|
||||
"Failed to save permissions.": "無法儲存權限設定。",
|
||||
@ -897,6 +898,7 @@
|
||||
"Generate Replenishment Order": "產生補貨單",
|
||||
"Generate and manage one-time pickup codes for customers": "產生並管理客戶的一次性取貨碼",
|
||||
"Gift Definitions": "禮品設定",
|
||||
"Global": "全域價",
|
||||
"Global roles accessible by all administrators.": "適用於所有管理者的全域角色。",
|
||||
"Go Authorize": "前往授權",
|
||||
"Go Back": "返回",
|
||||
@ -1020,6 +1022,7 @@
|
||||
"Latitude": "緯度",
|
||||
"Lease": "租賃",
|
||||
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "留空將立即部署更新,或設定未來時間由背景自動排程發布。",
|
||||
"Leave blank to use the global product price. Member price will be capped at the machine selling price.": "留空則沿用全域商品價。會員價會自動夾為不高於機台售價。",
|
||||
"Leave empty for permanent code": "留空表示永久有效",
|
||||
"Leave empty or 0 for permanent code": "留空或輸入 0 則為永久代碼",
|
||||
"Leave empty to inherit company settings": "留空則繼承公司全域設定",
|
||||
@ -1105,6 +1108,8 @@
|
||||
"Machine Model Settings": "機台型號設定",
|
||||
"Machine Name": "機台名稱",
|
||||
"Machine Permissions": "機台權限",
|
||||
"Machine Price": "機台價",
|
||||
"Machine Pricing": "機台定價",
|
||||
"Machine Reboot": "機台 APP 重啟",
|
||||
"Machine Registry": "機台清冊",
|
||||
"Machine Replenishment": "機台補貨單",
|
||||
@ -1123,6 +1128,7 @@
|
||||
"Machine Stock Movements": "機台庫存流水帳",
|
||||
"Machine System Settings": "機台系統設定",
|
||||
"Machine Utilization": "機台稼動率",
|
||||
"Machine company changed": "機台所屬公司已變更",
|
||||
"Machine created successfully.": "機台已成功建立。",
|
||||
"Machine deleted successfully.": "機台已成功刪除。",
|
||||
"Machine has reconnected to the server and resumed normal heartbeat.": "機台已重新連線至伺服器並恢復正常心跳。",
|
||||
@ -1135,6 +1141,8 @@
|
||||
"Machine normal": "機台正常",
|
||||
"Machine not found": "找不到機台",
|
||||
"Machine permissions updated successfully.": "機台權限已成功更新。",
|
||||
"Machine pricing saved and sync command pushed.": "機台定價已儲存並推送同步指令。",
|
||||
"Machine pricing updated": "機台定價已更新",
|
||||
"Machine settings updated successfully.": "機台設定已成功更新。",
|
||||
"Machine temperature exceeds limit": "機器超過設定溫度,暫停購買",
|
||||
"Machine to Warehouse": "機台對倉庫",
|
||||
@ -1338,6 +1346,7 @@
|
||||
"No maintenance records found": "找不到維修紀錄",
|
||||
"No matching logs found": "找不到符合條件的日誌",
|
||||
"No matching machines": "查無匹配機台",
|
||||
"No matching products": "找不到符合的商品",
|
||||
"No materials available": "沒有可用的素材",
|
||||
"No movement records found": "查無異動紀錄",
|
||||
"No movements found": "尚無異動紀錄",
|
||||
@ -1348,9 +1357,11 @@
|
||||
"No pickup codes found": "找不到取貨碼",
|
||||
"No product data matching search criteria": "沒有符合搜尋條件的商品資料",
|
||||
"No product record found": "無商品紀錄",
|
||||
"No products available for this company": "此公司尚無可販售商品",
|
||||
"No products found": "未找到商品",
|
||||
"No products found in this warehouse": "此倉庫目前無商品庫存",
|
||||
"No products found matching your criteria.": "找不到符合條件的商品。",
|
||||
"No products on this machine yet": "此機台尚無商品",
|
||||
"No records found": "未找到相關紀錄",
|
||||
"No related detail found": "無關聯詳細資料",
|
||||
"No remark": "尚無備註",
|
||||
@ -1827,6 +1838,7 @@
|
||||
"Sales Today": "今日銷售",
|
||||
"Sales revenue minus product cost net value": "銷售額扣除商品成本淨值",
|
||||
"Save": "儲存",
|
||||
"Save & Sync": "儲存並同步",
|
||||
"Save Changes": "儲存變更",
|
||||
"Save Config": "儲存配置",
|
||||
"Save Material": "儲存素材",
|
||||
@ -1834,6 +1846,8 @@
|
||||
"Save Settings": "儲存設定",
|
||||
"Save Version": "儲存版本",
|
||||
"Save error:": "儲存錯誤:",
|
||||
"Save failed": "儲存失敗",
|
||||
"Saved": "已儲存",
|
||||
"Saved.": "已儲存",
|
||||
"Saving...": "儲存中...",
|
||||
"Scale level and access control": "層級與存取控制",
|
||||
@ -2590,6 +2604,7 @@
|
||||
"of items": "筆項目",
|
||||
"optional": "選填",
|
||||
"orders": "筆",
|
||||
"overrides set": "項已設定機台價",
|
||||
"pending": "等待機台領取",
|
||||
"permissions": "權限設定",
|
||||
"permissions.accounts": "帳號管理",
|
||||
|
||||
@ -720,6 +720,15 @@
|
||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
|
||||
</svg>
|
||||
</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>
|
||||
</td>
|
||||
</tr>
|
||||
@ -821,6 +830,39 @@
|
||||
<p class="text-[9px] font-bold text-slate-400 mt-1">{{ $machine->latest_status_log_time->diffForHumans() }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
|
||||
{{ __('Hardware Status') }}</p>
|
||||
@php $hStatus = $machine->hardware_status; @endphp
|
||||
<div class="flex flex-col gap-1.5">
|
||||
{{-- 下位機 --}}
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full {{
|
||||
$hStatus === 'error' ? 'bg-rose-500 animate-pulse' :
|
||||
($hStatus === 'warning' ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500/50')
|
||||
}}"></span>
|
||||
<span class="text-xs font-black {{
|
||||
$hStatus === 'error' ? 'text-rose-500' :
|
||||
($hStatus === 'warning' ? 'text-amber-500' : 'text-slate-400')
|
||||
}} uppercase tracking-widest">{{ __('Sub-machine') }}</span>
|
||||
</div>
|
||||
{{-- 刷卡機 (信用卡/電子票證/手機支付共用同一台):僅基礎版且啟用刷卡機才顯示 --}}
|
||||
@if($machine->show_card_terminal)
|
||||
@php $ctStatus = $machine->card_terminal_status; @endphp
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full {{
|
||||
$ctStatus === 'error' ? 'bg-rose-500 animate-pulse' :
|
||||
($ctStatus === 'warning' ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500/50')
|
||||
}}"></span>
|
||||
<span class="text-xs font-black {{
|
||||
$ctStatus === 'error' ? 'text-rose-500' :
|
||||
($ctStatus === 'warning' ? 'text-amber-500' : 'text-slate-400')
|
||||
}} uppercase tracking-widest">{{ __('Card Terminal') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<p
|
||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
|
||||
@ -854,6 +896,14 @@
|
||||
</svg>
|
||||
{{ __('Logs') }}
|
||||
</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>
|
||||
@empty
|
||||
|
||||
226
resources/views/admin/machines/pricing.blade.php
Normal file
226
resources/views/admin/machines/pricing.blade.php
Normal file
@ -0,0 +1,226 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-6 pb-28"
|
||||
x-data="machinePricing({{ Js::from($items) }}, '{{ route('admin.machines.pricing.update', $machine) }}')">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center gap-4">
|
||||
<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">
|
||||
<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" />
|
||||
</svg>
|
||||
</a>
|
||||
<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">
|
||||
<span class="p-2 rounded-xl bg-cyan-500/10 dark:bg-cyan-500/20">
|
||||
<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" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ __('Machine Pricing') }}
|
||||
</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">
|
||||
<span class="font-mono text-cyan-600 dark:text-cyan-400 truncate">{{ $machine->serial_no }}</span>
|
||||
<span class="opacity-50">—</span>
|
||||
<span class="truncate">{{ $machine->name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="luxury-card rounded-3xl p-6 sm:p-8 space-y-6">
|
||||
{{-- Hint + Search --}}
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 font-medium leading-relaxed max-w-2xl">
|
||||
{{ __('Leave blank to use the global product price. Member price will be capped at the machine selling price.') }}
|
||||
</p>
|
||||
<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 z-10">
|
||||
<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>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" x-model="search" class="py-2.5 pl-12 pr-6 block w-full luxury-input"
|
||||
placeholder="{{ __('Search products...') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Empty (no products in company) --}}
|
||||
<template x-if="items.length === 0">
|
||||
<div class="text-center py-20 text-slate-400 font-bold uppercase tracking-widest text-sm">
|
||||
{{ __('No products available for this company') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{{-- Table --}}
|
||||
<div x-show="items.length > 0" class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-200/60 dark:border-slate-700/50">
|
||||
<th class="text-left py-3 px-3">{{ __('Product') }}</th>
|
||||
<th class="text-center py-3 px-3 w-40">{{ __('Machine Price') }}</th>
|
||||
<th class="text-center py-3 px-3 w-40">{{ __('Member Price') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="item in filteredItems" :key="item.product_id">
|
||||
<tr class="border-b border-slate-100 dark:border-slate-800/60 hover:bg-slate-50 dark:hover:bg-slate-800/40 transition-colors">
|
||||
{{-- Product --}}
|
||||
<td class="py-3 px-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="w-11 h-11 rounded-xl bg-slate-100 dark:bg-slate-800 overflow-hidden flex-shrink-0 flex items-center justify-center text-slate-300">
|
||||
<template x-if="item.image_url">
|
||||
<img :src="item.image_url" class="w-full h-full object-cover">
|
||||
</template>
|
||||
<template x-if="!item.image_url">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
|
||||
</template>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-black text-slate-800 dark:text-white truncate" x-text="item.name"></div>
|
||||
<div class="text-[11px] font-bold text-slate-400 uppercase tracking-widest">
|
||||
{{ __('Global') }}: $<span x-text="Math.floor(item.global_price)"></span>
|
||||
<template x-if="item.global_member_price !== null">
|
||||
<span> · {{ __('Member') }} $<span x-text="Math.floor(item.global_member_price)"></span></span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{{-- Machine Price --}}
|
||||
<td class="py-3 px-3">
|
||||
<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">
|
||||
<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="M20 12H4" /></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[56px]">
|
||||
<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>
|
||||
</td>
|
||||
{{-- Member Price --}}
|
||||
<td class="py-3 px-3">
|
||||
<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">
|
||||
<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="M20 12H4" /></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[56px]">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="items.length > 0 && filteredItems.length === 0">
|
||||
<tr><td colspan="3" class="text-center py-12 text-slate-400 font-bold uppercase tracking-widest text-sm">{{ __('No matching products') }}</td></tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Sticky Save Bar --}}
|
||||
<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">
|
||||
<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.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">
|
||||
{{ __('Cancel') }}
|
||||
</a>
|
||||
<button type="button" @click="save()" :disabled="saving || items.length === 0"
|
||||
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">
|
||||
<span class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></span>
|
||||
</template>
|
||||
{{ __('Save & Sync') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('machinePricing', (initialItems, saveUrl) => ({
|
||||
// 空字串代表「沿用全域價」,以 placeholder 顯示全域價作為提示
|
||||
items: initialItems.map(p => ({
|
||||
...p,
|
||||
override_price: p.override_price !== null ? p.override_price : '',
|
||||
override_member_price: p.override_member_price !== null ? p.override_member_price : '',
|
||||
})),
|
||||
saveUrl,
|
||||
search: '',
|
||||
saving: false,
|
||||
|
||||
get filteredItems() {
|
||||
const q = this.search.trim().toLowerCase();
|
||||
if (!q) return this.items;
|
||||
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() {
|
||||
return this.items.filter(i =>
|
||||
(i.override_price !== '' && i.override_price !== null) ||
|
||||
(i.override_member_price !== '' && i.override_member_price !== null)
|
||||
).length;
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (this.saving || this.items.length === 0) return;
|
||||
this.saving = true;
|
||||
const payload = this.items.map(p => ({
|
||||
product_id: p.product_id,
|
||||
price: (p.override_price === '' || p.override_price === null) ? null : p.override_price,
|
||||
member_price: (p.override_member_price === '' || p.override_member_price === null) ? null : p.override_member_price,
|
||||
}));
|
||||
try {
|
||||
const res = await fetch(this.saveUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
},
|
||||
body: JSON.stringify({ items: payload }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.success) {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: data.message || '{{ __('Saved') }}', type: 'success' } }));
|
||||
} else {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: data.message || '{{ __('Save failed') }}', type: 'error' } }));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Pricing save error:', e);
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Save failed') }}', type: 'error' } }));
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@ -783,6 +783,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@ -104,6 +104,7 @@
|
||||
'logs' => __('Machine Logs'),
|
||||
'permissions' => __('Machine Permissions'),
|
||||
'utilization' => __('Utilization Rate'),
|
||||
'pricing' => __('Machine Pricing'),
|
||||
'expiry' => __('Expiry Management'),
|
||||
'maintenance' => __('Maintenance Records'),
|
||||
'ui-elements' => __('UI Elements'),
|
||||
|
||||
@ -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('/{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::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}/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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user