saved = $request->input('saved') === '1'; $model->deleted = $request->input('deleted') === '1'; return $this->view('job-types.index', [ 'model' => $model, 'pageTitle' => $model->title, ]); } public function data(): Response { $rows = $this->repo()->allOrderedByName(); $data = array_map(static function (array $row): array { $attrs = !empty($row['attributes']) ? (json_decode((string) $row['attributes'], true) ?? []) : []; return [ 'id' => (int) $row['id'], 'name' => (string) $row['name'], 'attribute_count' => count($attrs), 'attributes_summary' => implode(', ', array_column($attrs, 'name')), 'created_at' => (string) $row['created_at'], ]; }, $rows); return $this->json($data); } public function create(): Response { $model = new JobTypeViewModel(); $model->title = 'New Job Type'; return $this->view('job-types.create', [ 'model' => $model, 'pageTitle' => $model->title, ]); } public function store(): Response { $request = Request::capture(); [$form, $errors] = $this->validateForm($request); if (empty($errors) && $this->repo()->findByName($form['name']) !== null) { $errors['name'][] = 'A job type with that name already exists.'; } if (!empty($errors)) { $model = new JobTypeViewModel(); $model->title = 'New Job Type'; $model->form = $form; $model->errors = $errors; return $this->view('job-types.create', [ 'model' => $model, 'pageTitle' => $model->title, ]); } $jobType = new JobType(); $jobType->name = $form['name']; $jobType->attributes = $form['attributes']; $this->repo()->create($jobType); $inserted = $this->repo()->findByName($form['name']); if ($inserted !== null) { $this->auditRepo()->log((int) $inserted['id'], 'I', $this->toAuditFields($inserted), $this->currentUsername()); } return $this->redirect('/job-types?saved=1'); } public function edit(string $id): Response { $row = $this->repo()->find((int) $id); if ($row === null) { return $this->redirect('/job-types'); } $model = new JobTypeViewModel(); $model->title = 'Edit Job Type'; $model->jobType = $row; $model->saved = Request::capture()->input('saved') === '1'; $model->form = [ 'name' => (string) $row['name'], 'attributes' => json_decode((string) ($row['attributes'] ?? '[]'), true) ?? [], ]; return $this->view('job-types.edit', [ 'model' => $model, 'pageTitle' => $model->title, ]); } public function update(string $id): Response { $row = $this->repo()->find((int) $id); if ($row === null) { return $this->redirect('/job-types'); } $request = Request::capture(); [$form, $errors] = $this->validateForm($request); if (empty($errors)) { $existing = $this->repo()->findByName($form['name']); if ($existing !== null && (int) $existing['id'] !== (int) $id) { $errors['name'][] = 'A job type with that name already exists.'; } } if (!empty($errors)) { $model = new JobTypeViewModel(); $model->title = 'Edit Job Type'; $model->jobType = $row; $model->form = $form; $model->errors = $errors; return $this->view('job-types.edit', [ 'model' => $model, 'pageTitle' => $model->title, ]); } $before = $row; $jobType = new JobType(); $jobType->id = (int) $id; $jobType->name = $form['name']; $jobType->attributes = $form['attributes']; $this->repo()->update($jobType); $after = $this->repo()->find((int) $id); $this->auditRepo()->log((int) $id, 'U', [ 'before' => $this->toAuditFields($before), 'after' => $this->toAuditFields($after ?? []), ], $this->currentUsername()); return $this->redirect('/job-types/' . $id . '/edit?saved=1'); } public function destroy(string $id): Response { $row = $this->repo()->find((int) $id); if ($row !== null) { $this->repo()->delete((int) $id); $this->auditRepo()->log((int) $row['id'], 'D', $this->toAuditFields($row), $this->currentUsername()); } return $this->redirect('/job-types?deleted=1'); } // ── Helpers ─────────────────────────────────────────────────────────────── private function toAuditFields(array $row): array { $attrs = []; if (!empty($row['attributes'])) { $raw = $row['attributes']; $attrs = is_string($raw) ? (json_decode($raw, true) ?? []) : (array) $raw; } return [ 'name' => (string) ($row['name'] ?? ''), 'attributes' => $attrs, 'created_at' => (string) ($row['created_at'] ?? ''), 'updated_at' => (string) ($row['updated_at'] ?? ''), ]; } private function currentUsername(): string { return auth()->user()?->username ?? 'system'; } private function validateForm(Request $request): array { $name = trim((string) $request->input('name', '')); $attributeNames = (array) ($request->input('attribute_name') ?? []); $attributeTypes = (array) ($request->input('attribute_type') ?? []); $attributeOrders = (array) ($request->input('attribute_order') ?? []); $errors = []; if (!verify_csrf_token((string) $request->input('_token', ''))) { $errors['_token'][] = 'Your form session expired. Please refresh and try again.'; } $errors = array_merge($errors, (new Validator()) ->required('name', $name, 'Job type name is required.') ->maxLength('name', $name, 255, 'Name must be 255 characters or fewer.') ->errors()); $attributes = []; foreach ($attributeNames as $i => $attrName) { $attrName = trim((string) $attrName); $attrType = trim((string) ($attributeTypes[$i] ?? 'text')); if ($attrName === '') continue; $attributes[] = [ 'name' => $attrName, 'type' => in_array($attrType, ['text', 'number', 'date', 'boolean'], true) ? $attrType : 'text', 'order' => isset($attributeOrders[$i]) && (string) $attributeOrders[$i] !== '' ? max(1, (int) $attributeOrders[$i]) : count($attributes) + 1, ]; } usort($attributes, static fn(array $a, array $b): int => $a['order'] <=> $b['order']); foreach ($attributes as $seq => &$attr) { $attr['order'] = $seq + 1; } unset($attr); return [['name' => $name, 'attributes' => $attributes], $errors]; } private function repo(): JobTypeRepository { return new JobTypeRepository(database()); } private function auditRepo(): JobTypeAuditRepository { return new JobTypeAuditRepository(database()); } }