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'; $model->customerTypes = $this->loadCustomerTypes(); 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; $model->customerTypes = $this->loadCustomerTypes(); 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->customerTypes = $this->loadCustomerTypes(); $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; $model->customerTypes = $this->loadCustomerTypes(); 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()); $attributeAliases = (array) ($request->input('attribute_alias') ?? []); $attributeApiUrls = (array) ($request->input('attribute_api_url') ?? []); $attributeApiFormats = (array) ($request->input('attribute_api_format') ?? []); $attributeApiReturnTypes = (array) ($request->input('attribute_api_return_type') ?? []); $attributeApiMatchFields = (array) ($request->input('attribute_api_match_field') ?? []); $attributeApiAutoFills = (array) ($request->input('attribute_api_auto_fill') ?? []); $attributeCustomerTypeIds = (array) ($request->input('attribute_customer_type_id') ?? []); $attributes = []; foreach ($attributeNames as $i => $attrName) { $attrName = trim((string) $attrName); $attrType = trim((string) ($attributeTypes[$i] ?? 'text')); if ($attrName === '') continue; // 'customer' is a legacy design-time placeholder (now replaced by customer_lookup). Drop if it slips through. if ($attrType === 'customer') continue; $validatedType = in_array($attrType, ['text', 'number', 'date', 'boolean', 'api_lookup', 'customer_lookup'], true) ? $attrType : 'text'; $attr = [ 'name' => $attrName, 'type' => $validatedType, 'alias' => trim((string) ($attributeAliases[$i] ?? '')), 'order' => isset($attributeOrders[$i]) && (string) $attributeOrders[$i] !== '' ? max(1, (int) $attributeOrders[$i]) : count($attributes) + 1, ]; if ($validatedType === 'api_lookup') { $rawFormat = trim((string) ($attributeApiFormats[$i] ?? '')); $rawReturnType = trim((string) ($attributeApiReturnTypes[$i] ?? '')); $attr['api_url'] = trim((string) ($attributeApiUrls[$i] ?? '')); $attr['api_format'] = in_array($rawFormat, ['json', 'xml'], true) ? $rawFormat : 'json'; $attr['api_return_type'] = in_array($rawReturnType, ['text', 'number', 'date', 'boolean'], true) ? $rawReturnType : 'text'; $attr['api_match_field'] = trim((string) ($attributeApiMatchFields[$i] ?? '')); $attr['api_auto_fill'] = trim((string) ($attributeApiAutoFills[$i] ?? '')); } if ($validatedType === 'customer_lookup') { $attr['customer_type_id'] = (int) ($attributeCustomerTypeIds[$i] ?? 0); $attr['api_match_field'] = trim((string) ($attributeApiMatchFields[$i] ?? '')); } $attributes[] = $attr; } 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()); } private function customerTypeRepo(): CustomerTypeRepository { return new CustomerTypeRepository(database()); } /** @return list */ private function loadCustomerTypes(): array { return array_map(static function (array $t): array { return [ 'id' => (int) $t['id'], 'name' => (string) $t['name'], 'api_match_field' => (string) ($t['api_match_field'] ?? ''), 'attributes' => json_decode((string) ($t['attributes'] ?? '[]'), true) ?? [], ]; }, $this->customerTypeRepo()->allOrderedByName()); } }