1. 修正客戶管理頁面 H1 標題與麵包屑導覽路徑。 2. 在機台編輯頁面實作分頁功能(基本、營運、機台系統設定)。 3. 實作極簡奢華風的機台系統設定 UI,按功能模組(電子發票、刷卡、支付、購物車等)進行卡片式分組。 4. 將系統設定資料從 Company JSON 遷移至 Machine 資料表獨立欄位,並更新關聯控制器與模型邏輯。 5. 清理 CompanyController 與視圖中的冗餘設定邏輯。 6. 完成現有資料遷移,確保機台正確繼承客戶層級之設定。 7. 優化倉儲管理「現有庫存」矩陣視圖與導覽功能。
308 lines
13 KiB
PHP
308 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin\BasicSettings;
|
|
|
|
use App\Http\Controllers\Admin\AdminController;
|
|
use App\Models\Machine\Machine;
|
|
use App\Models\Machine\MachineModel;
|
|
use App\Models\System\PaymentConfig;
|
|
use App\Traits\ImageHandler;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\View\View;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class MachineSettingController extends AdminController
|
|
{
|
|
use ImageHandler;
|
|
|
|
/**
|
|
* 顯示機台與型號設定列表 (採用標籤頁整合)
|
|
*/
|
|
public function index(Request $request): View|\Illuminate\Http\Response
|
|
{
|
|
$tab = $request->input('tab', 'machines');
|
|
$per_page = $request->input('per_page', 10);
|
|
$search = $request->input('search');
|
|
$isAjax = $request->boolean('_ajax');
|
|
|
|
// AJAX 模式:只查當前 Tab 的搜尋/分頁結果
|
|
if ($isAjax) {
|
|
$machines = null;
|
|
$models_list = null;
|
|
$users_list = null;
|
|
|
|
switch ($tab) {
|
|
case 'machines':
|
|
$machineQuery = Machine::query()->with(['machineModel', 'paymentConfig', 'company']);
|
|
if ($search) {
|
|
$machineQuery->where(function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('serial_no', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
$machines = $machineQuery->latest()->paginate($per_page)->withQueryString();
|
|
break;
|
|
|
|
case 'models':
|
|
$modelQuery = MachineModel::query()->withCount('machines');
|
|
if ($search) {
|
|
$modelQuery->where('name', 'like', "%{$search}%");
|
|
}
|
|
$models_list = $modelQuery->latest()->paginate($per_page)->withQueryString();
|
|
break;
|
|
|
|
case 'permissions':
|
|
$userQuery = \App\Models\System\User::query()
|
|
->where('is_admin', true)
|
|
->with(['company', 'machines']);
|
|
if ($search) {
|
|
$userQuery->where(function($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('username', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
if ($request->filled('company_id')) {
|
|
$userQuery->where('company_id', $request->company_id);
|
|
}
|
|
$users_list = $userQuery->latest()->paginate($per_page)->withQueryString();
|
|
break;
|
|
}
|
|
|
|
$companies = \App\Models\System\Company::select('id', 'name', 'code')->get();
|
|
|
|
$tabView = match($tab) {
|
|
'models' => 'admin.basic-settings.machines.partials.tab-models',
|
|
'permissions' => 'admin.basic-settings.machines.partials.tab-permissions',
|
|
default => 'admin.basic-settings.machines.partials.tab-machines',
|
|
};
|
|
return response()->view($tabView, compact(
|
|
'machines', 'models_list', 'users_list', 'companies', 'tab'
|
|
));
|
|
}
|
|
|
|
// SSR 模式:一次查好全部三個 Tab 的首頁資料(供 x-show 即時切換)
|
|
$machines = Machine::query()
|
|
->with(['machineModel', 'paymentConfig', 'company'])
|
|
->latest()->paginate($per_page)->withQueryString();
|
|
|
|
$models_list = MachineModel::query()
|
|
->withCount('machines')
|
|
->latest()->paginate($per_page)->withQueryString();
|
|
|
|
$userQuery = \App\Models\System\User::query()
|
|
->where('is_admin', true)
|
|
->with(['company', 'machines']);
|
|
$users_list = $userQuery->latest()->paginate($per_page)->withQueryString();
|
|
|
|
// 基礎下拉資料 (用於新增/編輯機台的彈窗)
|
|
$models = MachineModel::select('id', 'name')->get();
|
|
$paymentConfigs = PaymentConfig::select('id', 'name')->get();
|
|
$companies = \App\Models\System\Company::select('id', 'name', 'code')->get();
|
|
|
|
return view('admin.basic-settings.machines.index', compact(
|
|
'machines',
|
|
'models_list',
|
|
'users_list',
|
|
'models',
|
|
'paymentConfigs',
|
|
'companies',
|
|
'tab'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 儲存新機台 (僅核心欄位)
|
|
*/
|
|
public function store(Request $request): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'serial_no' => 'required|string|unique:machines,serial_no',
|
|
'company_id' => 'nullable|exists:companies,id',
|
|
'machine_model_id' => 'required|exists:machine_models,id',
|
|
'payment_config_id' => 'nullable|exists:payment_configs,id',
|
|
'location' => 'nullable|string|max:255',
|
|
'address' => 'nullable|string|max:255',
|
|
'latitude' => 'nullable|numeric|between:-90,90',
|
|
'longitude' => 'nullable|numeric|between:-180,180',
|
|
'images.*' => 'image|mimes:jpeg,png,jpg,gif,webp|max:10240', // Increase to 10MB
|
|
]);
|
|
|
|
$imagePaths = [];
|
|
if ($request->hasFile('images')) {
|
|
foreach (array_slice($request->file('images'), 0, 3) as $image) {
|
|
$imagePaths[] = $this->storeAsWebp($image, 'machines');
|
|
}
|
|
}
|
|
|
|
$machine = Machine::create(array_merge($validated, [
|
|
'api_token' => \Illuminate\Support\Str::random(60),
|
|
'creator_id' => auth()->id(),
|
|
'updater_id' => auth()->id(),
|
|
'card_reader_seconds' => 30, // 預設值
|
|
'card_reader_checkout_time_1' => '22:30:00',
|
|
'card_reader_checkout_time_2' => '23:45:00',
|
|
'payment_buffer_seconds' => 5,
|
|
'images' => $imagePaths,
|
|
]));
|
|
|
|
return redirect()->route('admin.basic-settings.machines.index')
|
|
->with('success', __('Machine created successfully.'));
|
|
}
|
|
|
|
/**
|
|
* 顯示詳細編輯頁面
|
|
*/
|
|
public function edit(Machine $machine): View
|
|
{
|
|
$models = MachineModel::select('id', 'name')->get();
|
|
$paymentConfigs = PaymentConfig::select('id', 'name')->get();
|
|
$companies = \App\Models\System\Company::select('id', 'name', 'code')->get();
|
|
|
|
return view('admin.basic-settings.machines.edit', compact('machine', 'models', 'paymentConfigs', 'companies'));
|
|
}
|
|
|
|
/**
|
|
* 更新機台詳細參數
|
|
*/
|
|
public function update(Request $request, Machine $machine): RedirectResponse
|
|
{
|
|
Log::info('Machine Update Request', ['machine_id' => $machine->id, 'data' => $request->all()]);
|
|
|
|
try {
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'serial_no' => 'sometimes|required|string|unique:machines,serial_no,' . $machine->id,
|
|
'card_reader_seconds' => 'required|integer|min:0',
|
|
'payment_buffer_seconds' => 'required|integer|min:0',
|
|
'card_reader_checkout_time_1' => 'nullable|string',
|
|
'card_reader_checkout_time_2' => 'nullable|string',
|
|
'heating_start_time' => 'nullable|string',
|
|
'heating_end_time' => 'nullable|string',
|
|
'card_reader_no' => 'nullable|string|max:255',
|
|
'key_no' => 'nullable|string|max:255',
|
|
'invoice_status' => 'required|integer|in:0,1,2',
|
|
'welcome_gift_enabled' => 'boolean',
|
|
'is_spring_slot_1_10' => 'boolean',
|
|
'is_spring_slot_11_20' => 'boolean',
|
|
'is_spring_slot_21_30' => 'boolean',
|
|
'is_spring_slot_31_40' => 'boolean',
|
|
'is_spring_slot_41_50' => 'boolean',
|
|
'is_spring_slot_51_60' => 'boolean',
|
|
'member_system_enabled' => 'boolean',
|
|
'tax_invoice_enabled' => 'boolean',
|
|
'card_terminal_enabled' => 'boolean',
|
|
'scan_pay_esun_enabled' => 'boolean',
|
|
'scan_pay_linepay_enabled' => 'boolean',
|
|
'shopping_cart_enabled' => 'boolean',
|
|
'cash_module_enabled' => 'boolean',
|
|
'machine_model_id' => 'required|exists:machine_models,id',
|
|
'payment_config_id' => 'nullable|exists:payment_configs,id',
|
|
'location' => 'nullable|string|max:255',
|
|
'address' => 'nullable|string|max:255',
|
|
'latitude' => 'nullable|numeric|between:-90,90',
|
|
'longitude' => 'nullable|numeric|between:-180,180',
|
|
'image_0' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:10240',
|
|
'image_1' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:10240',
|
|
'image_2' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:10240',
|
|
'remove_image_0' => 'nullable|boolean',
|
|
'remove_image_1' => 'nullable|boolean',
|
|
'remove_image_2' => 'nullable|boolean',
|
|
]);
|
|
|
|
// 僅限系統管理員可修改公司
|
|
if (auth()->user()->isSystemAdmin()) {
|
|
$companyRule = ['company_id' => 'nullable|exists:companies,id'];
|
|
$companyData = $request->validate($companyRule);
|
|
$validated = array_merge($validated, $companyData);
|
|
}
|
|
|
|
Log::info('Machine Update Validated Data', ['data' => $validated]);
|
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
Log::error('Machine Update Validation Failed', ['errors' => $e->errors()]);
|
|
throw $e;
|
|
}
|
|
|
|
// 排除虛擬欄位 (圖片上傳、移除標記),這些欄位不在資料表內
|
|
$dataToUpdate = \Illuminate\Support\Arr::except($validated, [
|
|
'image_0', 'image_1', 'image_2',
|
|
'remove_image_0', 'remove_image_1', 'remove_image_2'
|
|
]);
|
|
|
|
$machine->update(array_merge($dataToUpdate, [
|
|
'updater_id' => auth()->id(),
|
|
]));
|
|
|
|
// 處理圖片更新 (支援 3 個獨立槽位: image_0, image_1, image_2)
|
|
$currentImages = $machine->images ?? [];
|
|
$updated = false;
|
|
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$inputName = "image_$i";
|
|
$removeName = "remove_image_$i";
|
|
|
|
// 如果有新圖片上傳
|
|
if ($request->hasFile($inputName)) {
|
|
// 刪除舊圖
|
|
if (isset($currentImages[$i]) && !empty($currentImages[$i])) {
|
|
\Illuminate\Support\Facades\Storage::disk('public')->delete($currentImages[$i]);
|
|
}
|
|
// 儲存新圖
|
|
$currentImages[$i] = $this->storeAsWebp($request->file($inputName), 'machines');
|
|
$updated = true;
|
|
}
|
|
// 否則,如果有刪除標記
|
|
elseif ($request->input($removeName) === '1') {
|
|
if (isset($currentImages[$i]) && !empty($currentImages[$i])) {
|
|
\Illuminate\Support\Facades\Storage::disk('public')->delete($currentImages[$i]);
|
|
unset($currentImages[$i]);
|
|
$updated = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($updated) {
|
|
ksort($currentImages);
|
|
$machine->update(['images' => array_values($currentImages)]);
|
|
}
|
|
|
|
return redirect()->route('admin.basic-settings.machines.index')
|
|
->with('success', __('Machine settings updated successfully.'));
|
|
}
|
|
|
|
public function regenerateToken(Request $request, $serial): \Illuminate\Http\JsonResponse
|
|
{
|
|
$machine = Machine::where('serial_no', $serial)->firstOrFail();
|
|
$newToken = \Illuminate\Support\Str::random(60);
|
|
$machine->update(['api_token' => $newToken]);
|
|
|
|
Log::info('Machine API Token Regenerated', [
|
|
'machine_id' => $machine->id,
|
|
'serial_no' => $machine->serial_no,
|
|
'user_id' => auth()->id()
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('API Token regenerated successfully.'),
|
|
'api_token' => $newToken
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 公開機台分布地圖
|
|
*/
|
|
public function distribution(): View
|
|
{
|
|
$machines = Machine::whereNotNull('latitude')
|
|
->whereNotNull('longitude')
|
|
->get(['id', 'name', 'serial_no', 'address', 'location', 'latitude', 'longitude', 'status']);
|
|
|
|
return view('admin.basic-settings.machines.distribution', compact('machines'));
|
|
}
|
|
}
|