authorizeAbility($request, 'employees.view'); $organization = $this->organization($request); $employees = Employee::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) ->with(['branch', 'presence']) ->orderBy('full_name'); $this->scopeToBranch($request, $employees); $employees = $employees->paginate(25); return view('frontdesk.employees.index', compact('employees', 'organization')); } public function create(Request $request): View { $this->authorizeAbility($request, 'employees.manage'); $organization = $this->organization($request); return view('frontdesk.employees.create', [ 'organization' => $organization, 'branches' => $this->branches($request, $organization->id), 'hosts' => $this->hosts($request, $organization->id), ]); } public function store(Request $request, EmployeePresenceService $presence): RedirectResponse { $this->authorizeAbility($request, 'employees.manage'); $organization = $this->organization($request); $validated = $this->validatedEmployee($request); $pin = $validated['pin']; unset($validated['pin']); $employee = Employee::create([ 'owner_ref' => $this->ownerRef($request), 'organization_id' => $organization->id, 'pin_hash' => $presence->hashPin($pin), ...$this->employeePayload($validated), ]); $presence->presenceFor($employee); return redirect()->route('frontdesk.employees.index')->with('success', 'Employee added.'); } public function edit(Request $request, Employee $employee): View { $this->authorizeAbility($request, 'employees.manage'); $this->authorizeOwner($request, $employee); return view('frontdesk.employees.edit', [ 'employee' => $employee->load('presence'), 'branches' => $this->branches($request, $employee->organization_id), 'hosts' => $this->hosts($request, $employee->organization_id), ]); } public function update(Request $request, Employee $employee, EmployeePresenceService $presence): RedirectResponse { $this->authorizeAbility($request, 'employees.manage'); $this->authorizeOwner($request, $employee); $validated = $this->validatedEmployee($request, updating: true); $payload = $this->employeePayload($validated); if (! empty($validated['pin'])) { $payload['pin_hash'] = $presence->hashPin($validated['pin']); } $employee->update([ ...$payload, 'active' => $request->boolean('active', true), ]); return redirect()->route('frontdesk.employees.index')->with('success', 'Employee updated.'); } public function destroy(Request $request, Employee $employee): RedirectResponse { $this->authorizeAbility($request, 'employees.manage'); $this->authorizeOwner($request, $employee); $employee->delete(); return redirect()->route('frontdesk.employees.index')->with('success', 'Employee removed.'); } public function importForm(Request $request): View { $this->authorizeAbility($request, 'employees.manage'); $organization = $this->organization($request); return view('frontdesk.employees.import', compact('organization')); } public function import(Request $request, EmployeePresenceService $presence): RedirectResponse { $this->authorizeAbility($request, 'employees.manage'); $organization = $this->organization($request); $request->validate([ 'csv' => ['required', 'file', 'mimes:csv,txt', 'max:2048'], ]); $handle = fopen($request->file('csv')->getRealPath(), 'r'); if ($handle === false) { return back()->with('error', 'Could not read the CSV file.'); } $header = fgetcsv($handle); if ($header === false) { fclose($handle); return back()->with('error', 'CSV file is empty.'); } $header = array_map(fn ($col) => strtolower(trim((string) $col)), $header); $required = ['employee_code', 'full_name', 'pin']; foreach ($required as $column) { if (! in_array($column, $header, true)) { fclose($handle); return back()->with('error', "CSV must include columns: employee_code, full_name, pin."); } } $ownerRef = $this->ownerRef($request); $imported = 0; $rowNumber = 1; while (($row = fgetcsv($handle)) !== false) { $rowNumber++; if ($row === [null] || $row === []) { continue; } $data = array_combine($header, array_pad($row, count($header), null)); if ($data === false) { continue; } $code = trim((string) ($data['employee_code'] ?? '')); $name = trim((string) ($data['full_name'] ?? '')); $pin = trim((string) ($data['pin'] ?? '')); if ($code === '' || $name === '' || $pin === '') { continue; } if (! preg_match('/^\d{4,6}$/', $pin)) { continue; } $branchId = null; if (! empty($data['branch'])) { $branchId = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->where('name', trim((string) $data['branch'])) ->value('id'); } $employee = Employee::query()->updateOrCreate( [ 'organization_id' => $organization->id, 'employee_code' => $code, ], [ 'owner_ref' => $ownerRef, 'full_name' => $name, 'department' => trim((string) ($data['department'] ?? '')) ?: null, 'email' => trim((string) ($data['email'] ?? '')) ?: null, 'phone' => trim((string) ($data['phone'] ?? '')) ?: null, 'branch_id' => $branchId, 'pin_hash' => $presence->hashPin($pin), 'active' => true, ], ); $presence->presenceFor($employee); $imported++; } fclose($handle); return redirect()->route('frontdesk.employees.index')->with('success', "{$imported} employee(s) imported."); } public function badge(Request $request, Employee $employee, QrCodeService $qr): View { $this->authorizeAbility($request, 'employees.view'); $this->authorizeOwner($request, $employee); return view('frontdesk.employees.badge', [ 'employee' => $employee->load('organization'), 'qrSvg' => $qr->employeeSvg($employee), ]); } public function exportAttendance(Request $request): StreamedResponse { $this->authorizeAbility($request, 'reports.export'); $organization = $this->organization($request); $owner = $this->ownerRef($request); [$from, $to] = $this->dateRange($request); $branchId = app(\App\Services\Frontdesk\OrganizationResolver::class)->branchScope($this->member($request)); $query = EmployeePresenceEvent::owned($owner) ->where('organization_id', $organization->id) ->whereBetween('created_at', [$from, $to]) ->with(['employee']) ->orderBy('created_at'); if ($branchId) { $query->where('branch_id', $branchId); } $events = $query->get(); $filename = 'employee-attendance-'.$from->format('Y-m-d').'-'.$to->format('Y-m-d').'.csv'; return response()->streamDownload(function () use ($events) { $handle = fopen('php://output', 'w'); fputcsv($handle, ['Date', 'Time', 'Employee', 'Code', 'Department', 'Event', 'Destination', 'Notes']); foreach ($events as $event) { fputcsv($handle, [ $event->created_at->toDateString(), $event->created_at->format('H:i'), $event->employee->full_name, $event->employee->employee_code, $event->employee->department ?? '', config('frontdesk.employee_presence_events.'.$event->event, $event->event), $event->destination ?? '', $event->notes ?? '', ]); } fclose($handle); }, $filename, ['Content-Type' => 'text/csv']); } /** @return array{0: Carbon, 1: Carbon} */ protected function dateRange(Request $request): array { $from = $request->filled('from') ? Carbon::parse($request->string('from'))->startOfDay() : now()->subDays(30)->startOfDay(); $to = $request->filled('to') ? Carbon::parse($request->string('to'))->endOfDay() : now()->endOfDay(); return [$from, $to]; } /** @return array */ protected function validatedEmployee(Request $request, bool $updating = false): array { $pinRules = $updating ? ['nullable', 'string', 'regex:/^\d{4,6}$/'] : ['required', 'string', 'regex:/^\d{4,6}$/']; return $request->validate([ 'employee_code' => ['required', 'string', 'max:32'], 'full_name' => ['required', 'string', 'max:255'], 'department' => ['nullable', 'string', 'max:255'], 'email' => ['nullable', 'email', 'max:255'], 'phone' => ['nullable', 'string', 'max:50'], 'pin' => $pinRules, 'branch_id' => ['nullable', 'integer'], 'host_id' => ['nullable', 'integer'], 'user_ref' => ['nullable', 'string', 'max:64'], ]); } protected function employeePayload(array $validated): array { $payload = $validated; unset($payload['pin']); return array_filter($payload, fn ($value) => $value !== null && $value !== ''); } protected function branches(Request $request, int $organizationId) { return Branch::owned($this->ownerRef($request)) ->where('organization_id', $organizationId) ->where('is_active', true) ->orderBy('name') ->get(); } protected function hosts(Request $request, int $organizationId) { return Host::owned($this->ownerRef($request)) ->where('organization_id', $organizationId) ->orderBy('name') ->get(); } }