468 lines
17 KiB
PHP
468 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Users;
|
|
|
|
use Livewire\Component;
|
|
use App\Models\User;
|
|
use App\Models\Role;
|
|
use App\Models\Permission;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\Rules\Password;
|
|
|
|
class Edit extends Component
|
|
{
|
|
public User $user;
|
|
|
|
// User basic information
|
|
public $name = '';
|
|
public $email = '';
|
|
public $password = '';
|
|
public $password_confirmation = '';
|
|
public $employee_id = '';
|
|
public $phone = '';
|
|
public $department = '';
|
|
public $position = '';
|
|
public $branch_code = '';
|
|
public $hire_date = '';
|
|
public $salary = '';
|
|
public $status = 'active';
|
|
public $emergency_contact_name = '';
|
|
public $emergency_contact_phone = '';
|
|
public $address = '';
|
|
public $date_of_birth = '';
|
|
public $national_id = '';
|
|
|
|
// Role and permission management
|
|
public $selectedRoles = [];
|
|
public $selectedPermissions = [];
|
|
public $changePassword = false;
|
|
public $showDeleteModal = false;
|
|
|
|
// UI State
|
|
public $currentTab = 'profile';
|
|
public $saving = false;
|
|
public $showPasswordGenerator = false;
|
|
public $generatedPassword = '';
|
|
public $originalData = [];
|
|
|
|
protected function rules()
|
|
{
|
|
$rules = [
|
|
'name' => 'required|string|max:255|min:2',
|
|
'email' => 'required|email|unique:users,email,' . $this->user->id . '|max:255',
|
|
'employee_id' => 'nullable|string|max:50|unique:users,employee_id,' . $this->user->id . '|regex:/^[A-Z0-9-]+$/',
|
|
'phone' => 'nullable|string|max:20|regex:/^[\+]?[0-9\s\-\(\)]+$/',
|
|
'department' => 'nullable|string|max:100',
|
|
'position' => 'nullable|string|max:100',
|
|
'branch_code' => 'required|string|max:10|exists:branches,code',
|
|
'hire_date' => 'nullable|date|before_or_equal:today',
|
|
'salary' => 'nullable|numeric|min:0|max:999999.99',
|
|
'status' => 'required|in:active,inactive,suspended',
|
|
'emergency_contact_name' => 'nullable|string|max:255',
|
|
'emergency_contact_phone' => 'nullable|string|max:20|regex:/^[\+]?[0-9\s\-\(\)]+$/',
|
|
'address' => 'nullable|string|max:500',
|
|
'date_of_birth' => 'nullable|date|before:-18 years',
|
|
'national_id' => 'nullable|string|max:50|unique:users,national_id,' . $this->user->id,
|
|
'selectedRoles' => 'array',
|
|
'selectedRoles.*' => 'exists:roles,id',
|
|
'selectedPermissions' => 'array',
|
|
'selectedPermissions.*' => 'exists:permissions,id',
|
|
];
|
|
|
|
if ($this->changePassword) {
|
|
$rules['password'] = ['required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols()];
|
|
}
|
|
|
|
return $rules;
|
|
}
|
|
|
|
protected $messages = [
|
|
'name.min' => 'Name must be at least 2 characters long.',
|
|
'branch_code.required' => 'Branch code is required.',
|
|
'branch_code.exists' => 'Selected branch code does not exist.',
|
|
'email.unique' => 'This email address is already registered.',
|
|
'employee_id.unique' => 'This employee ID is already in use.',
|
|
'employee_id.regex' => 'Employee ID can only contain letters, numbers, and hyphens.',
|
|
'phone.regex' => 'Please enter a valid phone number.',
|
|
'emergency_contact_phone.regex' => 'Please enter a valid emergency contact phone number.',
|
|
'date_of_birth.before' => 'Employee must be at least 18 years old.',
|
|
'hire_date.before_or_equal' => 'Hire date cannot be in the future.',
|
|
'national_id.unique' => 'This national ID is already registered.',
|
|
'salary.max' => 'Salary cannot exceed 999,999.99.',
|
|
];
|
|
|
|
public function mount(User $user)
|
|
{
|
|
$this->user = $user;
|
|
|
|
// Store original data for change tracking
|
|
$this->originalData = [
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
'employee_id' => $user->employee_id,
|
|
'phone' => $user->phone,
|
|
'department' => $user->department,
|
|
'position' => $user->position,
|
|
'branch_code' => $user->branch_code,
|
|
'status' => $user->status,
|
|
];
|
|
|
|
// Load user data
|
|
$this->name = $user->name;
|
|
$this->email = $user->email;
|
|
$this->employee_id = $user->employee_id;
|
|
$this->phone = $user->phone;
|
|
$this->department = $user->department;
|
|
$this->position = $user->position;
|
|
$this->branch_code = $user->branch_code;
|
|
$this->hire_date = $user->hire_date ? $user->hire_date->format('Y-m-d') : '';
|
|
$this->salary = $user->salary;
|
|
$this->status = $user->status;
|
|
$this->emergency_contact_name = $user->emergency_contact_name;
|
|
$this->emergency_contact_phone = $user->emergency_contact_phone;
|
|
$this->address = $user->address;
|
|
$this->date_of_birth = $user->date_of_birth ? $user->date_of_birth->format('Y-m-d') : '';
|
|
$this->national_id = $user->national_id;
|
|
|
|
// Load current roles
|
|
$this->selectedRoles = $user->roles()
|
|
->where('user_roles.is_active', true)
|
|
->where(function ($q) {
|
|
$q->whereNull('user_roles.expires_at')
|
|
->orWhere('user_roles.expires_at', '>', now());
|
|
})
|
|
->pluck('roles.id')
|
|
->toArray();
|
|
|
|
// Load current direct permissions
|
|
$this->selectedPermissions = $user->permissions()
|
|
->where('user_permissions.granted', true)
|
|
->where(function ($q) {
|
|
$q->whereNull('user_permissions.expires_at')
|
|
->orWhere('user_permissions.expires_at', '>', now());
|
|
})
|
|
->pluck('permissions.id')
|
|
->toArray();
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$roles = Role::where('is_active', true)
|
|
->orderBy('display_name')
|
|
->get();
|
|
|
|
$permissions = Permission::where('is_active', true)
|
|
->orderBy('module')
|
|
->orderBy('name')
|
|
->get()
|
|
->groupBy('module');
|
|
|
|
$departments = User::select('department')
|
|
->distinct()
|
|
->whereNotNull('department')
|
|
->where('department', '!=', '')
|
|
->orderBy('department')
|
|
->pluck('department');
|
|
|
|
$branches = \DB::table('branches')
|
|
->where('is_active', true)
|
|
->orderBy('name')
|
|
->get(['code', 'name']);
|
|
|
|
$positions = $this->getPositionsForDepartment($this->department);
|
|
|
|
// Get user activity logs
|
|
$causedByUser = \Spatie\Activitylog\Models\Activity::where('causer_id', $this->user->id)
|
|
->where('causer_type', \App\Models\User::class)
|
|
->latest()
|
|
->limit(5)
|
|
->get();
|
|
|
|
$performedOnUser = \Spatie\Activitylog\Models\Activity::where('subject_id', $this->user->id)
|
|
->where('subject_type', \App\Models\User::class)
|
|
->latest()
|
|
->limit(5)
|
|
->get();
|
|
|
|
$recentActivity = $causedByUser->merge($performedOnUser)
|
|
->sortByDesc('created_at')
|
|
->take(10);
|
|
|
|
return view('livewire.users.edit', [
|
|
'availableRoles' => $roles,
|
|
'permissions' => $permissions,
|
|
'departments' => $departments,
|
|
'branches' => $branches,
|
|
'positions' => $positions,
|
|
'recentActivity' => $recentActivity,
|
|
]);
|
|
}
|
|
|
|
public function save()
|
|
{
|
|
$this->saving = true;
|
|
$this->validate();
|
|
|
|
DB::beginTransaction();
|
|
try {
|
|
// Prepare update data
|
|
$userData = [
|
|
'name' => trim($this->name),
|
|
'email' => strtolower(trim($this->email)),
|
|
'employee_id' => $this->employee_id ? strtoupper(trim($this->employee_id)) : null,
|
|
'phone' => $this->phone ? preg_replace('/[^0-9+\-\s\(\)]/', '', $this->phone) : null,
|
|
'department' => $this->department ?: null,
|
|
'position' => $this->position ?: null,
|
|
'branch_code' => $this->branch_code,
|
|
'hire_date' => $this->hire_date ?: null,
|
|
'salary' => $this->salary ?: null,
|
|
'status' => $this->status,
|
|
'emergency_contact_name' => trim($this->emergency_contact_name) ?: null,
|
|
'emergency_contact_phone' => $this->emergency_contact_phone ? preg_replace('/[^0-9+\-\s\(\)]/', '', $this->emergency_contact_phone) : null,
|
|
'address' => trim($this->address) ?: null,
|
|
'date_of_birth' => $this->date_of_birth ?: null,
|
|
'national_id' => $this->national_id ? trim($this->national_id) : null,
|
|
'updated_by' => auth()->id(),
|
|
];
|
|
|
|
// Update password if requested
|
|
if ($this->changePassword && $this->password) {
|
|
$userData['password'] = Hash::make($this->password);
|
|
$userData['password_changed_at'] = now();
|
|
}
|
|
|
|
// Track changes for activity log
|
|
$changes = $this->getChangedData($userData);
|
|
|
|
$this->user->update($userData);
|
|
|
|
// Sync roles with branch code
|
|
$roleData = [];
|
|
foreach ($this->selectedRoles as $roleId) {
|
|
$roleData[$roleId] = [
|
|
'branch_code' => $this->branch_code,
|
|
'is_active' => true,
|
|
'assigned_at' => now(),
|
|
'expires_at' => null,
|
|
];
|
|
}
|
|
$this->user->roles()->sync($roleData);
|
|
|
|
// Sync direct permissions
|
|
$permissionData = [];
|
|
foreach ($this->selectedPermissions as $permissionId) {
|
|
$permissionData[$permissionId] = [
|
|
'granted' => true,
|
|
'branch_code' => $this->branch_code,
|
|
'assigned_at' => now(),
|
|
'expires_at' => null,
|
|
];
|
|
}
|
|
$this->user->permissions()->sync($permissionData);
|
|
|
|
// Log the update with changes
|
|
if (!empty($changes) || $this->changePassword) {
|
|
$logProperties = ['changes' => $changes];
|
|
if ($this->changePassword) {
|
|
$logProperties['password_changed'] = true;
|
|
}
|
|
|
|
activity()
|
|
->performedOn($this->user)
|
|
->causedBy(auth()->user())
|
|
->withProperties($logProperties)
|
|
->log('User updated');
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
session()->flash('success', "User '{$this->user->name}' updated successfully!");
|
|
$this->saving = false;
|
|
|
|
return redirect()->route('users.show', $this->user);
|
|
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
$this->saving = false;
|
|
|
|
\Log::error('Failed to update user', [
|
|
'user_id' => $this->user->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
session()->flash('error', 'Failed to update user: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function cancel()
|
|
{
|
|
return redirect()->route('users.show', $this->user);
|
|
}
|
|
|
|
public function generatePassword()
|
|
{
|
|
$this->generatedPassword = $this->generateSecurePassword();
|
|
$this->password = $this->generatedPassword;
|
|
$this->password_confirmation = $this->generatedPassword;
|
|
$this->changePassword = true;
|
|
$this->showPasswordGenerator = true;
|
|
}
|
|
|
|
public function generateSecurePassword($length = 12)
|
|
{
|
|
// Generate a secure password with mixed case, numbers, and symbols
|
|
$uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
$lowercase = 'abcdefghijklmnopqrstuvwxyz';
|
|
$numbers = '0123456789';
|
|
$symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
|
|
|
$password = '';
|
|
$password .= $uppercase[random_int(0, strlen($uppercase) - 1)];
|
|
$password .= $lowercase[random_int(0, strlen($lowercase) - 1)];
|
|
$password .= $numbers[random_int(0, strlen($numbers) - 1)];
|
|
$password .= $symbols[random_int(0, strlen($symbols) - 1)];
|
|
|
|
$allChars = $uppercase . $lowercase . $numbers . $symbols;
|
|
for ($i = 4; $i < $length; $i++) {
|
|
$password .= $allChars[random_int(0, strlen($allChars) - 1)];
|
|
}
|
|
|
|
return str_shuffle($password);
|
|
}
|
|
|
|
public function resetPassword()
|
|
{
|
|
|
|
try {
|
|
$newPassword = \Str::random(12);
|
|
$this->user->update([
|
|
'password' => Hash::make($newPassword)
|
|
]);
|
|
|
|
// TODO: Send password reset email
|
|
// $this->user->notify(new PasswordResetNotification($newPassword));
|
|
|
|
session()->flash('success', 'Password reset successfully. New password: ' . $newPassword);
|
|
} catch (\Exception $e) {
|
|
session()->flash('error', 'Failed to reset password: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function impersonateUser()
|
|
{
|
|
|
|
if ($this->user->id === auth()->id()) {
|
|
session()->flash('error', 'You cannot impersonate yourself.');
|
|
return;
|
|
}
|
|
|
|
// Store original user ID for returning later
|
|
session(['impersonate_original_user' => auth()->id()]);
|
|
auth()->loginUsingId($this->user->id);
|
|
|
|
session()->flash('success', 'Now impersonating ' . $this->user->name);
|
|
return redirect()->route('dashboard');
|
|
}
|
|
|
|
public function getAvailableRolesProperty()
|
|
{
|
|
return Role::orderBy('name')->get();
|
|
}
|
|
|
|
public function confirmDelete()
|
|
{
|
|
$this->showDeleteModal = true;
|
|
}
|
|
|
|
public function deleteUser()
|
|
{
|
|
|
|
// Prevent self-deletion
|
|
if ($this->user->id === auth()->id()) {
|
|
session()->flash('error', 'You cannot delete your own account.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$this->user->delete();
|
|
session()->flash('success', 'User deleted successfully.');
|
|
return redirect()->route('users.index');
|
|
} catch (\Exception $e) {
|
|
session()->flash('error', 'Failed to delete user: ' . $e->getMessage());
|
|
}
|
|
|
|
$this->showDeleteModal = false;
|
|
}
|
|
|
|
public function setActiveTab($tab)
|
|
{
|
|
$this->currentTab = $tab;
|
|
}
|
|
|
|
public function getChangedData($newData)
|
|
{
|
|
$changes = [];
|
|
foreach ($newData as $key => $value) {
|
|
if (isset($this->originalData[$key]) && $this->originalData[$key] != $value) {
|
|
$changes[$key] = [
|
|
'old' => $this->originalData[$key],
|
|
'new' => $value
|
|
];
|
|
}
|
|
}
|
|
return $changes;
|
|
}
|
|
|
|
public function getPositionsForDepartment($department)
|
|
{
|
|
$positions = [
|
|
'Service' => ['Service Advisor', 'Service Manager', 'Service Coordinator', 'Service Writer'],
|
|
'Technician' => ['Lead Technician', 'Senior Technician', 'Junior Technician', 'Apprentice Technician'],
|
|
'Parts' => ['Parts Manager', 'Parts Associate', 'Inventory Specialist', 'Parts Counter Person'],
|
|
'Administration' => ['Administrator', 'Office Manager', 'Receptionist', 'Data Entry Clerk'],
|
|
'Management' => ['General Manager', 'Assistant Manager', 'Supervisor', 'Team Lead'],
|
|
'Sales' => ['Sales Manager', 'Sales Associate', 'Sales Coordinator'],
|
|
'Finance' => ['Finance Manager', 'Accountant', 'Cashier', 'Billing Specialist'],
|
|
];
|
|
|
|
return $positions[$department] ?? [];
|
|
}
|
|
|
|
public function updatedDepartment()
|
|
{
|
|
// Clear position when department changes unless it's valid for new department
|
|
$validPositions = $this->getPositionsForDepartment($this->department);
|
|
if (!in_array($this->position, $validPositions)) {
|
|
$this->position = '';
|
|
}
|
|
}
|
|
|
|
public function hasUnsavedChanges()
|
|
{
|
|
$currentData = [
|
|
'name' => $this->name,
|
|
'email' => $this->email,
|
|
'employee_id' => $this->employee_id,
|
|
'phone' => $this->phone,
|
|
'department' => $this->department,
|
|
'position' => $this->position,
|
|
'branch_code' => $this->branch_code,
|
|
'status' => $this->status,
|
|
];
|
|
|
|
return !empty($this->getChangedData($currentData)) || $this->changePassword;
|
|
}
|
|
|
|
public function copyPasswordToClipboard()
|
|
{
|
|
// This will be handled by Alpine.js on the frontend
|
|
$this->dispatch('password-copied');
|
|
}
|
|
|
|
public function togglePasswordVisibility()
|
|
{
|
|
$this->showPasswordGenerator = !$this->showPasswordGenerator;
|
|
}
|
|
}
|