Files
GSB-Construction/Modules/UserManagement/app/Http/Controllers/UserController.php
Ajjj 936ae1c6b2
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled
feat: implement comprehensive dashboard controller and modular administrative and project management interfaces
2026-08-03 18:28:52 +08:00

500 lines
21 KiB
PHP

<?php
namespace Modules\UserManagement\Http\Controllers;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Inertia\Response;
use Modules\ContractorManagement\Models\Contractor;
use Modules\UserManagement\Models\CustomerProfile;
use Modules\UserManagement\Models\EmployeeProfile;
class UserController extends Controller
{
public function index(Request $request): Response
{
$query = User::with(['roles', 'employeeProfile', 'customerProfile', 'contractor']);
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
}
if ($request->filled('type') && $request->type !== 'all') {
$query->ofType($request->type);
}
if ($request->filled('status') && $request->status !== 'all') {
$query->where('status', $request->status);
}
$users = $query->latest()->paginate(15)->withQueryString();
return Inertia::render('UserManagement::Users/Index', [
'users' => $users,
'filters' => $request->only(['search', 'type', 'status']),
]);
}
public function create(): Response
{
$isSuperAdmin = auth()->user()->hasRole('Super Admin');
// Auto-generate the next employee code (EMP-XXXX)
$last = EmployeeProfile::where('employee_code', 'like', 'EMP-%')
->orderByRaw("CAST(SUBSTR(employee_code, 5) AS INTEGER) DESC")
->value('employee_code');
$nextNumber = $last ? ((int) substr($last, 4)) + 1 : 1;
$nextCode = 'EMP-' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
return Inertia::render('UserManagement::Users/Create', [
'isSuperAdmin' => $isSuperAdmin,
'nextEmployeeCode' => $nextCode,
'contractors' => $isSuperAdmin
? Contractor::where('status', 'active')
->orderBy('company_name')
->get(['id', 'company_name', 'type'])
: [],
]);
}
public function store(Request $request): RedirectResponse
{
$isSuperAdmin = auth()->user()->hasRole('Super Admin');
$allowedRoles = $isSuperAdmin ? ['admin', 'contractor', 'employee', 'customer'] : ['contractor', 'employee', 'customer'];
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'user_type' => ['required', Rule::in($allowedRoles)],
'status' => ['required', Rule::in(['active', 'inactive', 'suspended'])],
'invite_method' => ['required', Rule::in(['direct', 'email'])],
'auto_password' => ['nullable', 'boolean'],
'password' => ['nullable', 'string', 'min:8', 'confirmed', 'required_if:auto_password,false'],
// Platform admin only: pick the contractor for this user
'contractor_id' => $isSuperAdmin ? ['nullable', 'exists:contractors,id'] : ['prohibited'],
// Employee fields
'department' => ['nullable', 'string', 'max:255'],
'position' => ['nullable', 'string', 'max:255'],
'employee_code' => ['nullable', 'string', 'max:255', 'unique:employee_profiles'],
'hire_date' => ['nullable', 'date'],
'phone' => ['nullable', 'string', 'max:50'],
'address' => ['nullable', 'string'],
// Customer fields
'company_name' => ['nullable', 'string', 'max:255'],
'contact_person' => ['nullable', 'string', 'max:255'],
'tin_number' => ['nullable', 'string', 'max:255'],
'spatie_role' => ['nullable', 'string', Rule::in(['Super Admin', 'admin', 'Contractor', 'Contractor Admin', 'Project Manager', 'Designer', 'Site Technical', 'Construction Supervisor'])],
]);
// Resolve contractor_id: contractor admins always use their own;
// platform admins use the one selected in the form (null = platform user)
$contractorId = $isSuperAdmin
? ($validated['contractor_id'] ?? null)
: auth()->user()->contractor_id;
// Resolve temporary password
$plainPassword = $request->boolean('auto_password')
? Str::random(10)
: $validated['password'];
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($plainPassword),
'user_type' => $validated['user_type'],
'status' => $validated['status'],
'contractor_id' => $contractorId,
'must_change_password' => true,
]);
// Determine Spatie role to assign
$roleToAssign = $validated['spatie_role'] ?? $validated['user_type'];
if ($roleToAssign === 'Contractor Admin') {
$roleToAssign = 'Contractor';
} elseif ($roleToAssign === 'admin') {
$roleToAssign = 'Main Contractor Admin';
}
// Explicitly assign the spatie role matching the user type or selected role
if ($roleToAssign) {
$user->assignRole($roleToAssign);
}
// Create profile
$this->createProfile($user, $validated);
// Deliver credentials
if ($validated['invite_method'] === 'email') {
$this->deliverByEmail($user, $plainPassword);
}
$flashMessage = $validated['invite_method'] === 'direct'
? "User created. Temporary password: {$plainPassword}"
: "User invited. Credentials sent to {$user->email}.";
return redirect()->route('users.index')->with('success', $flashMessage);
}
public function show(User $user): Response
{
$this->authorizeScopedUser($user);
$user->load(['roles', 'permissions', 'employeeProfile', 'customerProfile']);
return Inertia::render('UserManagement::Users/Show', [
'user' => $user,
]);
}
public function edit(User $user): Response
{
$this->authorizeScopedUser($user);
$user->load(['employeeProfile', 'customerProfile', 'contractor']);
$isPlatformAdmin = is_null(auth()->user()->contractor_id);
return Inertia::render('UserManagement::Users/Edit', [
'user' => $user,
'contractors' => $isPlatformAdmin
? Contractor::where('status', 'active')
->orderBy('company_name')
->get(['id', 'company_name', 'type'])
: [],
'isPlatformAdmin' => $isPlatformAdmin,
]);
}
public function update(Request $request, User $user): RedirectResponse
{
$this->authorizeScopedUser($user);
$isPlatformAdmin = is_null(auth()->user()->contractor_id);
$allowedRoles = $isPlatformAdmin ? ['admin', 'contractor', 'employee', 'customer'] : ['contractor', 'employee', 'customer'];
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
'user_type' => ['required', Rule::in($allowedRoles)],
'status' => ['required', Rule::in(['active', 'inactive', 'suspended'])],
'department' => ['nullable', 'string', 'max:255'],
'position' => ['nullable', 'string', 'max:255'],
'employee_code' => ['nullable', 'string', 'max:255', Rule::unique('employee_profiles')->ignore($user->employeeProfile?->id)],
'hire_date' => ['nullable', 'date'],
'phone' => ['nullable', 'string', 'max:50'],
'address' => ['nullable', 'string'],
'company_name' => ['nullable', 'string', 'max:255'],
'contact_person'=> ['nullable', 'string', 'max:255'],
'tin_number' => ['nullable', 'string', 'max:255'],
'spatie_role' => ['nullable', 'string', Rule::in(['Super Admin', 'Contractor', 'Project Manager', 'Designer', 'Site Technical', 'Construction Supervisor'])],
]);
$userData = [
'name' => $validated['name'],
'email' => $validated['email'],
'user_type' => $validated['user_type'],
'status' => $validated['status'],
];
if (! empty($validated['password'])) {
$userData['password'] = Hash::make($validated['password']);
}
$user->update($userData);
$roleToAssign = $validated['spatie_role'] ?? $validated['user_type'];
if ($roleToAssign === 'admin') {
$roleToAssign = 'Super Admin';
}
$user->syncRoles([$roleToAssign]);
// Update profile
if (in_array($validated['user_type'], ['admin', 'employee', 'contractor'])) {
$profile = $user->employeeProfile()->updateOrCreate(
['user_id' => $user->id],
[
'department' => $validated['department'] ?? null,
'position' => $validated['position'] ?? null,
'employee_code' => $validated['employee_code'] ?? null,
'hire_date' => $validated['hire_date'] ?? null,
'phone' => $validated['phone'] ?? null,
'address' => $validated['address'] ?? null,
]
);
$user->update(['userable_type' => EmployeeProfile::class, 'userable_id' => $profile->id]);
} elseif ($validated['user_type'] === 'customer') {
$profile = $user->customerProfile()->updateOrCreate(
['user_id' => $user->id],
[
'company_name' => $validated['company_name'] ?? null,
'contact_person'=> $validated['contact_person'] ?? null,
'tin_number' => $validated['tin_number'] ?? null,
'phone' => $validated['phone'] ?? null,
'address' => $validated['address'] ?? null,
]
);
$user->update(['userable_type' => CustomerProfile::class, 'userable_id' => $profile->id]);
}
return redirect()->route('users.index')->with('success', 'User updated successfully.');
}
public function destroy(User $user): RedirectResponse
{
$this->authorizeScopedUser($user);
if ($user->id === auth()->id()) {
return back()->with('error', 'You cannot delete your own account.');
}
$user->delete();
return redirect()->route('users.index')->with('success', 'User deleted successfully.');
}
public function toggleStatus(User $user): RedirectResponse
{
$this->authorizeScopedUser($user);
if ($user->id === auth()->id()) {
return back()->with('error', 'You cannot change your own status.');
}
$newStatus = $user->status === 'active' ? 'suspended' : 'active';
$user->update(['status' => $newStatus]);
return back()->with('success', "User status changed to {$newStatus}.");
}
public function linkContractor(Request $request, User $user): RedirectResponse
{
// Only platform admins may link/unlink contractors
abort_unless(is_null(auth()->user()->contractor_id), 403, 'Only platform admins can link contractors.');
$this->authorizeScopedUser($user);
$validated = $request->validate([
'contractor_id' => ['nullable', 'exists:contractors,id'],
]);
$user->update(['contractor_id' => $validated['contractor_id'] ?? null]);
$message = $validated['contractor_id']
? 'Contractor linked to user successfully.'
: 'Contractor link removed from user.';
return back()->with('success', $message);
}
// ─── Bulk Import ──────────────────────────────────────────────────────────
public function bulkImportForm(): Response
{
$isPlatformAdmin = is_null(auth()->user()->contractor_id);
return Inertia::render('UserManagement::Users/BulkImport', [
'contractors' => $isPlatformAdmin
? Contractor::where('status', 'active')
->orderBy('company_name')
->get(['id', 'company_name', 'type'])
: [],
]);
}
public function bulkImport(Request $request): Response|RedirectResponse
{
$isPlatformAdmin = is_null(auth()->user()->contractor_id);
$request->validate([
'file' => ['required', 'file', 'mimes:csv,txt', 'max:2048'],
'invite_method' => ['required', Rule::in(['direct', 'email'])],
'contractor_id' => $isPlatformAdmin ? ['nullable', 'exists:contractors,id'] : ['prohibited'],
]);
$contractorId = $isPlatformAdmin
? ($request->contractor_id ?? null)
: auth()->user()->contractor_id;
$file = $request->file('file');
$handle = fopen($file->getRealPath(), 'r');
$headers = fgetcsv($handle); // skip header row
if (! $headers) {
return back()->with('error', 'CSV file is empty or unreadable.');
}
$created = [];
$errors = [];
$rowNum = 1;
while (($row = fgetcsv($handle)) !== false) {
$rowNum++;
if (count($row) < 2) {
$errors[] = "Row {$rowNum}: Not enough columns.";
continue;
}
[$name, $email, $role, $department, $position] = array_pad($row, 5, null);
$name = trim($name);
$email = trim($email);
$role = trim($role ?: 'employee');
$department= trim($department ?? '');
$position = trim($position ?? '');
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Row {$rowNum}: Invalid email '{$email}'.";
continue;
}
if (User::where('email', $email)->exists()) {
$errors[] = "Row {$rowNum}: Email '{$email}' already exists.";
continue;
}
$allowedRoles = ['Super Admin', 'Contractor', 'Project Manager', 'Designer', 'Site Technical', 'Construction Supervisor'];
$resolvedRole = in_array($role, $allowedRoles) ? $role : 'Designer';
// Map the Spatie role to the base user_type
$userType = $resolvedRole;
if (in_array($resolvedRole, ['Contractor', 'Project Manager', 'Designer', 'Site Technical', 'Construction Supervisor'])) {
$userType = 'contractor';
}
$plainPassword = Str::random(10);
$user = User::create([
'name' => $name,
'email' => $email,
'password' => Hash::make($plainPassword),
'user_type' => $userType,
'status' => 'active',
'contractor_id' => $contractorId,
'must_change_password' => true,
]);
$user->assignRole($resolvedRole);
if (in_array($userType, ['admin', 'employee', 'contractor'])) {
$profile = EmployeeProfile::create([
'user_id' => $user->id,
'department' => $department ?: null,
'position' => $position ?: null,
]);
$user->update(['userable_type' => EmployeeProfile::class, 'userable_id' => $profile->id]);
}
if ($request->invite_method === 'email') {
$this->deliverByEmail($user, $plainPassword);
$created[] = ['name' => $name, 'email' => $email, 'role' => $resolvedRole, 'password' => '(sent via email)'];
} else {
$created[] = ['name' => $name, 'email' => $email, 'role' => $resolvedRole, 'password' => $plainPassword];
}
}
fclose($handle);
return Inertia::render('UserManagement::Users/BulkImport', [
'results' => [
'created' => $created,
'errors' => $errors,
],
]);
}
// ─── Forced Password Change ────────────────────────────────────────────────
public function changePasswordForm(): Response
{
return Inertia::render('UserManagement::Users/ChangePassword');
}
public function changePassword(Request $request): RedirectResponse
{
$request->validate([
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
auth()->user()->update([
'password' => Hash::make($request->password),
'must_change_password' => false,
]);
return redirect()->route('dashboard')->with('success', 'Password updated successfully. Welcome aboard!');
}
// ─── Helpers ──────────────────────────────────────────────────────────────
/**
* Redirect contractor admins away from users outside their scope (friendly 404 alternative).
*/
private function authorizeScopedUser(User $user): void
{
$authUser = auth()->user();
if (! is_null($authUser->contractor_id) && $user->contractor_id !== $authUser->contractor_id) {
abort(redirect()->route('users.index')->with('error', 'You do not have permission to access this user.'));
}
}
private function createProfile(User $user, array $validated): void
{
if (in_array($validated['user_type'], ['admin', 'employee', 'contractor'])) {
// Server-side strict generation & lock for employee_code
$last = EmployeeProfile::where('employee_code', 'like', 'EMP-%')
->orderByRaw("CAST(SUBSTR(employee_code, 5) AS INTEGER) DESC")
->value('employee_code');
$nextNumber = $last ? ((int) substr($last, 4)) + 1 : 1;
$employeeCode = 'EMP-' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
$profile = EmployeeProfile::create([
'user_id' => $user->id,
'department' => $validated['department'] ?? null,
'position' => $validated['position'] ?? null,
'employee_code' => $employeeCode,
'hire_date' => $validated['hire_date'] ?? null,
'phone' => $validated['phone'] ?? null,
'address' => $validated['address'] ?? null,
]);
$user->update(['userable_type' => EmployeeProfile::class, 'userable_id' => $profile->id]);
} elseif ($validated['user_type'] === 'customer') {
$profile = CustomerProfile::create([
'user_id' => $user->id,
'company_name' => $validated['company_name'] ?? null,
'contact_person'=> $validated['contact_person'] ?? null,
'tin_number' => $validated['tin_number'] ?? null,
'phone' => $validated['phone'] ?? null,
'address' => $validated['address'] ?? null,
]);
$user->update(['userable_type' => CustomerProfile::class, 'userable_id' => $profile->id]);
}
}
private function deliverByEmail(User $user, string $plainPassword): void
{
try {
Mail::raw(
"Hello {$user->name},\n\nYou have been added to the system.\n\nEmail: {$user->email}\nTemporary Password: {$plainPassword}\n\nPlease log in and change your password immediately.",
fn ($m) => $m->to($user->email)->subject('Your Account Credentials')
);
} catch (\Exception $e) {
// Fallback: log credentials if mail is not configured
Log::info("User invite credentials (mail not configured) — Email: {$user->email} | Temp Password: {$plainPassword}");
}
}
}