-
diff --git a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php
new file mode 100644
index 0000000..c0d8552
--- /dev/null
+++ b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php
@@ -0,0 +1,199 @@
+input('per_page', 20);
+ $query = Machine::query()->with(['machineModel', 'paymentConfig', 'company']);
+
+ // 搜尋:名稱或序號
+ if ($search = $request->input('search')) {
+ $query->where(function ($q) use ($search) {
+ $q->where('name', 'like', "%{$search}%")
+ ->orWhere('serial_no', 'like', "%{$search}%");
+ });
+ }
+
+ $machines = $query->latest()
+ ->paginate($per_page)
+ ->withQueryString();
+
+ $models = MachineModel::select('id', 'name')->get();
+ $paymentConfigs = PaymentConfig::select('id', 'name')->get();
+ // 這裡應根據租戶 (Company) 決定可用的選項,暫採簡單模擬或從 Auth 取得
+ $companies = \App\Models\System\Company::select('id', 'name')->get();
+
+ return view('admin.basic-settings.machines.index', compact('machines', 'models', 'paymentConfigs', 'companies'));
+ }
+
+ /**
+ * 儲存新機台 (僅核心欄位)
+ */
+ public function store(Request $request): RedirectResponse
+ {
+ $validated = $request->validate([
+ 'name' => 'required|string|max:255',
+ 'serial_no' => 'required|string|unique:machines,serial_no',
+ 'company_id' => 'required|exists:companies,id',
+ 'machine_model_id' => 'required|exists:machine_models,id',
+ 'payment_config_id' => 'nullable|exists:payment_configs,id',
+ 'images.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048',
+ ]);
+
+ $imagePaths = [];
+ if ($request->hasFile('images')) {
+ foreach (array_slice($request->file('images'), 0, 3) as $image) {
+ $imagePaths[] = $this->processAndStoreImage($image);
+ }
+ }
+
+ $machine = Machine::create(array_merge($validated, [
+ 'status' => 'offline',
+ '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')->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',
+ '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',
+ 'machine_model_id' => 'required|exists:machine_models,id',
+ 'payment_config_id' => 'nullable|exists:payment_configs,id',
+ ]);
+
+ Log::info('Machine Update Validated Data', ['data' => $validated]);
+ } catch (\Illuminate\Validation\ValidationException $e) {
+ Log::error('Machine Update Validation Failed', ['errors' => $e->errors()]);
+ throw $e;
+ }
+
+ $machine->update(array_merge($validated, [
+ 'updater_id' => auth()->id(),
+ ]));
+
+ // 處理圖片更新 (若有上傳新圖片,則替換或附加,這裡採簡單邏輯:若有傳 images 則全換)
+ if ($request->hasFile('images')) {
+ // 刪除舊圖
+ if (!empty($machine->images)) {
+ foreach ($machine->images as $oldPath) {
+ Storage::disk('public')->delete($oldPath);
+ }
+ }
+
+ $imagePaths = [];
+ foreach (array_slice($request->file('images'), 0, 3) as $image) {
+ $imagePaths[] = $this->processAndStoreImage($image);
+ }
+ $machine->update(['images' => $imagePaths]);
+ }
+
+ return redirect()->route('admin.basic-settings.machines.index')
+ ->with('success', __('Machine settings updated successfully.'));
+ }
+
+ /**
+ * 處理圖片並轉換為 WebP
+ */
+ private function processAndStoreImage($file): string
+ {
+ $filename = Str::random(40) . '.webp';
+ $path = 'machines/' . $filename;
+
+ // 建立圖資源
+ $image = null;
+ $extension = strtolower($file->getClientOriginalExtension());
+
+ switch ($extension) {
+ case 'jpeg':
+ case 'jpg':
+ $image = imagecreatefromjpeg($file->getRealPath());
+ break;
+ case 'png':
+ $image = imagecreatefrompng($file->getRealPath());
+ break;
+ case 'gif':
+ $image = imagecreatefromgif($file->getRealPath());
+ break;
+ case 'webp':
+ $image = imagecreatefromwebp($file->getRealPath());
+ break;
+ }
+
+ if ($image) {
+ // 確保目錄存在
+ Storage::disk('public')->makeDirectory('machines');
+ $fullPath = Storage::disk('public')->path($path);
+
+ // 轉換並儲存
+ imagewebp($image, $fullPath, 80); // 品質 80
+ imagedestroy($image);
+
+ return $path;
+ }
+
+ // Fallback to standard store if GD fails
+ return $file->store('machines', 'public');
+ }
+}
diff --git a/app/Http/Controllers/Admin/BasicSettings/PaymentConfigController.php b/app/Http/Controllers/Admin/BasicSettings/PaymentConfigController.php
new file mode 100644
index 0000000..a7936d4
--- /dev/null
+++ b/app/Http/Controllers/Admin/BasicSettings/PaymentConfigController.php
@@ -0,0 +1,100 @@
+input('per_page', 20);
+ $configs = PaymentConfig::query()
+ ->with(['company', 'creator'])
+ ->latest()
+ ->paginate($per_page)
+ ->withQueryString();
+
+ return view('admin.basic-settings.payment-configs.index', [
+ 'paymentConfigs' => $configs
+ ]);
+ }
+
+ /**
+ * 顯示新增頁面
+ */
+ public function create(): View
+ {
+ $companies = \App\Models\System\Company::select('id', 'name')->get();
+ return view('admin.basic-settings.payment-configs.create', compact('companies'));
+ }
+
+ /**
+ * 儲存金流配置
+ */
+ public function store(Request $request): RedirectResponse
+ {
+ $request->validate([
+ 'name' => 'required|string|max:255',
+ 'company_id' => 'required|exists:companies,id',
+ 'settings' => 'required|array',
+ ]);
+
+ PaymentConfig::create([
+ 'name' => $request->name,
+ 'company_id' => $request->company_id,
+ 'settings' => $request->settings,
+ 'creator_id' => auth()->id(),
+ 'updater_id' => auth()->id(),
+ ]);
+
+ return redirect()->route('admin.basic-settings.payment-configs.index')
+ ->with('success', __('Payment Configuration created successfully.'));
+ }
+
+ /**
+ * 顯示編輯頁面
+ */
+ public function edit(PaymentConfig $paymentConfig): View
+ {
+ $companies = \App\Models\System\Company::select('id', 'name')->get();
+ return view('admin.basic-settings.payment-configs.edit', compact('paymentConfig', 'companies'));
+ }
+
+ /**
+ * 更新金流配置
+ */
+ public function update(Request $request, PaymentConfig $paymentConfig): RedirectResponse
+ {
+ $request->validate([
+ 'name' => 'required|string|max:255',
+ 'settings' => 'required|array',
+ ]);
+
+ $paymentConfig->update([
+ 'name' => $request->name,
+ 'settings' => $request->settings,
+ 'updater_id' => auth()->id(),
+ ]);
+
+ return redirect()->route('admin.basic-settings.payment-configs.index')
+ ->with('success', __('Payment Configuration updated successfully.'));
+ }
+
+ /**
+ * 刪除金流配置
+ */
+ public function destroy(PaymentConfig $paymentConfig): RedirectResponse
+ {
+ $paymentConfig->delete();
+ return redirect()->route('admin.basic-settings.payment-configs.index')
+ ->with('success', __('Payment Configuration deleted successfully.'));
+ }
+}
diff --git a/app/Http/Controllers/Admin/CompanyController.php b/app/Http/Controllers/Admin/CompanyController.php
index b7d03b1..e2ee1a7 100644
--- a/app/Http/Controllers/Admin/CompanyController.php
+++ b/app/Http/Controllers/Admin/CompanyController.php
@@ -96,7 +96,7 @@ class CompanyController extends Controller
'name' => 'required|string|max:255',
'code' => 'required|string|max:50|unique:companies,code,' . $company->id,
'tax_id' => 'nullable|string|max:50',
- 'contact_name' => 'required|string|max:255',
+ 'contact_name' => 'nullable|string|max:255',
'contact_phone' => 'nullable|string|max:50',
'contact_email' => 'nullable|email|max:255',
'valid_until' => 'nullable|date',
diff --git a/app/Http/Controllers/Admin/MachineController.php b/app/Http/Controllers/Admin/MachineController.php
index e1c89d5..6d4a8b5 100644
--- a/app/Http/Controllers/Admin/MachineController.php
+++ b/app/Http/Controllers/Admin/MachineController.php
@@ -14,8 +14,17 @@ class MachineController extends AdminController
public function index(Request $request): View
{
$per_page = $request->input('per_page', 10);
- $machines = Machine::query()
- ->when($request->status, function ($query, $status) {
+ $query = Machine::query();
+
+ // 搜尋:名稱或序號
+ if ($search = $request->input('search')) {
+ $query->where(function ($q) use ($search) {
+ $q->where('name', 'like', "%{$search}%")
+ ->orWhere('serial_no', 'like', "%{$search}%");
+ });
+ }
+
+ $machines = $query->when($request->status, function ($query, $status) {
return $query->where('status', $status);
})
->latest()
diff --git a/app/Http/Controllers/Admin/PermissionController.php b/app/Http/Controllers/Admin/PermissionController.php
index 3b452fe..d4688a3 100644
--- a/app/Http/Controllers/Admin/PermissionController.php
+++ b/app/Http/Controllers/Admin/PermissionController.php
@@ -11,15 +11,46 @@ class PermissionController extends Controller
public function roles()
{
$per_page = request()->input('per_page', 10);
- $roles = \Spatie\Permission\Models\Role::with(['permissions', 'users'])->latest()->paginate($per_page)->withQueryString();
- $all_permissions = \Spatie\Permission\Models\Permission::all()->groupBy(function($perm) {
- if (str_starts_with($perm->name, 'menu.')) {
- return 'menu';
- }
- return 'other';
- });
+ $user = auth()->user();
+ $query = \App\Models\System\Role::query()->with(['permissions', 'users']);
+
+ // 租戶隔離:租戶只能看到自己公司的角色 + 系統角色 (company_id is null)
+ if (!$user->isSystemAdmin()) {
+ $query->where(function($q) use ($user) {
+ $q->where('company_id', $user->company_id)
+ ->orWhereNull('company_id');
+ });
+ }
+
+ // 搜尋:角色名稱
+ if ($search = request()->input('search')) {
+ $query->where('name', 'like', "%{$search}%");
+ }
+
+ $roles = $query->latest()->paginate($per_page)->withQueryString();
+ $all_permissions = \Spatie\Permission\Models\Permission::all()
+ ->filter(function($perm) {
+ // 排除子項目的權限,只顯示主選單權限
+ $excluded = [
+ 'menu.basic.machines',
+ 'menu.basic.payment-configs',
+ 'menu.companies',
+ 'menu.accounts',
+ 'menu.roles',
+ ];
+ return !in_array($perm->name, $excluded);
+ })
+ ->groupBy(function($perm) {
+ if (str_starts_with($perm->name, 'menu.')) {
+ return 'menu';
+ }
+ return 'other';
+ });
- return view('admin.permission.roles', compact('roles', 'all_permissions'));
+ // 根據路由決定標題
+ $title = request()->routeIs('*.sub-account-roles') ? __('Sub Account Roles') : __('Role Settings');
+
+ return view('admin.permission.roles', compact('roles', 'all_permissions', 'title'));
}
/**
@@ -33,14 +64,22 @@ class PermissionController extends Controller
'permissions.*' => 'string|exists:permissions,name',
]);
- $role = \Spatie\Permission\Models\Role::create([
+ $is_system = auth()->user()->isSystemAdmin() && $request->boolean('is_system');
+
+ $role = \App\Models\System\Role::create([
'name' => $validated['name'],
'guard_name' => 'web',
- 'is_system' => false,
+ 'company_id' => $is_system ? null : auth()->user()->company_id,
+ 'is_system' => $is_system,
]);
if (!empty($validated['permissions'])) {
- $role->syncPermissions($validated['permissions']);
+ $perms = $validated['permissions'];
+ // 如果不是系統角色,排除主選單的系統權限
+ if (!$is_system) {
+ $perms = array_diff($perms, ['menu.basic-settings', 'menu.permissions']);
+ }
+ $role->syncPermissions($perms);
}
return redirect()->back()->with('success', __('Role created successfully.'));
@@ -51,7 +90,7 @@ class PermissionController extends Controller
*/
public function updateRole(Request $request, $id)
{
- $role = \Spatie\Permission\Models\Role::findOrFail($id);
+ $role = \App\Models\System\Role::findOrFail($id);
$validated = $request->validate([
'name' => 'required|string|max:255|unique:roles,name,' . $id,
@@ -59,11 +98,28 @@ class PermissionController extends Controller
'permissions.*' => 'string|exists:permissions,name',
]);
- if (!$role->is_system) {
- $role->update(['name' => $validated['name']]);
+ if ($role->name === 'super-admin') {
+ return redirect()->back()->with('error', __('The Super Admin role is immutable.'));
}
- $role->syncPermissions($validated['permissions'] ?? []);
+ if (!auth()->user()->isSystemAdmin() && $role->is_system) {
+ return redirect()->back()->with('error', __('System roles cannot be modified by tenant administrators.'));
+ }
+
+ $is_system = auth()->user()->isSystemAdmin() ? $request->boolean('is_system') : $role->is_system;
+
+ $role->update([
+ 'name' => $validated['name'],
+ 'is_system' => $is_system,
+ 'company_id' => $is_system ? null : $role->company_id,
+ ]);
+
+ $perms = $validated['permissions'] ?? [];
+ // 如果不是系統角色,排除主選單的系統權限
+ if (!$is_system) {
+ $perms = array_diff($perms, ['menu.basic-settings', 'menu.permissions']);
+ }
+ $role->syncPermissions($perms);
return redirect()->back()->with('success', __('Role updated successfully.'));
}
@@ -73,10 +129,14 @@ class PermissionController extends Controller
*/
public function destroyRole($id)
{
- $role = \Spatie\Permission\Models\Role::findOrFail($id);
+ $role = \App\Models\System\Role::findOrFail($id);
- if ($role->is_system) {
- return redirect()->back()->with('error', __('System roles cannot be deleted.'));
+ if ($role->name === 'super-admin') {
+ return redirect()->back()->with('error', __('The Super Admin role cannot be deleted.'));
+ }
+
+ if (!auth()->user()->isSystemAdmin() && $role->is_system) {
+ return redirect()->back()->with('error', __('System roles cannot be deleted by tenant administrators.'));
}
if ($role->users()->count() > 0) {
@@ -115,9 +175,19 @@ class PermissionController extends Controller
$per_page = $request->input('per_page', 10);
$users = $query->latest()->paginate($per_page)->withQueryString();
$companies = auth()->user()->isSystemAdmin() ? \App\Models\System\Company::all() : collect();
- $roles = \Spatie\Permission\Models\Role::all();
+ $roles_query = \App\Models\System\Role::where('name', '!=', 'super-admin');
+ if (!auth()->user()->isSystemAdmin()) {
+ $roles_query->where(function($q) {
+ $q->where('company_id', auth()->user()->company_id)
+ ->orWhereNull('company_id');
+ });
+ }
+ $roles = $roles_query->get();
- return view('admin.data-config.accounts', compact('users', 'companies', 'roles'));
+ // 根據路由決定標題
+ $title = request()->routeIs('*.sub-accounts') ? __('Sub Account Management') : __('Account Management');
+
+ return view('admin.data-config.accounts', compact('users', 'companies', 'roles', 'title'));
}
/**
@@ -158,6 +228,10 @@ class PermissionController extends Controller
{
$user = \App\Models\System\User::findOrFail($id);
+ if ($user->hasRole('super-admin')) {
+ return redirect()->back()->with('error', __('System super admin accounts cannot be modified via this interface.'));
+ }
+
$validated = $request->validate([
'name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:users,username,' . $id,
@@ -178,7 +252,13 @@ class PermissionController extends Controller
];
if (auth()->user()->isSystemAdmin()) {
- $updateData['company_id'] = $validated['company_id'];
+ // 防止超級管理員不小心把自己綁定到租客公司或降級
+ if ($user->id === auth()->id()) {
+ $updateData['company_id'] = null;
+ $validated['role'] = 'super-admin';
+ } else {
+ $updateData['company_id'] = $validated['company_id'];
+ }
}
if (!empty($validated['password'])) {
@@ -187,7 +267,12 @@ class PermissionController extends Controller
$user->update($updateData);
- $user->syncRoles([$validated['role']]);
+ // 如果是編輯自己且原本是超級管理員,強制保留 super-admin 角色
+ if ($user->id === auth()->id() && auth()->user()->isSystemAdmin()) {
+ $user->syncRoles(['super-admin']);
+ } else {
+ $user->syncRoles([$validated['role']]);
+ }
return redirect()->back()->with('success', __('Account updated successfully.'));
}
@@ -199,6 +284,10 @@ class PermissionController extends Controller
{
$user = \App\Models\System\User::findOrFail($id);
+ if ($user->hasRole('super-admin')) {
+ return redirect()->back()->with('error', __('System super admin accounts cannot be deleted.'));
+ }
+
if ($user->id === auth()->id()) {
return redirect()->back()->with('error', __('You cannot delete your own account.'));
}
diff --git a/app/Listeners/LogSuccessfulLogin.php b/app/Listeners/LogSuccessfulLogin.php
index f63a186..4d37776 100644
--- a/app/Listeners/LogSuccessfulLogin.php
+++ b/app/Listeners/LogSuccessfulLogin.php
@@ -2,7 +2,7 @@
namespace App\Listeners;
-use App\Models\UserLoginLog;
+use App\Models\System\UserLoginLog;
use Illuminate\Auth\Events\Login;
use Illuminate\Http\Request;
diff --git a/app/Models/Machine/Machine.php b/app/Models/Machine/Machine.php
index 6a29848..e06a6af 100644
--- a/app/Models/Machine/Machine.php
+++ b/app/Models/Machine/Machine.php
@@ -25,15 +25,78 @@ class Machine extends Model
'firmware_version',
'api_token',
'last_heartbeat_at',
+ 'card_reader_seconds',
+ 'card_reader_checkout_time_1',
+ 'card_reader_checkout_time_2',
+ 'heating_start_time',
+ 'heating_end_time',
+ 'payment_buffer_seconds',
+ 'card_reader_no',
+ 'key_no',
+ 'invoice_status',
+ 'welcome_gift_enabled',
+ 'is_spring_slot_1_10',
+ 'is_spring_slot_11_20',
+ 'is_spring_slot_21_30',
+ 'is_spring_slot_31_40',
+ 'is_spring_slot_41_50',
+ 'is_spring_slot_51_60',
+ 'member_system_enabled',
+ 'payment_config_id',
+ 'machine_model_id',
+ 'images',
+ 'creator_id',
+ 'updater_id',
];
protected $casts = [
'last_heartbeat_at' => 'datetime',
+ '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',
+ 'images' => 'array',
];
+ /**
+ * Get machine images absolute URLs
+ */
+ public function getImageUrlsAttribute(): array
+ {
+ if (empty($this->images)) {
+ return [];
+ }
+
+ return array_map(fn($path) => \Illuminate\Support\Facades\Storage::disk('public')->url($path), $this->images);
+ }
+
public function logs()
{
return $this->hasMany(MachineLog::class);
}
+ public function machineModel()
+ {
+ return $this->belongsTo(MachineModel::class);
+ }
+
+ public function paymentConfig()
+ {
+ return $this->belongsTo(\App\Models\System\PaymentConfig::class);
+ }
+
+ public function creator()
+ {
+ return $this->belongsTo(\App\Models\System\User::class, 'creator_id');
+ }
+
+ public function updater()
+ {
+ return $this->belongsTo(\App\Models\System\User::class, 'updater_id');
+ }
+
}
diff --git a/app/Models/Machine/MachineModel.php b/app/Models/Machine/MachineModel.php
new file mode 100644
index 0000000..f2724de
--- /dev/null
+++ b/app/Models/Machine/MachineModel.php
@@ -0,0 +1,35 @@
+hasMany(Machine::class);
+ }
+
+ public function company()
+ {
+ return $this->belongsTo(\App\Models\System\Company::class);
+ }
+
+ public function creator()
+ {
+ return $this->belongsTo(\App\Models\System\User::class, 'creator_id');
+ }
+
+ public function updater()
+ {
+ return $this->belongsTo(\App\Models\System\User::class, 'updater_id');
+ }
+}
diff --git a/app/Models/System/PaymentConfig.php b/app/Models/System/PaymentConfig.php
new file mode 100644
index 0000000..8f0b126
--- /dev/null
+++ b/app/Models/System/PaymentConfig.php
@@ -0,0 +1,40 @@
+ 'array',
+ ];
+
+ public function machines()
+ {
+ return $this->hasMany(\App\Models\Machine\Machine::class);
+ }
+
+ public function company()
+ {
+ return $this->belongsTo(\App\Models\System\Company::class);
+ }
+
+ public function creator()
+ {
+ return $this->belongsTo(User::class, 'creator_id');
+ }
+
+ public function updater()
+ {
+ return $this->belongsTo(User::class, 'updater_id');
+ }
+}
diff --git a/app/Models/System/Role.php b/app/Models/System/Role.php
new file mode 100644
index 0000000..cc6dc32
--- /dev/null
+++ b/app/Models/System/Role.php
@@ -0,0 +1,34 @@
+belongsTo(Company::class);
+ }
+
+ /**
+ * Scope a query to only include roles for a specific company or system roles.
+ */
+ public function scopeForCompany($query, $company_id)
+ {
+ return $query->where(function($q) use ($company_id) {
+ $q->where('company_id', $company_id)
+ ->orWhereNull('company_id');
+ });
+ }
+}
diff --git a/app/Models/System/User.php b/app/Models/System/User.php
index 066b705..2d2f571 100644
--- a/app/Models/System/User.php
+++ b/app/Models/System/User.php
@@ -58,7 +58,7 @@ class User extends Authenticatable
*/
public function loginLogs()
{
- return $this->hasMany(\App\Models\UserLoginLog::class);
+ return $this->hasMany(UserLoginLog::class);
}
/**
diff --git a/app/Models/UserLoginLog.php b/app/Models/System/UserLoginLog.php
similarity index 82%
rename from app/Models/UserLoginLog.php
rename to app/Models/System/UserLoginLog.php
index dc70a54..fc4993b 100644
--- a/app/Models/UserLoginLog.php
+++ b/app/Models/System/UserLoginLog.php
@@ -1,9 +1,9 @@
Spatie\Permission\Models\Role::class,
+ 'role' => \App\Models\System\Role::class,
],
diff --git a/database/migrations/2026_03_17_105051_create_machine_models_table.php b/database/migrations/2026_03_17_105051_create_machine_models_table.php
new file mode 100644
index 0000000..8efd6ae
--- /dev/null
+++ b/database/migrations/2026_03_17_105051_create_machine_models_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->string('name')->comment('型號名稱');
+ $table->foreignId('company_id')->nullable()->constrained()->onDelete('cascade')->comment('關聯公司');
+ $table->foreignId('creator_id')->nullable()->constrained('users')->nullOnDelete()->comment('建立者');
+ $table->foreignId('updater_id')->nullable()->constrained('users')->nullOnDelete()->comment('修改者');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('machine_models');
+ }
+};
diff --git a/database/migrations/2026_03_17_105100_create_payment_configs_table.php b/database/migrations/2026_03_17_105100_create_payment_configs_table.php
new file mode 100644
index 0000000..93cc8f2
--- /dev/null
+++ b/database/migrations/2026_03_17_105100_create_payment_configs_table.php
@@ -0,0 +1,32 @@
+id();
+ $table->foreignId('company_id')->constrained()->onDelete('cascade')->comment('關聯公司');
+ $table->string('name')->comment('組合名稱');
+ $table->json('settings')->nullable()->comment('金流參數 (JSON)');
+ $table->foreignId('creator_id')->nullable()->constrained('users')->nullOnDelete()->comment('建立者');
+ $table->foreignId('updater_id')->nullable()->constrained('users')->nullOnDelete()->comment('修改者');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('payment_configs');
+ }
+};
diff --git a/database/migrations/2026_03_17_105101_add_settings_to_machines_table.php b/database/migrations/2026_03_17_105101_add_settings_to_machines_table.php
new file mode 100644
index 0000000..0a5b23c
--- /dev/null
+++ b/database/migrations/2026_03_17_105101_add_settings_to_machines_table.php
@@ -0,0 +1,76 @@
+integer('card_reader_seconds')->nullable()->default(30)->comment('刷卡機秒數');
+ $table->time('card_reader_checkout_time_1')->nullable()->default('22:30:00')->comment('卡機結帳時間1');
+ $table->time('card_reader_checkout_time_2')->nullable()->default('23:45:00')->comment('卡機結帳時間2');
+ $table->time('heating_start_time')->default('00:00:00')->comment('開啟-加熱時間');
+ $table->time('heating_end_time')->default('00:00:00')->comment('關閉-加熱時間');
+ $table->integer('payment_buffer_seconds')->nullable()->default(5)->comment('金流緩衝時間(s)');
+ $table->string('card_reader_no')->nullable()->comment('刷卡機編號');
+ $table->string('key_no')->nullable()->comment('鑰匙編號');
+ $table->tinyInteger('invoice_status')->default(0)->comment('發票狀態碼: 0=不開發票, 1=預設捐, 2=預設不捐');
+ $table->boolean('welcome_gift_enabled')->default(0)->comment('來店禮開關');
+ $table->boolean('is_spring_slot_1_10')->default(0)->comment('貨道類型(1~10): 0=履帶, 1=彈簧');
+ $table->boolean('is_spring_slot_11_20')->default(0)->comment('貨道類型(11~20): 0=履帶, 1=彈簧');
+ $table->boolean('is_spring_slot_21_30')->default(0)->comment('貨道類型(21~30): 0=履帶, 1=彈簧');
+ $table->boolean('is_spring_slot_31_40')->default(0)->comment('貨道類型(31~40): 0=履帶, 1=彈簧');
+ $table->boolean('is_spring_slot_41_50')->default(0)->comment('貨道類型(41~50): 0=履帶, 1=彈簧');
+ $table->boolean('is_spring_slot_51_60')->default(0)->comment('貨道類型(51~60): 0=履帶, 1=彈簧');
+ $table->boolean('member_system_enabled')->default(0)->comment('會員系統開關');
+
+ $table->foreignId('payment_config_id')->nullable()->constrained('payment_configs')->nullOnDelete()->comment('關聯金流參數組合');
+ $table->foreignId('machine_model_id')->nullable()->constrained('machine_models')->nullOnDelete()->comment('關類型號組合');
+ $table->foreignId('creator_id')->nullable()->constrained('users')->nullOnDelete()->comment('建立者');
+ $table->foreignId('updater_id')->nullable()->constrained('users')->nullOnDelete()->comment('修改者');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('machines', function (Blueprint $table) {
+ $table->dropForeign(['payment_config_id']);
+ $table->dropForeign(['machine_model_id']);
+ $table->dropForeign(['creator_id']);
+ $table->dropForeign(['updater_id']);
+
+ $table->dropColumn([
+ 'card_reader_seconds',
+ 'card_reader_checkout_time_1',
+ 'card_reader_checkout_time_2',
+ 'heating_start_time',
+ 'heating_end_time',
+ 'payment_buffer_seconds',
+ 'card_reader_no',
+ 'key_no',
+ 'invoice_status',
+ 'welcome_gift_enabled',
+ 'is_spring_slot_1_10',
+ 'is_spring_slot_11_20',
+ 'is_spring_slot_21_30',
+ 'is_spring_slot_31_40',
+ 'is_spring_slot_41_50',
+ 'is_spring_slot_51_60',
+ 'member_system_enabled',
+ 'payment_config_id',
+ 'machine_model_id',
+ 'creator_id',
+ 'updater_id',
+ ]);
+ });
+ }
+};
diff --git a/database/migrations/2026_03_17_131857_add_images_to_machines_table.php b/database/migrations/2026_03_17_131857_add_images_to_machines_table.php
new file mode 100644
index 0000000..5d6247c
--- /dev/null
+++ b/database/migrations/2026_03_17_131857_add_images_to_machines_table.php
@@ -0,0 +1,28 @@
+json('images')->after('firmware_version')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('machines', function (Blueprint $table) {
+ $table->dropColumn('images');
+ });
+ }
+};
diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php
index 1f466fe..b9d7fc3 100644
--- a/database/seeders/RoleSeeder.php
+++ b/database/seeders/RoleSeeder.php
@@ -31,9 +31,8 @@ class RoleSeeder extends Seeder
'menu.line',
'menu.reservation',
'menu.special-permission',
- 'menu.companies',
- 'menu.accounts',
- 'menu.roles',
+ 'menu.basic-settings',
+ 'menu.permissions',
];
foreach ($permissions as $permission) {
@@ -47,9 +46,23 @@ class RoleSeeder extends Seeder
);
$superAdmin->syncPermissions(Permission::all());
- Role::updateOrCreate(
+ $tenantAdmin = Role::updateOrCreate(
['name' => 'tenant-admin'],
- ['is_system' => true]
+ ['is_system' => false]
);
+ $tenantAdmin->syncPermissions([
+ 'menu.members',
+ 'menu.machines',
+ 'menu.app',
+ 'menu.warehouses',
+ 'menu.sales',
+ 'menu.analysis',
+ 'menu.audit',
+ 'menu.data-config',
+ 'menu.remote',
+ 'menu.line',
+ 'menu.reservation',
+ 'menu.special-permission',
+ ]);
}
}
diff --git a/docs/architecture_plan.md b/docs/architecture_plan.md
index 7c02008..0f46126 100644
--- a/docs/architecture_plan.md
+++ b/docs/architecture_plan.md
@@ -187,18 +187,59 @@ Spatie 預設的 roles 表必須加上多租戶設計,確保留戶只能管理
---
-#### 🟡 `machines` 表 — 需擴充欄位
+### 機台設定與參數模型 (Configuration & Parameters)
-現有 `machines` 表僅有基礎欄位,需為 B010 API 補充:
+為了提升機台的可維護性與金流設定的靈活性,系統引入了以下結構:
-| 新增欄位 | 類型 | 說明 | 來源 API |
-|----------|------|------|----------|
-| `serial_no` | `VARCHAR UNIQUE` | 機台序號(API 用此識別) | B010 `machine` |
-| `model` | `VARCHAR` | 機台型號 | B010 `M_Stus` |
-| `current_page` | `TINYINT` | 當前頁面狀態碼 | B010 `M_Stus2` |
-| `door_status` | `VARCHAR` | 門禁狀態 | B010 `door` |
-| `is_online` | `BOOLEAN` | 是否在線(心跳超時判斷) | 計算欄位 |
-| `api_token` | `VARCHAR` | 機台專屬 API Token | 取代硬編碼 key |
+#### 1. 機台型號設定表 (machine_models)
+記錄系統支援的機台型號,供機台綁定基礎參數。
+
+| 欄位 | 類型 | 說明 |
+|------|------|------|
+| `id` | BIGINT PK | — |
+| `name` | VARCHAR(255) | 型號名稱 (例如: "B010-STD") |
+| `company_id` | BIGINT FK | 所屬公司 (支援多租戶隔離) |
+| `creator_id / updater_id` | BIGINT FK | 審計記錄 (建立者/修改者) |
+| `timestamps` | — | — |
+
+---
+
+#### 2. 金流參數組合表 (payment_configs)
+**範本化設計**:由公司建立支付參數組合範本,機台端選擇套用。
+
+| 欄位 | 類型 | 說明 |
+|------|------|------|
+| `id` | BIGINT PK | — |
+| `company_id` | BIGINT FK | 所屬公司 |
+| `name` | VARCHAR(255) | 組合名稱 (例如: "玉山+綠界組合A") |
+| `settings` | **JSON** | 儲存各支付平台 API Key (Ecpay, Esun, Tappay, LinePay 等) |
+| `creator_id / updater_id` | BIGINT FK | 審計記錄 |
+| `timestamps` | — | — |
+
+---
+
+#### 3. 機台主表擴充 (machines)
+針對 `machines` 表擴充以下欄位以支援 IoT 通訊與進階營運設定:
+
+| 類別 | 欄位 | 類型 | 說明 |
+|------|------|------|------|
+| **基礎識別** | `serial_no` | `VARCHAR` | 機台序號 (API 唯一識別碼) |
+| **狀態監控** | `current_page` | `VARCHAR` | 機台當前畫面停留頁面 |
+| | `door_status` | `VARCHAR` | 門禁狀態 (open/closed) |
+| | `temperature` | `DECIMAL` | 機器溫度 (來自硬體回傳) |
+| **金流設定** | `payment_config_id` | `BIGINT FK` | 關聯金流參數樣本 (`payment_configs`) |
+| | `card_reader_seconds` | `INT` | 刷卡機秒數 (預設 30) |
+| | `card_reader_checkout_time_1 / 2` | `TIME` | 卡機結帳清機時間 |
+| | `payment_buffer_seconds` | `INT` | 金流回傳緩衝時間 (預設 5) |
+| **硬體與營運**| `machine_model_id` | `BIGINT FK` | 關聯機台型號 (`machine_models`) |
+| | `heating_start / end_time` | `TIME` | 加熱自動排程開啟與關閉時間 |
+| | `card_reader_no / key_no` | `VARCHAR` | 刷卡機編號與鑰匙編號 |
+| | `invoice_status` | `TINYINT` | 發票狀態 (0:不開, 1:預設捐, 2:預設不捐) |
+| | `welcome_gift_enabled` | `BOOLEAN` | 來店禮開關 |
+| | `member_system_enabled` | `BOOLEAN` | 會員系統開關 |
+| | `is_spring_slot_1_10` ~ `60` | `BOOLEAN` | 貨道類型標記 (0=履帶, 1=彈簧) |
+| **審計權限** | `company_id` | `BIGINT FK` | 關聯所屬公司 (租戶隔離) |
+| | `creator_id / updater_id` | `BIGINT FK` | 建立者與最後修改者 ID |
---
diff --git a/lang/en.json b/lang/en.json
index e756e9c..441b27d 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -140,13 +140,15 @@
"Clear Stock": "Clear Stock",
"APK Versions": "APK Versions",
"Discord Notifications": "Discord Notifications",
+ "Basic Settings": "Basic Settings",
+ "Machine Settings": "Machine Settings",
"Permission Settings": "Permission Settings",
"APP Features": "APP Features",
"Sales": "Sales",
"Others": "Others",
"AI Prediction": "AI Prediction",
- "Roles": "Roles",
- "Role Management": "Role Management",
+ "Roles": "Role Permissions",
+ "Role Management": "Role Permission Management",
"Define and manage security roles and permissions.": "Define and manage security roles and permissions.",
"Search roles...": "Search roles...",
"No permissions": "No permissions",
@@ -162,6 +164,11 @@
"Permissions": "Permissions",
"Users": "Users",
"System role name cannot be modified.": "System role name cannot be modified.",
+ "The Super Admin role name cannot be modified.": "The Super Admin role name cannot be modified.",
+ "System Level": "System Level",
+ "Company Level": "Company Level",
+ "Global roles accessible by all administrators.": "Global roles accessible by all administrators.",
+ "Roles scoped to specific customer companies.": "Roles scoped to specific customer companies.",
"members": "Member Management",
"machines": "Machine Management",
"app": "APP Management",
@@ -176,8 +183,9 @@
"special-permission": "Special Permission",
"companies": "Customer Management",
"accounts": "Account Management",
- "roles": "Role Settings",
- "Role Settings": "Role Settings",
+ "roles": "Role Permissions",
+ "Role Permissions": "Role Permissions",
+ "Role Settings": "Role Permissions",
"No login history yet": "No login history yet",
"Signed in as": "Signed in as",
"Logout": "Logout",
@@ -268,5 +276,7 @@
"Unknown": "Unknown",
"Info": "Info",
"Warning": "Warning",
+ "basic-settings": "Basic Settings",
+ "permissions": "Permission Settings",
"Error": "Error"
}
\ No newline at end of file
diff --git a/lang/ja.json b/lang/ja.json
index 1687864..b5be74c 100644
--- a/lang/ja.json
+++ b/lang/ja.json
@@ -140,13 +140,15 @@
"Clear Stock": "在庫クリア",
"APK Versions": "APKバージョン",
"Discord Notifications": "Discord通知",
+ "Basic Settings": "基本設定",
+ "Machine Settings": "機台設定",
"Permission Settings": "権限設定",
"APP Features": "APP機能",
"Sales": "販売",
"Others": "その他",
"AI Prediction": "AI予測",
- "Roles": "ロール",
- "Role Management": "ロール管理",
+ "Roles": "ロール権限",
+ "Role Management": "ロール権限管理",
"Define and manage security roles and permissions.": "システムのセキュリティロールと権限を定義および管理します。",
"Search roles...": "ロールを検索...",
"No permissions": "権限項目なし",
@@ -162,6 +164,8 @@
"Permissions": "権限",
"Users": "ユーザー数",
"System role name cannot be modified.": "システムロール名は変更できません。",
+ "System Level": "システムレベル",
+ "Company Level": "顧客レベル",
"Menu Permissions": "メニュー権限",
"Save Changes": "変更を保存",
"members": "会員管理",
@@ -178,8 +182,9 @@
"special-permission": "特別権限",
"companies": "顧客管理",
"accounts": "アカウント管理",
- "roles": "ロール設定",
- "Role Settings": "ロール設定",
+ "roles": "ロール権限",
+ "Role Permissions": "ロール権限",
+ "Role Settings": "ロール權限",
"No login history yet": "ログイン履歴はまだありません",
"Signed in as": "ログイン中",
"Logout": "ログアウト",
@@ -269,5 +274,7 @@
"Unknown": "不明",
"Info": "情報",
"Warning": "警告",
+ "basic-settings": "基本設定",
+ "permissions": "權限設定",
"Error": "エラー"
}
\ No newline at end of file
diff --git a/lang/zh_TW.json b/lang/zh_TW.json
index 261bbe6..9bb7c71 100644
--- a/lang/zh_TW.json
+++ b/lang/zh_TW.json
@@ -140,13 +140,15 @@
"Clear Stock": "庫存清空",
"APK Versions": "APK版本",
"Discord Notifications": "Discord通知",
+ "Basic Settings": "基本設定",
+ "Machine Settings": "機台設定",
"Permission Settings": "權限設定",
"APP Features": "APP功能",
"Sales": "銷售管理",
"Others": "其他功能",
"AI Prediction": "AI智能預測",
- "Roles": "角色設定",
- "Role Management": "角色管理",
+ "Roles": "角色權限",
+ "Role Management": "角色權限管理",
"Define and manage security roles and permissions.": "定義並管理系統安全角色與權限。",
"Search roles...": "搜尋角色...",
"No permissions": "無權限項目",
@@ -162,6 +164,11 @@
"Permissions": "權限",
"Users": "帳號數",
"System role name cannot be modified.": "內建系統角色的名稱無法修改。",
+ "The Super Admin role name cannot be modified.": "超級管理員角色的名稱無法修改。",
+ "System Level": "系統層級",
+ "Company Level": "客戶層級",
+ "Global roles accessible by all administrators.": "適用於所有管理者的全域角色。",
+ "Roles scoped to specific customer companies.": "適用於各個客戶單位的特定角色。",
"members": "會員管理",
"machines": "機台管理",
"app": "APP 管理",
@@ -176,8 +183,9 @@
"special-permission": "特殊權限",
"companies": "客戶管理",
"accounts": "帳號管理",
- "roles": "角色設定",
- "Role Settings": "角色設定",
+ "roles": "角色權限",
+ "Role Permissions": "角色權限",
+ "Role Settings": "角色權限",
"No login history yet": "尚無登入紀錄",
"Signed in as": "登入身份",
"Logout": "登出",
@@ -273,5 +281,78 @@
"Unknown": "未知",
"Info": "一般",
"Warning": "警告",
- "Error": "錯誤"
+ "Error": "錯誤",
+ "Management of operational parameters": "機台運作參數管理",
+ "Add Machine": "新增機台",
+ "Search machines...": "搜尋機台...",
+ "Items": "項",
+ "Machine Name": "機台名稱",
+ "Serial No": "機台序號",
+ "Owner": "所屬客戶",
+ "Model": "機台型號",
+ "Action": "操作",
+ "No location set": "尚未設定位置",
+ "Edit Settings": "編輯設定",
+ "Enter machine name": "請輸入機台名稱",
+ "Enter serial number": "請輸入機台序號",
+ "Select Owner": "請選擇所屬客戶",
+ "Select Model": "請選擇機台型號",
+ "Customer Payment Config": "客戶金流設定",
+ "Not Used": "不使用",
+ "Edit Machine Settings": "編輯機台設定",
+ "Operational Parameters": "運作參數",
+ "Card Reader Seconds": "刷卡機秒數",
+ "Payment Buffer Seconds": "金流緩衝時間(s)",
+ "Checkout Time 1": "卡機結帳時間1",
+ "Checkout Time 2": "卡機結帳時間2",
+ "Heating Start Time": "開啟-加熱時間",
+ "Heating End Time": "關閉-加熱時間",
+ "Hardware & Slots": "硬體與貨道設定",
+ "Card Reader No": "刷卡機編號",
+ "Key No": "鑰匙編號",
+ "Slot Mechanism (default: Conveyor, check for Spring)": "貨道類型 (預設履帶,勾選為彈簧)",
+ "Payment & Invoice": "金流與發票",
+ "Invoice Status": "發票狀態碼",
+ "No Invoice": "不開發票",
+ "Default Donate": "開發票預設捐",
+ "Default Not Donate": "開發票預設不捐",
+ "Member & External": "會員與外部系統",
+ "Welcome Gift": "來店禮開關",
+ "Enabled/Disabled": "啟用/禁用",
+ "Member System": "會員系統",
+ "Payment Configuration": "客戶金流設定",
+ "Merchant payment gateway settings management": "特約商店支付網關參數管理",
+ "Create Config": "建立配置",
+ "Config Name": "配置名稱",
+ "Last Updated": "最後更新日期",
+ "Are you sure you want to delete this configuration?": "您確定要刪除此金流配置嗎?",
+ "Create Payment Config": "建立金流配置",
+ "Define new third-party payment parameters": "定義新的第三方支付介接參數",
+ "Save Config": "儲存配置",
+ "Configuration Name": "金流組合名稱",
+ "Belongs To Company": "所屬客戶公司",
+ "ECPay Invoice": "綠界發票",
+ "Store ID": "特約商店代號 (MerchantID)",
+ "HashKey": "HashKey",
+ "HashIV": "HashIV",
+ "E.SUN QR Scan": "玉山掃碼",
+ "StoreID": "商店代號 (StoreID)",
+ "TermID": "終端代號 (TermID)",
+ "Key": "金鑰 (Key)",
+ "LINE Pay Direct": "Line官方支付",
+ "ChannelId": "ChannelId",
+ "ChannelSecret": "ChannelSecret",
+ "TapPay Integration": "TapPay 整合支付",
+ "PARTNER_KEY": "PARTNER_KEY",
+ "APP_ID": "APP_ID",
+ "APP_KEY": "APP_KEY",
+ "Merchant IDs": "特約商店代號 (Merchant IDs)",
+ "LINE_MERCHANT_ID": "LINE Pay 商店代號",
+ "JKO_MERCHANT_ID": "街口支付 商店代號",
+ "PI_MERCHANT_ID": "Pi 拍錢包 商店代號",
+ "PS_MERCHANT_ID": "全盈+Pay 商店代號",
+ "EASY_MERCHANT_ID": "悠遊付 商店代號",
+ "basic-settings": "基本設定",
+ "permissions": "權限設定",
+ "Edit Payment Config": "編輯金流配置"
}
\ No newline at end of file
diff --git a/resources/css/app.css b/resources/css/app.css
index 3ab8179..9d39296 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -99,9 +99,9 @@
@layer components {
.luxury-nav-item {
- @apply flex items-center gap-x-3.5 py-2.5 px-4 text-sm font-medium rounded-xl transition-all duration-200;
- @apply text-slate-500 hover:text-slate-900 hover:bg-slate-100;
- @apply dark:text-slate-400 dark:hover:text-white dark:hover:bg-white/5;
+ @apply flex items-center gap-x-3.5 py-2.5 px-4 text-sm font-semibold rounded-xl transition-all duration-200;
+ @apply text-slate-600 hover:text-slate-900 hover:bg-slate-100;
+ @apply dark:text-slate-300 dark:hover:text-white dark:hover:bg-white/5;
}
.luxury-nav-item.active {
@@ -184,13 +184,17 @@
@apply dark:ring-cyan-500/20 dark:border-cyan-400/50;
}
- /* Date Input Calendar Icon Optimization */
- .luxury-input[type="date"]::-webkit-calendar-picker-indicator {
+ /* Input Icon Optimization (Date/Time) */
+ .luxury-input[type="date"]::-webkit-calendar-picker-indicator,
+ .luxury-input[type="time"]::-webkit-calendar-picker-indicator,
+ .luxury-input[type="datetime-local"]::-webkit-calendar-picker-indicator {
@apply cursor-pointer transition-opacity hover:opacity-100;
opacity: 0.6;
}
- .dark .luxury-input[type="date"]::-webkit-calendar-picker-indicator {
+ .dark .luxury-input[type="date"]::-webkit-calendar-picker-indicator,
+ .dark .luxury-input[type="time"]::-webkit-calendar-picker-indicator,
+ .dark .luxury-input[type="datetime-local"]::-webkit-calendar-picker-indicator {
filter: invert(1);
}
diff --git a/resources/views/admin/basic-settings/machines/edit.blade.php b/resources/views/admin/basic-settings/machines/edit.blade.php
new file mode 100644
index 0000000..ced0f3a
--- /dev/null
+++ b/resources/views/admin/basic-settings/machines/edit.blade.php
@@ -0,0 +1,251 @@
+@extends('layouts.admin')
+
+@section('content')
+
+
+
+
+
+
+
+
+
{{ __('Edit Machine Settings') }}
+
{{ $machine->name }} / {{ $machine->serial_no }}
+
+
+
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/admin/basic-settings/machines/index.blade.php b/resources/views/admin/basic-settings/machines/index.blade.php
new file mode 100644
index 0000000..ba254af
--- /dev/null
+++ b/resources/views/admin/basic-settings/machines/index.blade.php
@@ -0,0 +1,317 @@
+@extends('layouts.admin')
+
+@section('content')
+
+
+
+
+
{{ __('Machine Settings') }}
+
{{ __('Management of operational parameters') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | {{ __('Machine Info') }} |
+ {{ __('Status') }} |
+ {{ __('Card Reader') }} |
+ {{ __('Owner') }} |
+ {{ __('Action') }} |
+
+
+
+ @foreach($machines as $machine)
+
+ toJson() }})">
+
+
+
+
+
+ {{ $machine->serial_no }}
+ •
+ {{ $machine->machineModel->name ?? '--' }}
+
+
+
+ |
+
+ @php
+ $isOnline = $machine->last_heartbeat_at && $machine->last_heartbeat_at->diffInMinutes() < 5;
+ @endphp
+
+
+ @if($isOnline)
+
+
+ @else
+
+ @endif
+
+
+ {{ $isOnline ? __('Online') : __('Offline') }}
+
+
+ |
+
+
+ {{ $machine->card_reader_seconds ?? 0 }}s / No.{{ $machine->card_reader_no ?? '--' }}
+
+ |
+
+
+ {{ $machine->company->name ?? __('None') }}
+
+ |
+
+
+
+
+
+ |
+
+ @endforeach
+
+
+
+
+
+
+ {{ $machines->links('vendor.pagination.luxury') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ __('Add Machine') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ __('Parameters') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/admin/basic-settings/payment-configs/create.blade.php b/resources/views/admin/basic-settings/payment-configs/create.blade.php
new file mode 100644
index 0000000..0aa4cee
--- /dev/null
+++ b/resources/views/admin/basic-settings/payment-configs/create.blade.php
@@ -0,0 +1,187 @@
+@extends('layouts.admin')
+
+@section('content')
+
+
+
+
+
+
+
+
+
{{ __('Create Payment Config') }}
+
{{ __('Define new third-party payment parameters') }}
+
+
+
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/admin/basic-settings/payment-configs/edit.blade.php b/resources/views/admin/basic-settings/payment-configs/edit.blade.php
new file mode 100644
index 0000000..f586d6b
--- /dev/null
+++ b/resources/views/admin/basic-settings/payment-configs/edit.blade.php
@@ -0,0 +1,191 @@
+@extends('layouts.admin')
+
+@section('content')
+
+
+
+
+
+
+
+
+
{{ __('Edit Payment Config') }}
+
{{ $paymentConfig->name }}
+
+
+
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/admin/basic-settings/payment-configs/index.blade.php b/resources/views/admin/basic-settings/payment-configs/index.blade.php
new file mode 100644
index 0000000..bfb7e84
--- /dev/null
+++ b/resources/views/admin/basic-settings/payment-configs/index.blade.php
@@ -0,0 +1,82 @@
+@extends('layouts.admin')
+
+@section('content')
+
+
+
+
+
{{ __('Payment Configuration') }}
+
{{ __('Merchant payment gateway settings management') }}
+
+
+
+
+
+
+
+
+
+
+ | {{ __('Config Name') }} |
+ {{ __('Belongs To') }} |
+ {{ __('Last Updated') }} |
+ {{ __('Action') }} |
+
+
+
+ @foreach($paymentConfigs as $config)
+
+
+ |
+
+ {{ $config->company->name ?? __('None') }}
+
+ |
+
+
+ {{ $config->updated_at->format('Y/m/d H:i') }}
+ {{ $config->updated_at->diffForHumans() }}
+
+ |
+
+
+
+
+
+ |
+
+ @endforeach
+
+
+
+
+
+ {{ $paymentConfigs->links('vendor.pagination.luxury') }}
+
+
+
+@endsection
diff --git a/resources/views/admin/companies/index.blade.php b/resources/views/admin/companies/index.blade.php
index 5321a16..786c87f 100644
--- a/resources/views/admin/companies/index.blade.php
+++ b/resources/views/admin/companies/index.blade.php
@@ -62,16 +62,16 @@