500 lines
21 KiB
PHP
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}");
|
|
}
|
|
}
|
|
}
|