sackey a65fee9d75
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
Add customer portal workflow progress component and analytics dashboard
- Implemented the customer portal workflow progress component with detailed service progress tracking, including current status, workflow steps, and contact information.
- Developed a management workflow analytics dashboard featuring key performance indicators, charts for revenue by branch, labor utilization, and recent quality issues.
- Created tests for admin-only middleware to ensure proper access control for admin routes.
- Added tests for customer portal view rendering and workflow integration, ensuring the workflow service operates correctly through various stages.
- Introduced a .gitignore file for the debugbar storage directory to prevent unnecessary files from being tracked.
2025-08-10 19:41:25 +00:00

465 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 App\Models\Branch;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Validation\Rules\Password;
use Illuminate\Support\Str;
class Create extends Component
{
// 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 $sendWelcomeEmail = true;
public $sendCredentials = false;
// UI State
public $showPasswordGenerator = false;
public $generatedPassword = '';
public $currentStep = 1;
public $totalSteps = 4;
public $saving = false;
// Advanced options
public $requirePasswordChange = true;
public $temporaryPassword = false;
public $accountExpiry = '';
public $notes = '';
protected function rules()
{
return [
'name' => 'required|string|max:255|min:2',
'email' => 'required|email|unique:users,email|max:255',
'password' => ['required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols()],
'employee_id' => 'nullable|string|max:50|unique:users,employee_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',
'selectedRoles' => 'array|min:1',
'selectedRoles.*' => 'exists:roles,id',
'selectedPermissions' => 'array',
'selectedPermissions.*' => 'exists:permissions,id',
'accountExpiry' => 'nullable|date|after:today',
'notes' => 'nullable|string|max:1000',
];
}
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.',
'selectedRoles.min' => 'Please assign at least one role to the user.',
'salary.max' => 'Salary cannot exceed 999,999.99.',
'accountExpiry.after' => 'Account expiry must be in the future.',
'notes.max' => 'Notes cannot exceed 1000 characters.',
];
public function mount()
{
$this->hire_date = now()->format('Y-m-d');
$this->branch_code = auth()->user()->branch_code ?? '';
// Auto-generate employee ID if needed
$this->generateEmployeeId();
}
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 = Branch::where('is_active', true)
->orderBy('name')
->get(['code', 'name']);
$positions = $this->getPositionsForDepartment($this->department);
return view('livewire.users.create', [
'roles' => $roles,
'permissions' => $permissions,
'departments' => $departments,
'branches' => $branches,
'positions' => $positions,
]);
}
public function generateEmployeeId()
{
if (empty($this->employee_id) && !empty($this->branch_code)) {
$lastEmployee = User::where('branch_code', $this->branch_code)
->where('employee_id', 'like', $this->branch_code . '%')
->orderByDesc('employee_id')
->first();
if ($lastEmployee) {
$number = (int) substr($lastEmployee->employee_id, strlen($this->branch_code)) + 1;
} else {
$number = 1;
}
$this->employee_id = $this->branch_code . str_pad($number, 4, '0', STR_PAD_LEFT);
}
}
public function updatedBranchCode()
{
$this->generateEmployeeId();
}
public function generatePassword()
{
$this->generatedPassword = Str::random(12);
$this->password = $this->generatedPassword;
$this->password_confirmation = $this->generatedPassword;
$this->showPasswordGenerator = false;
$this->temporaryPassword = true;
$this->requirePasswordChange = true;
}
public function applyRolePreset($preset)
{
$this->selectedRoles = [];
$this->selectedPermissions = [];
switch ($preset) {
case 'manager':
$roles = Role::whereIn('name', ['manager', 'service_supervisor'])->get();
break;
case 'technician':
$roles = Role::whereIn('name', ['technician'])->get();
break;
case 'receptionist':
$roles = Role::whereIn('name', ['receptionist', 'customer_service'])->get();
break;
case 'service_coordinator':
$roles = Role::whereIn('name', ['service_coordinator'])->get();
break;
default:
$roles = collect();
}
$this->selectedRoles = $roles->pluck('id')->toArray();
}
public function nextStep()
{
$this->validateCurrentStep();
if ($this->currentStep < $this->totalSteps) {
$this->currentStep++;
}
}
public function previousStep()
{
if ($this->currentStep > 1) {
$this->currentStep--;
}
}
public function validateCurrentStep()
{
$rules = $this->rules();
switch ($this->currentStep) {
case 1: // Basic Information
$stepRules = [
'name' => $rules['name'],
'email' => $rules['email'],
'employee_id' => $rules['employee_id'],
'branch_code' => $rules['branch_code'],
];
break;
case 2: // Employment Details
$stepRules = [
'department' => $rules['department'],
'position' => $rules['position'],
'hire_date' => $rules['hire_date'],
'salary' => $rules['salary'],
];
break;
case 3: // Security & Access
$stepRules = [
'password' => $rules['password'],
'selectedRoles' => $rules['selectedRoles'],
'selectedRoles.*' => $rules['selectedRoles.*'],
];
break;
case 4: // Additional Information
$stepRules = [
'phone' => $rules['phone'],
'address' => $rules['address'],
'emergency_contact_name' => $rules['emergency_contact_name'],
'emergency_contact_phone' => $rules['emergency_contact_phone'],
];
break;
default:
$stepRules = [];
}
$this->validate($stepRules);
}
public function getPositionsForDepartment($department)
{
if (empty($department)) return [];
// Get positions from existing users in the same department
$existingPositions = User::select('position')
->where('department', $department)
->distinct()
->whereNotNull('position')
->where('position', '!=', '')
->orderBy('position')
->pluck('position')
->toArray();
// Common positions by department
$commonPositions = [
'Service' => ['Service Advisor', 'Service Manager', 'Service Coordinator', 'Service Writer'],
'Technical' => ['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'],
];
$predefinedPositions = $commonPositions[$department] ?? [];
// Merge and deduplicate
$allPositions = array_unique(array_merge($existingPositions, $predefinedPositions));
sort($allPositions);
return $allPositions;
}
public function save()
{
$this->saving = true;
$this->validate();
DB::beginTransaction();
try {
// Create the user
$user = User::create([
'name' => trim($this->name),
'email' => strtolower(trim($this->email)),
'password' => Hash::make($this->password),
'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,
'email_verified_at' => now(),
'created_by' => auth()->id(),
]);
// Assign roles
if (!empty($this->selectedRoles)) {
foreach ($this->selectedRoles as $roleId) {
$role = Role::find($roleId);
if ($role) {
$user->assignRole($role, $this->branch_code);
}
}
}
// Assign direct permissions
if (!empty($this->selectedPermissions)) {
foreach ($this->selectedPermissions as $permissionId) {
$permission = Permission::find($permissionId);
if ($permission) {
$user->givePermission($permission, $this->branch_code);
}
}
}
// Log the creation
activity()
->performedOn($user)
->causedBy(auth()->user())
->withProperties([
'user_data' => [
'name' => $user->name,
'email' => $user->email,
'employee_id' => $user->employee_id,
'department' => $user->department,
'branch_code' => $user->branch_code,
'status' => $user->status,
],
'roles_assigned' => $this->selectedRoles,
'permissions_assigned' => $this->selectedPermissions,
])
->log('User created');
// Send welcome email if requested
if ($this->sendWelcomeEmail) {
try {
// TODO: Implement welcome email notification
// $user->notify(new WelcomeNotification($this->password));
} catch (\Exception $e) {
// Log email failure but don't fail the user creation
\Log::warning('Failed to send welcome email to user: ' . $user->email, ['error' => $e->getMessage()]);
}
}
DB::commit();
session()->flash('success', "User '{$user->name}' created successfully!");
$this->saving = false;
return redirect()->route('users.show', $user);
} catch (\Exception $e) {
DB::rollBack();
$this->saving = false;
\Log::error('Failed to create user', [
'error' => $e->getMessage(),
'user_data' => [
'name' => $this->name,
'email' => $this->email,
'employee_id' => $this->employee_id,
]
]);
session()->flash('error', 'Failed to create user: ' . $e->getMessage());
}
}
public function cancel()
{
return redirect()->route('users.index');
}
public function updatedDepartment()
{
// Clear position when department changes
$this->position = '';
}
public function validateStep1()
{
$this->validateOnly([
'name',
'email',
'password',
'password_confirmation',
'employee_id',
'phone',
]);
}
public function validateStep2()
{
$this->validateOnly([
'department',
'position',
'branch_code',
'hire_date',
'salary',
'status',
]);
}
public function copyPasswordToClipboard()
{
// This will be handled by Alpine.js on the frontend
$this->dispatch('password-copied');
}
public function getRolePermissionCount($roleId)
{
$role = Role::find($roleId);
return $role ? $role->permissions()->count() : 0;
}
public function getSelectedRolesPermissions()
{
if (empty($this->selectedRoles)) {
return collect();
}
return Permission::whereHas('roles', function($query) {
$query->whereIn('roles.id', $this->selectedRoles);
})->get();
}
public function hasValidationErrors()
{
return $this->getErrorBag()->isNotEmpty();
}
public function getProgressPercentage()
{
return ($this->currentStep / $this->totalSteps) * 100;
}
}