From 1685b858f98ad44c9011755214176527827b7f59 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 23 Jun 2026 15:24:29 +0800 Subject: [PATCH 1/5] =?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/5] =?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') }}
+
+

+ {{ __('Hardware Status') }}

+ @php $hStatus = $machine->hardware_status; @endphp +
+ {{-- 下位機 --}} +
+ + {{ __('Sub-machine') }} +
+ {{-- 刷卡機 (信用卡/電子票證/手機支付共用同一台):僅基礎版且啟用刷卡機才顯示 --}} + @if($machine->show_card_terminal) + @php $ctStatus = $machine->card_terminal_status; @endphp +
+ + {{ __('Card Terminal') }} +
+ @endif +
+

From c5a3f9911318d97ecd585d8b03d2d48e18a2c626 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 23 Jun 2026 16:23:29 +0800 Subject: [PATCH 5/5] =?UTF-8?q?[FIX]=20=E6=AF=8F=E6=A9=9F=E5=8F=B0?= =?UTF-8?q?=E5=AE=9A=E5=83=B9=E5=A4=9A=E8=AA=9E=E7=B3=BB=E4=B8=89=E8=AA=9E?= =?UTF-8?q?=E5=B0=8D=E9=BD=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. en.json、ja.json 補齊定價功能 15 個新 key(原本只有 zh_TW 有) 2. zh_TW.json 依 ksort 重排,三語系 key 集合 100% 對齊(各 2620) 3. 斜線不逸出,JSON_UNESCAPED_SLASHES/UNICODE 格式 Co-Authored-By: Claude Opus 4.8 (1M context) --- lang/en.json | 15 +++++++++++++++ lang/ja.json | 15 +++++++++++++++ lang/zh_TW.json | 30 +++++++++++++++--------------- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/lang/en.json b/lang/en.json index e583de5..d14532b 100644 --- a/lang/en.json +++ b/lang/en.json @@ -757,6 +757,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.", @@ -889,6 +890,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", @@ -1009,6 +1011,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", @@ -1094,6 +1097,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", @@ -1112,6 +1117,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.", @@ -1124,6 +1130,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", @@ -1325,6 +1333,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", @@ -1334,9 +1343,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", @@ -1806,6 +1817,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", @@ -1813,6 +1825,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", @@ -2562,6 +2576,7 @@ "of": "of", "of items": "items", "orders": "orders", + "overrides set": "overrides set", "pending": "Pending", "permissions": "Permission Settings", "permissions.accounts": "Account Management", diff --git a/lang/ja.json b/lang/ja.json index 34e1ce9..befd334 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -757,6 +757,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.": "権限の保存に失敗しました。", @@ -889,6 +890,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": "戻る", @@ -1009,6 +1011,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": "空白のままにすると会社のグローバル設定を継承します", @@ -1094,6 +1097,8 @@ "Machine Model Settings": "機器モデル設定", "Machine Name": "機器名", "Machine Permissions": "機器権限", + "Machine Price": "マシン価格", + "Machine Pricing": "マシン価格設定", "Machine Reboot": "機器アプリ再起動", "Machine Registry": "機器リスト", "Machine Replenishment": "機器補充伝票", @@ -1112,6 +1117,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.": "マシンがサーバーに再接続し、通常のハートビートを再開しました。", @@ -1124,6 +1130,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": "機器から倉庫", @@ -1325,6 +1333,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": "変動が見つかりません", @@ -1334,9 +1343,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": "備考なし", @@ -1806,6 +1817,7 @@ "Sales Today": "本日の販売数", "Sales revenue minus product cost net value": "販売額から商品コストを差し引いた純利益", "Save": "保存", + "Save & Sync": "保存して同期", "Save Changes": "変更を保存", "Save Config": "設定を保存", "Save Material": "素材を保存", @@ -1813,6 +1825,8 @@ "Save Settings": "設定を保存", "Save Version": "Save Version", "Save error:": "保存エラー:", + "Save failed": "保存に失敗しました", + "Saved": "保存しました", "Saved.": "保存しました。", "Saving...": "保存中...", "Scale level and access control": "スケールレベルとアクセス制御", @@ -2562,6 +2576,7 @@ "of": "/全", "of items": "件中", "orders": "件", + "overrides set": "件のマシン価格を設定済み", "pending": "機器受取待機中", "permissions": "権限設定", "permissions.accounts": "アカウント管理", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index d52a5fa..f4b9749 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -757,6 +757,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.": "無法儲存權限設定。", @@ -889,6 +890,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": "返回", @@ -1009,6 +1011,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": "留空則繼承公司全域設定", @@ -1094,6 +1097,8 @@ "Machine Model Settings": "機台型號設定", "Machine Name": "機台名稱", "Machine Permissions": "機台權限", + "Machine Price": "機台價", + "Machine Pricing": "機台定價", "Machine Reboot": "機台 APP 重啟", "Machine Registry": "機台清冊", "Machine Replenishment": "機台補貨單", @@ -1110,23 +1115,9 @@ "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 company changed": "機台所屬公司已變更", "Machine created successfully.": "機台已成功建立。", "Machine deleted successfully.": "機台已成功刪除。", "Machine has reconnected to the server and resumed normal heartbeat.": "機台已重新連線至伺服器並恢復正常心跳。", @@ -1139,6 +1130,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": "機台對倉庫", @@ -1340,6 +1333,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": "尚無異動紀錄", @@ -1349,9 +1343,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": "尚無備註", @@ -1821,6 +1817,7 @@ "Sales Today": "今日銷售", "Sales revenue minus product cost net value": "銷售額扣除商品成本淨值", "Save": "儲存", + "Save & Sync": "儲存並同步", "Save Changes": "儲存變更", "Save Config": "儲存配置", "Save Material": "儲存素材", @@ -1828,6 +1825,8 @@ "Save Settings": "儲存設定", "Save Version": "儲存版本", "Save error:": "儲存錯誤:", + "Save failed": "儲存失敗", + "Saved": "已儲存", "Saved.": "已儲存", "Saving...": "儲存中...", "Scale level and access control": "層級與存取控制", @@ -2577,6 +2576,7 @@ "of": "/共", "of items": "筆項目", "orders": "筆", + "overrides set": "項已設定機台價", "pending": "等待機台領取", "permissions": "權限設定", "permissions.accounts": "帳號管理",