From 1685b858f98ad44c9011755214176527827b7f59 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 23 Jun 2026 15:24:29 +0800 Subject: [PATCH 1/2] =?UTF-8?q?[FEAT]=20=E6=AF=8F=E6=A9=9F=E5=8F=B0?= =?UTF-8?q?=E5=95=86=E5=93=81=E5=AE=9A=E5=83=B9=20WIP=20=E6=9A=AB=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增 MachineProductPrice 模型與 machine_product_prices 資料表 migration 2. 機台設定頁新增定價分頁 pricing.blade.php 3. 調整 ProductCatalogService、MachineController、Machine 模型支援每機台價格 4. 補貨單修正前的進度暫存,待後續完成 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../MachineSettingController.php | 90 ++++++++ .../Api/V1/App/MachineController.php | 4 +- app/Models/Machine/Machine.php | 13 ++ app/Models/Machine/MachineProductPrice.php | 58 +++++ .../Product/ProductCatalogService.php | 50 +++++ ...01_create_machine_product_prices_table.php | 35 +++ lang/zh_TW.json | 15 ++ .../machines/partials/tab-machines.blade.php | 14 ++ .../basic-settings/machines/pricing.blade.php | 203 ++++++++++++++++++ .../warehouses/machine-inventory.blade.php | 1 + routes/web.php | 4 + 11 files changed, 485 insertions(+), 2 deletions(-) create mode 100644 app/Models/Machine/MachineProductPrice.php create mode 100644 database/migrations/2026_06_23_000001_create_machine_product_prices_table.php create mode 100644 resources/views/admin/basic-settings/machines/pricing.blade.php diff --git a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php index b007952..042c9c3 100644 --- a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php +++ b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php @@ -3,12 +3,17 @@ namespace App\Http\Controllers\Admin\BasicSettings; use App\Http\Controllers\Admin\AdminController; +use App\Jobs\Product\SendProductSyncCommandJob; use App\Models\Machine\Machine; use App\Models\Machine\MachineModel; +use App\Models\Machine\MachineProductPrice; +use App\Models\Product\Product; use App\Models\System\Company; use App\Models\System\PaymentConfig; use App\Traits\ImageHandler; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Storage; @@ -197,6 +202,91 @@ class MachineSettingController extends AdminController 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.'), + ]); + } + /** * 更新機台詳細參數 */ diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index 02eaebd..18996a4 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -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); } diff --git a/app/Models/Machine/Machine.php b/app/Models/Machine/Machine.php index 369d505..e69ed23 100644 --- a/app/Models/Machine/Machine.php +++ b/app/Models/Machine/Machine.php @@ -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); diff --git a/app/Models/Machine/MachineProductPrice.php b/app/Models/Machine/MachineProductPrice.php new file mode 100644 index 0000000..a38864d --- /dev/null +++ b/app/Models/Machine/MachineProductPrice.php @@ -0,0 +1,58 @@ + '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); + } +} diff --git a/app/Services/Product/ProductCatalogService.php b/app/Services/Product/ProductCatalogService.php index 204db9d..cfdc1e1 100644 --- a/app/Services/Product/ProductCatalogService.php +++ b/app/Services/Product/ProductCatalogService.php @@ -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. * diff --git a/database/migrations/2026_06_23_000001_create_machine_product_prices_table.php b/database/migrations/2026_06_23_000001_create_machine_product_prices_table.php new file mode 100644 index 0000000..817f9b5 --- /dev/null +++ b/database/migrations/2026_06_23_000001_create_machine_product_prices_table.php @@ -0,0 +1,35 @@ +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'); + } +}; diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 0a69bc9..d52a5fa 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -1110,6 +1110,21 @@ "Machine Status List": "機台運行狀態列表", "Machine Stock": "機台庫存", "Machine Stock Movements": "機台庫存流水帳", + "Machine Pricing": "機台定價", + "Machine Price": "機台價", + "Leave blank to use the global product price. Member price will be capped at the machine selling price.": "留空則沿用全域商品價。會員價會自動夾為不高於機台售價。", + "No products on this machine yet": "此機台尚無商品", + "No products available for this company": "此公司尚無可販售商品", + "No matching products": "找不到符合的商品", + "overrides set": "項已設定機台價", + "Global": "全域價", + "Save & Sync": "儲存並同步", + "Failed to load pricing": "載入定價失敗", + "Saved": "已儲存", + "Save failed": "儲存失敗", + "Machine pricing saved and sync command pushed.": "機台定價已儲存並推送同步指令。", + "Machine pricing updated": "機台定價已更新", + "Machine company changed": "機台所屬公司已變更", "Machine System Settings": "機台系統設定", "Machine Utilization": "機台稼動率", "Machine created successfully.": "機台已成功建立。", diff --git a/resources/views/admin/basic-settings/machines/partials/tab-machines.blade.php b/resources/views/admin/basic-settings/machines/partials/tab-machines.blade.php index cea18f6..3fcdbf2 100644 --- a/resources/views/admin/basic-settings/machines/partials/tab-machines.blade.php +++ b/resources/views/admin/basic-settings/machines/partials/tab-machines.blade.php @@ -149,6 +149,13 @@ + + + + + + + + +@endsection + +@section('scripts') + +@endsection diff --git a/resources/views/admin/warehouses/machine-inventory.blade.php b/resources/views/admin/warehouses/machine-inventory.blade.php index 48e9832..805c739 100644 --- a/resources/views/admin/warehouses/machine-inventory.blade.php +++ b/resources/views/admin/warehouses/machine-inventory.blade.php @@ -783,6 +783,7 @@ + @endsection diff --git a/routes/web.php b/routes/web.php index bfa8bd6..641f514 100644 --- a/routes/web.php +++ b/routes/web.php @@ -293,6 +293,10 @@ 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::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) Route::post('/geocode', [App\Http\Controllers\Admin\GeocodingController::class, 'resolve'])->name('geocode'); }); From 3a793978b4fdc03b3955579140bbba700d9eb1ba Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 23 Jun 2026 15:54:16 +0800 Subject: [PATCH 2/2] =?UTF-8?q?[REFACTOR]=20=E6=AF=8F=E6=A9=9F=E5=8F=B0?= =?UTF-8?q?=E5=AE=9A=E5=83=B9=E6=94=B9=E7=BD=AE=E6=96=BC=E6=A9=9F=E5=8F=B0?= =?UTF-8?q?=E4=B8=BB=E6=A8=A1=E7=B5=84=EF=BC=88=E8=87=AA=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E7=A7=BB=E5=87=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../MachineSettingController.php | 90 ------------------- .../Controllers/Admin/MachineController.php | 90 +++++++++++++++++++ .../machines/partials/tab-machines.blade.php | 14 --- .../views/admin/machines/index.blade.php | 17 ++++ .../machines/pricing.blade.php | 63 ++++++++----- routes/web.php | 7 +- 6 files changed, 153 insertions(+), 128 deletions(-) rename resources/views/admin/{basic-settings => }/machines/pricing.blade.php (67%) diff --git a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php index 042c9c3..b007952 100644 --- a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php +++ b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php @@ -3,17 +3,12 @@ namespace App\Http\Controllers\Admin\BasicSettings; use App\Http\Controllers\Admin\AdminController; -use App\Jobs\Product\SendProductSyncCommandJob; use App\Models\Machine\Machine; use App\Models\Machine\MachineModel; -use App\Models\Machine\MachineProductPrice; -use App\Models\Product\Product; use App\Models\System\Company; use App\Models\System\PaymentConfig; use App\Traits\ImageHandler; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\DB; use Illuminate\View\View; use Illuminate\Http\RedirectResponse; 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')); } - /** - * 機台專屬定價頁:列出該機台「所屬公司的完整商品目錄」(非僅貨道商品), - * 可在上貨前先把機台價/會員價訂好。未填=沿用全域 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.'), - ]); - } - /** * 更新機台詳細參數 */ diff --git a/app/Http/Controllers/Admin/MachineController.php b/app/Http/Controllers/Admin/MachineController.php index 6ac1339..fe8d57b 100644 --- a/app/Http/Controllers/Admin/MachineController.php +++ b/app/Http/Controllers/Admin/MachineController.php @@ -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) */ diff --git a/resources/views/admin/basic-settings/machines/partials/tab-machines.blade.php b/resources/views/admin/basic-settings/machines/partials/tab-machines.blade.php index 3fcdbf2..cea18f6 100644 --- a/resources/views/admin/basic-settings/machines/partials/tab-machines.blade.php +++ b/resources/views/admin/basic-settings/machines/partials/tab-machines.blade.php @@ -149,13 +149,6 @@ - - - - - + + + + + @@ -854,6 +863,14 @@ {{ __('Logs') }} + + + + + {{ __('Pricing') }} + @empty diff --git a/resources/views/admin/basic-settings/machines/pricing.blade.php b/resources/views/admin/machines/pricing.blade.php similarity index 67% rename from resources/views/admin/basic-settings/machines/pricing.blade.php rename to resources/views/admin/machines/pricing.blade.php index 9fb71aa..fb35c44 100644 --- a/resources/views/admin/basic-settings/machines/pricing.blade.php +++ b/resources/views/admin/machines/pricing.blade.php @@ -2,11 +2,11 @@ @section('content')
+ x-data="machinePricing({{ Js::from($items) }}, '{{ route('admin.machines.pricing.update', $machine) }}')"> {{-- Header --}}
- @@ -14,15 +14,15 @@

- - + + {{ __('Machine Pricing') }}

- {{ $machine->serial_no }} + {{ $machine->serial_no }} {{ $machine->name }}
@@ -36,7 +36,7 @@ {{ __('Leave blank to use the global product price. Member price will be capped at the machine selling price.') }}

- + @@ -91,22 +91,36 @@ {{-- Machine Price --}} -
- $ - +
+ +
+ +
+
{{-- Member Price --}} -
- $ - +
+ +
+ +
+
@@ -123,12 +137,12 @@
- {{ __('Cancel') }}