buildViewModel((string) $request->input('search', '')); $viewModel->saved = $request->input('saved') === '1'; return $this->view('employees.create', [ 'model' => $viewModel, 'pageTitle' => $viewModel->title, ]); } public function store() { $request = Request::capture(); $form = $this->sanitizeFormData($request); $errors = $this->validateForm($form, $request); if (empty($errors) && $this->employees()->findByEmail($form['email']) !== null) { $errors['email'][] = 'That email address is already in use.'; } if (!empty($errors)) { $viewModel = $this->buildViewModel(); $viewModel->form = $form; $viewModel->errors = $errors; if ($this->isHtmxRequest($request)) { return $this->fragment('employees.partials.form', [ 'model' => $viewModel, ]); } return $this->view('employees.create', [ 'model' => $viewModel, 'pageTitle' => $viewModel->title, ]); } $employee = new Employee(); $employee->firstName = $form['first_name']; $employee->lastName = $form['last_name']; $employee->email = $form['email']; $employee->department = $form['department']; $employee->jobTitle = $form['job_title']; $employee->startDate = $form['start_date']; $this->employees()->create($employee); if ($this->isHtmxRequest($request)) { $viewModel = $this->buildViewModel(); $viewModel->saved = true; return $this->fragment('employees.partials.form', [ 'model' => $viewModel, ], 200, [ 'HX-Trigger' => json_encode(['employees-changed' => true]), ]); } return $this->redirect('/employees?saved=1'); } public function create() { return $this->redirect('/employees'); } public function summary() { $request = Request::capture(); $viewModel = $this->buildViewModel((string) $request->input('search', '')); return $this->fragment('employees.partials.summary', [ 'model' => $viewModel, ]); } public function data() { $request = Request::capture(); $search = trim((string) $request->input('search', '')); $rows = $this->employees()->search($search); $data = array_map( static function (array $row): array { return [ 'id' => (int) $row['id'], 'full_name' => trim($row['first_name'] . ' ' . $row['last_name']), 'first_name' => (string) $row['first_name'], 'last_name' => (string) $row['last_name'], 'email' => (string) $row['email'], 'department' => (string) $row['department'], 'job_title' => (string) $row['job_title'], 'start_date' => (string) $row['start_date'], 'created_at' => (string) $row['created_at'], ]; }, $rows ); return $this->json($data); } /** * @return array */ private function sanitizeFormData(Request $request): array { return [ 'first_name' => trim((string) $request->input('first_name', '')), 'last_name' => trim((string) $request->input('last_name', '')), 'email' => trim((string) $request->input('email', '')), 'department' => trim((string) $request->input('department', '')), 'job_title' => trim((string) $request->input('job_title', '')), 'start_date' => trim((string) $request->input('start_date', '')), ]; } /** * @param array $form * @return array> */ private function validateForm(array $form, Request $request): array { $validator = new Validator(); $validator ->required('first_name', $form['first_name'], 'First name is required.') ->maxLength('first_name', $form['first_name'], 100, 'First name must be 100 characters or fewer.') ->required('last_name', $form['last_name'], 'Last name is required.') ->maxLength('last_name', $form['last_name'], 100, 'Last name must be 100 characters or fewer.') ->required('email', $form['email'], 'Email is required.') ->maxLength('email', $form['email'], 255, 'Email must be 255 characters or fewer.') ->required('department', $form['department'], 'Department is required.') ->maxLength('department', $form['department'], 100, 'Department must be 100 characters or fewer.') ->required('job_title', $form['job_title'], 'Job title is required.') ->maxLength('job_title', $form['job_title'], 150, 'Job title must be 150 characters or fewer.') ->required('start_date', $form['start_date'], 'Start date is required.'); $errors = $validator->errors(); if (!verify_csrf_token((string) $request->input('_token', ''))) { $errors['_token'][] = 'Your form session expired. Please refresh the page and try again.'; } if ($form['email'] !== '' && filter_var($form['email'], FILTER_VALIDATE_EMAIL) === false) { $errors['email'][] = 'Enter a valid email address.'; } if ($form['start_date'] !== '' && !$this->isValidDate($form['start_date'])) { $errors['start_date'][] = 'Enter a valid start date.'; } return $errors; } private function isValidDate(string $value): bool { $date = \DateTimeImmutable::createFromFormat('Y-m-d', $value); return $date !== false && $date->format('Y-m-d') === $value; } private function employees(): EmployeeRepository { return new EmployeeRepository(database()); } private function isHtmxRequest(Request $request): bool { return strtolower((string) $request->server('HTTP_HX_REQUEST', '')) === 'true'; } private function buildViewModel(string $search = ''): EmployeeFormViewModel { $viewModel = new EmployeeFormViewModel(); $viewModel->search = trim($search); $employees = $this->employees()->search($viewModel->search); $newestEmployee = $this->employees()->newestMatching($viewModel->search); $viewModel->employees = array_slice($employees, 0, 5); $viewModel->newestEmployee = $newestEmployee; $viewModel->summary = [ 'employee_count' => $this->employees()->countMatching($viewModel->search), 'department_count' => $this->employees()->countDepartments($viewModel->search), 'latest_start_date' => $newestEmployee['start_date'] ?? 'N/A', ]; return $viewModel; } }