chore: 完善模組化架構遷移與修復前端顯示錯誤
- 修正所有模組 Controller 的 Model 引用路徑 (App\Modules\...) - 更新 ProductionOrder 與 ProductionOrderItem 模型結構以符合新版邏輯 - 修復 resources/js/utils/format.ts 在處理空值時導致 toLocaleString 崩潰的問題 - 清除全域路徑與 Controller 遷移殘留檔案
This commit is contained in:
127
app/Modules/Core/Controllers/ActivityLogController.php
Normal file
127
app/Modules/Core/Controllers/ActivityLogController.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Core\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class ActivityLogController extends Controller
|
||||
{
|
||||
private function getSubjectMap()
|
||||
{
|
||||
return [
|
||||
'App\Modules\Core\Models\User' => '使用者',
|
||||
'App\Modules\Core\Models\Role' => '角色',
|
||||
'App\Modules\Inventory\Models\Product' => '商品',
|
||||
'App\Modules\Procurement\Models\Vendor' => '廠商',
|
||||
'App\Modules\Inventory\Models\Category' => '商品分類',
|
||||
'App\Modules\Inventory\Models\Unit' => '單位',
|
||||
'App\Modules\Procurement\Models\PurchaseOrder' => '採購單',
|
||||
'App\Modules\Inventory\Models\Warehouse' => '倉庫',
|
||||
'App\Modules\Inventory\Models\Inventory' => '庫存',
|
||||
'App\Modules\Finance\Models\UtilityFee' => '公共事業費',
|
||||
];
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$perPage = $request->input('per_page', 10);
|
||||
$sortBy = $request->input('sort_by', 'created_at');
|
||||
$sortOrder = $request->input('sort_order', 'desc');
|
||||
|
||||
$search = $request->input('search');
|
||||
$dateStart = $request->input('date_start');
|
||||
$dateEnd = $request->input('date_end');
|
||||
$event = $request->input('event');
|
||||
$subjectType = $request->input('subject_type');
|
||||
$causerId = $request->input('causer_id');
|
||||
|
||||
$query = Activity::with('causer');
|
||||
|
||||
if ($search) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('description', 'like', "%{$search}%")
|
||||
->orWhere('log_name', 'like', "%{$search}%")
|
||||
->orWhere('properties', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($dateStart) {
|
||||
$query->whereDate('created_at', '>=', $dateStart);
|
||||
}
|
||||
|
||||
if ($dateEnd) {
|
||||
$query->whereDate('created_at', '<=', $dateEnd);
|
||||
}
|
||||
|
||||
if ($event) {
|
||||
$query->where('event', $event);
|
||||
}
|
||||
|
||||
if ($subjectType) {
|
||||
$query->where('subject_type', $subjectType);
|
||||
}
|
||||
|
||||
if ($causerId) {
|
||||
$query->where('causer_id', $causerId);
|
||||
}
|
||||
|
||||
if ($sortBy === 'created_at') {
|
||||
$query->orderBy($sortBy, $sortOrder);
|
||||
} else {
|
||||
$query->latest();
|
||||
}
|
||||
|
||||
$activities = $query->paginate($perPage)
|
||||
->through(function ($activity) {
|
||||
$subjectMap = $this->getSubjectMap();
|
||||
|
||||
$eventMap = [
|
||||
'created' => '新增',
|
||||
'updated' => '更新',
|
||||
'deleted' => '刪除',
|
||||
];
|
||||
|
||||
return [
|
||||
'id' => $activity->id,
|
||||
'description' => $eventMap[$activity->event] ?? $activity->event,
|
||||
'subject_type' => $subjectMap[$activity->subject_type] ?? class_basename($activity->subject_type),
|
||||
'event' => $activity->event,
|
||||
'causer' => $activity->causer ? $activity->causer->name : 'System',
|
||||
'created_at' => $activity->created_at->format('Y-m-d H:i:s'),
|
||||
'properties' => $activity->properties,
|
||||
];
|
||||
});
|
||||
|
||||
// Prepare subject types for frontend filter
|
||||
$subjectTypes = collect($this->getSubjectMap())->map(function ($label, $value) {
|
||||
return ['label' => $label, 'value' => $value];
|
||||
})->values();
|
||||
|
||||
// Get users for causer filter
|
||||
$users = \App\Modules\Core\Models\User::select('id', 'name')->orderBy('name')->get()
|
||||
->map(function ($user) {
|
||||
return ['label' => $user->name, 'value' => (string) $user->id];
|
||||
});
|
||||
|
||||
return Inertia::render('Admin/ActivityLog/Index', [
|
||||
'activities' => $activities,
|
||||
'filters' => [
|
||||
'per_page' => $request->input('per_page', '10'),
|
||||
'sort_by' => $request->input('sort_by'),
|
||||
'sort_order' => $request->input('sort_order'),
|
||||
'search' => $request->input('search'),
|
||||
'date_start' => $request->input('date_start'),
|
||||
'date_end' => $request->input('date_end'),
|
||||
'event' => $request->input('event'),
|
||||
'subject_type' => $request->input('subject_type'),
|
||||
'causer_id' => $request->input('causer_id'),
|
||||
],
|
||||
'subject_types' => $subjectTypes,
|
||||
'users' => $users,
|
||||
]);
|
||||
}
|
||||
}
|
||||
76
app/Modules/Core/Controllers/Auth/LoginController.php
Normal file
76
app/Modules/Core/Controllers/Auth/LoginController.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Core\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the login view.
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
$centralDomains = config('tenancy.central_domains', []);
|
||||
|
||||
// [Hack] Demo 環境特殊規則
|
||||
$demoPort = config('tenancy.demo_tenant_port');
|
||||
if ((!$demoPort || request()->getPort() != $demoPort) && in_array(request()->getHost(), $centralDomains)) {
|
||||
return Inertia::render('Landlord/Auth/Login');
|
||||
}
|
||||
|
||||
return Inertia::render('Auth/Login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'username' => ['required', 'string'],
|
||||
'password' => ['required', 'string'],
|
||||
], [
|
||||
'username.required' => '請輸入帳號',
|
||||
'password.required' => '請輸入密碼',
|
||||
]);
|
||||
|
||||
$credentials = $request->only('username', 'password');
|
||||
|
||||
if (Auth::attempt($credentials, $request->boolean('remember'))) {
|
||||
$request->session()->regenerate();
|
||||
|
||||
$centralDomains = config('tenancy.central_domains', []);
|
||||
$centralDomains = config('tenancy.central_domains', []);
|
||||
// [Hack] Demo 環境特殊規則
|
||||
$demoPort = config('tenancy.demo_tenant_port');
|
||||
if ((!$demoPort || $request->getPort() != $demoPort) && in_array($request->getHost(), $centralDomains)) {
|
||||
return redirect()->intended(route('landlord.dashboard'));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard'));
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'username' => '帳號或密碼錯誤。',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
53
app/Modules/Core/Controllers/DashboardController.php
Normal file
53
app/Modules/Core/Controllers/DashboardController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Core\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Inventory\Models\Product;
|
||||
use App\Modules\Procurement\Models\Vendor;
|
||||
use App\Modules\Procurement\Models\PurchaseOrder;
|
||||
use App\Modules\Inventory\Models\Warehouse;
|
||||
use App\Modules\Inventory\Models\Inventory;
|
||||
use App\Modules\Inventory\Models\WarehouseProductSafetyStock;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$centralDomains = config('tenancy.central_domains', []);
|
||||
|
||||
$demoPort = config('tenancy.demo_tenant_port');
|
||||
if ((!$demoPort || request()->getPort() != $demoPort) && in_array(request()->getHost(), $centralDomains)) {
|
||||
return redirect()->route('landlord.dashboard');
|
||||
}
|
||||
|
||||
// 計算低庫存數量:各商品在各倉庫的總量 < 安全庫存
|
||||
$lowStockCount = DB::table('warehouse_product_safety_stocks as ss')
|
||||
->join(DB::raw('(SELECT warehouse_id, product_id, SUM(quantity) as total_qty FROM inventories WHERE deleted_at IS NULL GROUP BY warehouse_id, product_id) as inv'),
|
||||
function ($join) {
|
||||
$join->on('ss.warehouse_id', '=', 'inv.warehouse_id')
|
||||
->on('ss.product_id', '=', 'inv.product_id');
|
||||
})
|
||||
->whereRaw('inv.total_qty <= ss.safety_stock')
|
||||
->count();
|
||||
|
||||
$stats = [
|
||||
'productsCount' => Product::count(),
|
||||
'vendorsCount' => Vendor::count(),
|
||||
'purchaseOrdersCount' => PurchaseOrder::count(),
|
||||
'warehousesCount' => Warehouse::count(),
|
||||
'totalInventoryValue' => Inventory::join('products', 'inventories.product_id', '=', 'products.id')
|
||||
->sum('inventories.quantity'),
|
||||
'pendingOrdersCount' => PurchaseOrder::where('status', 'pending')->count(),
|
||||
'lowStockCount' => $lowStockCount,
|
||||
];
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
'stats' => $stats,
|
||||
]);
|
||||
}
|
||||
}
|
||||
55
app/Modules/Core/Controllers/ProfileController.php
Normal file
55
app/Modules/Core/Controllers/ProfileController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Core\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* 顯示使用者設定頁面
|
||||
*/
|
||||
public function edit(Request $request)
|
||||
{
|
||||
return Inertia::render('Profile/Edit', [
|
||||
'user' => $request->user(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新使用者基本資料
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'username' => ['required', 'string', 'max:255', 'unique:users,username,' . $request->user()->id],
|
||||
'email' => ['nullable', 'string', 'email', 'max:255', 'unique:users,email,' . $request->user()->id],
|
||||
]);
|
||||
|
||||
$request->user()->update($validated);
|
||||
|
||||
return back()->with('success', '個人資料已更新');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新密碼
|
||||
*/
|
||||
public function updatePassword(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', 'confirmed', Password::defaults()],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
return back()->with('success', '密碼已更新');
|
||||
}
|
||||
}
|
||||
210
app/Modules/Core/Controllers/RoleController.php
Normal file
210
app/Modules/Core/Controllers/RoleController.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Core\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RoleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$sortBy = $request->input('sort_by', 'id');
|
||||
$sortOrder = $request->input('sort_order', 'asc');
|
||||
|
||||
$query = Role::withCount('users', 'permissions')
|
||||
->with('users:id,name,username');
|
||||
|
||||
// Handle sorting
|
||||
if (in_array($sortBy, ['users_count', 'permissions_count', 'created_at', 'id'])) {
|
||||
$query->orderBy($sortBy, $sortOrder);
|
||||
} else {
|
||||
$query->orderBy('id', 'asc');
|
||||
}
|
||||
|
||||
$roles = $query->get();
|
||||
|
||||
return Inertia::render('Admin/Role/Index', [
|
||||
'roles' => $roles,
|
||||
'filters' => $request->only(['sort_by', 'sort_order']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$permissions = $this->getGroupedPermissions();
|
||||
|
||||
return Inertia::render('Admin/Role/Create', [
|
||||
'groupedPermissions' => $permissions
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255', 'unique:roles,name'],
|
||||
'display_name' => ['required', 'string', 'max:255'],
|
||||
'permissions' => ['array'],
|
||||
'permissions.*' => ['exists:permissions,name']
|
||||
]);
|
||||
|
||||
$role = Role::create([
|
||||
'name' => $validated['name'],
|
||||
'display_name' => $validated['display_name']
|
||||
]);
|
||||
|
||||
if (!empty($validated['permissions'])) {
|
||||
$role->syncPermissions($validated['permissions']);
|
||||
}
|
||||
|
||||
return redirect()->route('roles.index')->with('success', '角色建立成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(string $id)
|
||||
{
|
||||
$role = Role::with('permissions')->findOrFail($id);
|
||||
|
||||
// 禁止編輯超級管理員角色
|
||||
if ($role->name === 'super-admin') {
|
||||
return redirect()->route('roles.index')->with('error', '超級管理員角色不可編輯');
|
||||
}
|
||||
|
||||
$groupedPermissions = $this->getGroupedPermissions();
|
||||
$currentPermissions = $role->permissions->pluck('name')->toArray();
|
||||
|
||||
return Inertia::render('Admin/Role/Edit', [
|
||||
'role' => $role,
|
||||
'groupedPermissions' => $groupedPermissions,
|
||||
'currentPermissions' => $currentPermissions
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
$role = Role::findOrFail($id);
|
||||
|
||||
if ($role->name === 'super-admin') {
|
||||
return redirect()->route('roles.index')->with('error', '超級管理員角色不可變更');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255', Rule::unique('roles', 'name')->ignore($role->id)],
|
||||
'display_name' => ['required', 'string', 'max:255'],
|
||||
'permissions' => ['array'],
|
||||
'permissions.*' => ['exists:permissions,name']
|
||||
]);
|
||||
|
||||
$role->update([
|
||||
'name' => $validated['name'],
|
||||
'display_name' => $validated['display_name']
|
||||
]);
|
||||
|
||||
if (isset($validated['permissions'])) {
|
||||
$role->syncPermissions($validated['permissions']);
|
||||
}
|
||||
|
||||
return redirect()->route('roles.index')->with('success', '角色更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(string $id)
|
||||
{
|
||||
$role = Role::withCount('users')->findOrFail($id);
|
||||
|
||||
if ($role->name === 'super-admin') {
|
||||
return back()->with('error', '超級管理員角色不可刪除');
|
||||
}
|
||||
|
||||
if ($role->users_count > 0) {
|
||||
return back()->with('error', "尚有 {$role->users_count} 位使用者屬於此角色,無法刪除");
|
||||
}
|
||||
|
||||
$role->delete();
|
||||
|
||||
return redirect()->route('roles.index')->with('success', '角色已刪除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得並分組權限
|
||||
*/
|
||||
private function getGroupedPermissions()
|
||||
{
|
||||
$allPermissions = Permission::orderBy('name')->get();
|
||||
$grouped = [];
|
||||
|
||||
foreach ($allPermissions as $permission) {
|
||||
$parts = explode('.', $permission->name);
|
||||
$group = $parts[0];
|
||||
$action = $parts[1] ?? '';
|
||||
|
||||
// 特定權限遷移邏輯
|
||||
if ($permission->name === 'inventory.transfer') {
|
||||
$group = 'warehouses'; // 調撥功能移至倉庫管理下
|
||||
}
|
||||
|
||||
if (!isset($grouped[$group])) {
|
||||
$grouped[$group] = [];
|
||||
}
|
||||
|
||||
$grouped[$group][] = $permission;
|
||||
}
|
||||
|
||||
// 依照側邊欄順序定義
|
||||
$groupDefinitions = [
|
||||
'products' => '商品資料管理',
|
||||
'warehouses' => '倉庫管理',
|
||||
'inventory' => '庫存管理',
|
||||
'vendors' => '廠商資料管理',
|
||||
'purchase_orders' => '採購單管理',
|
||||
'users' => '使用者管理',
|
||||
'roles' => '角色與權限',
|
||||
'utility_fees' => '公共事業費管理',
|
||||
'accounting' => '會計報表',
|
||||
];
|
||||
|
||||
$result = [];
|
||||
foreach ($groupDefinitions as $key => $displayName) {
|
||||
if (isset($grouped[$key])) {
|
||||
$result[] = [
|
||||
'key' => $key,
|
||||
'name' => $displayName,
|
||||
'permissions' => $grouped[$key]
|
||||
];
|
||||
unset($grouped[$key]); // 從待處理中移除
|
||||
}
|
||||
}
|
||||
|
||||
// 處理剩餘未定義在 groupDefinitions 中的群組 (安全機制)
|
||||
foreach ($grouped as $key => $permissions) {
|
||||
$result[] = [
|
||||
'key' => $key,
|
||||
'name' => ucfirst($key),
|
||||
'permissions' => $permissions
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
245
app/Modules/Core/Controllers/UserController.php
Normal file
245
app/Modules/Core/Controllers/UserController.php
Normal file
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Core\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Core\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$perPage = $request->input('per_page', 10);
|
||||
$sortBy = $request->input('sort_by', 'id');
|
||||
$sortOrder = $request->input('sort_order', 'asc');
|
||||
$search = $request->input('search');
|
||||
$roleId = $request->input('role');
|
||||
|
||||
$query = User::with(['roles:id,name,display_name']);
|
||||
|
||||
// Handle Search
|
||||
if ($search) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
->orWhere('username', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Role Filter
|
||||
if ($roleId && $roleId !== 'all') {
|
||||
$query->whereHas('roles', function ($q) use ($roleId) {
|
||||
$q->where('id', $roleId);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle sorting
|
||||
if (in_array($sortBy, ['name', 'created_at'])) {
|
||||
$query->orderBy($sortBy, $sortOrder);
|
||||
} else {
|
||||
$query->orderBy('id', 'asc');
|
||||
}
|
||||
|
||||
$users = $query->paginate($perPage)->withQueryString();
|
||||
$roles = Role::select('id', 'name', 'display_name')->get();
|
||||
|
||||
return Inertia::render('Admin/User/Index', [
|
||||
'users' => $users,
|
||||
'roles' => $roles,
|
||||
'filters' => $request->only(['per_page', 'sort_by', 'sort_order', 'search', 'role']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$roles = Role::pluck('display_name', 'name');
|
||||
|
||||
return Inertia::render('Admin/User/Create', [
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['nullable', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'username' => ['required', 'string', 'max:255', 'unique:users'],
|
||||
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||
'roles' => ['array'],
|
||||
], [
|
||||
'password.required' => '請輸入密碼',
|
||||
'password.min' => '密碼長度至少需 :min 個字元',
|
||||
'password.confirmed' => '密碼確認不符',
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'username' => $validated['username'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
if (!empty($validated['roles'])) {
|
||||
$user->syncRoles($validated['roles']);
|
||||
|
||||
// Update the 'created' log to include roles
|
||||
$activity = \Spatie\Activitylog\Models\Activity::where('subject_type', get_class($user))
|
||||
->where('subject_id', $user->id)
|
||||
->where('event', 'created')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($activity) {
|
||||
$roleNames = $user->roles()->pluck('display_name')->join(', ');
|
||||
$properties = $activity->properties->toArray();
|
||||
$properties['attributes']['role_id'] = $roleNames;
|
||||
$activity->properties = $properties;
|
||||
$activity->save();
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('users.index')->with('success', '使用者建立成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(string $id)
|
||||
{
|
||||
$user = User::with('roles')->findOrFail($id);
|
||||
$roles = Role::get(['id', 'name', 'display_name']);
|
||||
|
||||
return Inertia::render('Admin/User/Edit', [
|
||||
'user' => $user,
|
||||
'roles' => $roles,
|
||||
'currentRoles' => $user->getRoleNames()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['nullable', 'string', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'username' => ['required', 'string', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
|
||||
'roles' => ['array'],
|
||||
], [
|
||||
'password.min' => '密碼長度至少需 :min 個字元',
|
||||
'password.confirmed' => '密碼確認不符',
|
||||
]);
|
||||
|
||||
// 1. Prepare data and detect changes
|
||||
$userData = [
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'username' => $validated['username'],
|
||||
];
|
||||
|
||||
if (!empty($validated['password'])) {
|
||||
$userData['password'] = Hash::make($validated['password']);
|
||||
}
|
||||
|
||||
$user->fill($userData);
|
||||
|
||||
// Capture dirty attributes for manual logging
|
||||
$dirty = $user->getDirty();
|
||||
$oldAttributes = [];
|
||||
$newAttributes = [];
|
||||
|
||||
foreach ($dirty as $key => $value) {
|
||||
$oldAttributes[$key] = $user->getOriginal($key);
|
||||
$newAttributes[$key] = $value;
|
||||
}
|
||||
|
||||
// Save without triggering events (prevents duplicate log)
|
||||
$user->saveQuietly();
|
||||
|
||||
// 2. Handle Roles
|
||||
$roleChanges = null;
|
||||
if (isset($validated['roles'])) {
|
||||
$oldRoles = $user->roles()->pluck('display_name')->join(', ');
|
||||
$user->syncRoles($validated['roles']);
|
||||
$newRoles = $user->roles()->pluck('display_name')->join(', ');
|
||||
|
||||
if ($oldRoles !== $newRoles) {
|
||||
$roleChanges = [
|
||||
'old' => $oldRoles,
|
||||
'new' => $newRoles
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Manually Log activity (Single Consolidated Log)
|
||||
if (!empty($newAttributes) || $roleChanges) {
|
||||
$properties = [
|
||||
'attributes' => $newAttributes,
|
||||
'old' => $oldAttributes,
|
||||
];
|
||||
|
||||
if ($roleChanges) {
|
||||
$properties['attributes']['role_id'] = $roleChanges['new'];
|
||||
$properties['old']['role_id'] = $roleChanges['old'];
|
||||
}
|
||||
|
||||
activity()
|
||||
->performedOn($user)
|
||||
->causedBy(auth()->user())
|
||||
->event('updated')
|
||||
->withProperties($properties)
|
||||
->tap(function (\Spatie\Activitylog\Contracts\Activity $activity) use ($user) {
|
||||
// Manually add snapshot since we aren't using the model's LogOptions due to saveQuietly
|
||||
$activity->properties = $activity->properties->merge([
|
||||
'snapshot' => [
|
||||
'name' => $user->name,
|
||||
'username' => $user->username,
|
||||
]
|
||||
]);
|
||||
})
|
||||
->log('updated');
|
||||
}
|
||||
|
||||
return redirect()->route('users.index')->with('success', '使用者更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(string $id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
if ($user->hasRole('super-admin')) {
|
||||
return back()->with('error', '無法刪除超級管理員帳號');
|
||||
}
|
||||
|
||||
if ($user->id === auth()->id()) {
|
||||
return back()->with('error', '無法刪除自己');
|
||||
}
|
||||
|
||||
$user->delete();
|
||||
|
||||
return redirect()->route('users.index')->with('success', '使用者已刪除');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user