saved = $request->input('saved') === '1'; $model->deleted = $request->input('deleted') === '1'; return $this->view('customers.index', [ 'model' => $model, 'pageTitle' => $model->title, ]); } public function data(): Response { $rows = $this->repo()->allWithType(); $data = array_map(static function (array $row): array { $attrValues = !empty($row['attribute_values']) ? (json_decode((string) $row['attribute_values'], true) ?? []) : []; $customerTypeAttributes = !empty($row['customer_type_attributes']) ? (json_decode((string) $row['customer_type_attributes'], true) ?? []) : []; $summary = implode(', ', array_map( static fn($k, $v) => "{$k}: {$v}", array_keys($attrValues), array_values($attrValues) )); return [ 'id' => (int) $row['id'], 'customer_type_id' => (int) $row['customer_type_id'], 'customer_type_name' => (string) $row['customer_type_name'], 'customer_type_attributes' => $customerTypeAttributes, 'attribute_values' => $attrValues, 'attributes_summary' => $summary, 'created_at' => (string) $row['created_at'], ]; }, $rows); return $this->json($data); } public function create(): Response { $model = new CustomerViewModel(); $model->title = 'New Customer'; $model->customerTypes = $this->loadCustomerTypes(); return $this->view('customers.create', [ 'model' => $model, 'pageTitle' => $model->title, ]); } public function store(): Response { $request = Request::capture(); $model = new CustomerViewModel(); $model->title = 'New Customer'; $model->customerTypes = $this->loadCustomerTypes(); [$form, $errors] = $this->validateForm($request, $model->customerTypes); if (!empty($errors)) { $model->form = $form; $model->errors = $errors; return $this->view('customers.create', [ 'model' => $model, 'pageTitle' => $model->title, ]); } $customer = new Customer(); $customer->customerTypeId = (int) $form['customer_type_id']; $customer->attributeValues = $form['attribute_values']; $this->repo()->create($customer); $inserted = $this->repo()->findLatestByType($customer->customerTypeId); if ($inserted !== null) { $this->auditRepo()->log((int) $inserted['id'], 'I', $this->toAuditFields($inserted), $this->currentUsername()); } return $this->redirect('/customers?saved=1'); } public function edit(string $id): Response { $row = $this->repo()->findWithType((int) $id); if ($row === null) { return $this->redirect('/customers'); } $storedValues = !empty($row['attribute_values']) ? (json_decode((string) $row['attribute_values'], true) ?? []) : []; $model = new CustomerViewModel(); $model->title = 'Edit Customer'; $model->customer = $row; $model->saved = Request::capture()->input('saved') === '1'; $model->customerTypes = $this->loadCustomerTypes(); $model->form = [ 'customer_type_id' => (int) $row['customer_type_id'], 'attribute_values' => $storedValues, ]; return $this->view('customers.edit', [ 'model' => $model, 'pageTitle' => $model->title, ]); } public function update(string $id): Response { $before = $this->repo()->findWithType((int) $id); if ($before === null) { return $this->redirect('/customers'); } $request = Request::capture(); $model = new CustomerViewModel(); $model->title = 'Edit Customer'; $model->customer = $before; $model->customerTypes = $this->loadCustomerTypes(); [$form, $errors] = $this->validateForm($request, $model->customerTypes); if (!empty($errors)) { $model->form = $form; $model->errors = $errors; return $this->view('customers.edit', [ 'model' => $model, 'pageTitle' => $model->title, ]); } $customer = new Customer(); $customer->id = (int) $id; $customer->customerTypeId = (int) $form['customer_type_id']; $customer->attributeValues = $form['attribute_values']; $this->repo()->update($customer); $after = $this->repo()->findWithType((int) $id); $this->auditRepo()->log((int) $id, 'U', [ 'before' => $this->toAuditFields($before), 'after' => $this->toAuditFields($after ?? []), ], $this->currentUsername()); return $this->redirect('/customers/' . $id . '/edit?saved=1'); } public function destroy(string $id): Response { $row = $this->repo()->findWithType((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('/customers?deleted=1'); } public function lookup(): Response { $request = Request::capture(); $typeId = (int) $request->input('type_id', 0); $matchParam = trim((string) $request->input('match_field', '')); if ($typeId <= 0) { return $this->json(['fields' => [], 'records' => []]); } // match_field is an attribute NAME (not alias) $matchNames = $matchParam !== '' ? array_values(array_filter(array_map('trim', explode(';', $matchParam)))) : []; $rows = $this->repo()->searchByType($typeId); if (empty($rows)) { return $this->json(['fields' => [], 'records' => []]); } $typeAttrs = []; if (!empty($rows[0]['type_attributes'])) { $typeAttrs = json_decode((string) $rows[0]['type_attributes'], true) ?? []; } // Collect all attribute names in order $attrNames = []; foreach ($typeAttrs as $attr) { $name = trim((string) ($attr['name'] ?? '')); if ($name !== '') { $attrNames[] = $name; } } if (empty($matchNames) && !empty($attrNames)) { $matchNames = [$attrNames[0]]; } $records = []; foreach ($rows as $row) { $attrValues = !empty($row['attribute_values']) ? (json_decode((string) $row['attribute_values'], true) ?? []) : []; // _row keyed by attribute NAME — matches attribute_values storage format $rowByName = []; foreach ($attrNames as $name) { $rowByName[$name] = (string) ($attrValues[$name] ?? ''); } $display = array_map(fn($n) => $rowByName[$n] ?? '', $matchNames); $primary = $display[0] ?? ''; if ($primary === '') { continue; } $records[] = [ '_primary' => $primary, '_display' => array_values($display), '_row' => $rowByName, ]; } return $this->json(['fields' => $matchNames, 'records' => $records]); } // ── Helpers ─────────────────────────────────────────────────────────────── private function loadCustomerTypes(): array { return array_map(static function (array $type): array { return [ 'id' => (int) $type['id'], 'name' => (string) $type['name'], 'attributes' => json_decode((string) ($type['attributes'] ?? '[]'), true) ?? [], ]; }, $this->ctRepo()->allOrderedByName()); } private function attributesForType(int $typeId, array $types): array { foreach ($types as $type) { if ($type['id'] === $typeId) return $type['attributes']; } return []; } private function validateForm(Request $request, array $customerTypes): array { $customerTypeId = (int) $request->input('customer_type_id', 0); $submittedValues = (array) ($request->input('attribute_values') ?? []); $errors = []; if (!verify_csrf_token((string) $request->input('_token', ''))) { $errors['_token'][] = 'Your form session expired. Please refresh and try again.'; } if ($customerTypeId === 0) { $errors['customer_type_id'][] = 'Please select a customer type.'; } $typeAttributes = $this->attributesForType($customerTypeId, $customerTypes); $attributeValues = []; foreach ($typeAttributes as $attr) { $attributeValues[$attr['name']] = trim((string) ($submittedValues[$attr['name']] ?? '')); } return [ ['customer_type_id' => $customerTypeId, 'attribute_values' => $attributeValues], $errors, ]; } private function toAuditFields(array $row): array { $attrValues = []; if (!empty($row['attribute_values'])) { $raw = $row['attribute_values']; $attrValues = is_string($raw) ? (json_decode($raw, true) ?? []) : (array) $raw; } return [ 'customer_type_id' => (int) ($row['customer_type_id'] ?? 0), 'customer_type_name' => (string) ($row['customer_type_name'] ?? ''), 'attribute_values' => $attrValues, 'created_at' => (string) ($row['created_at'] ?? ''), 'updated_at' => (string) ($row['updated_at'] ?? ''), ]; } private function currentUsername(): string { return auth()->user()?->username ?? 'system'; } private function repo(): CustomerRepository { return new CustomerRepository(database()); } private function auditRepo(): CustomerAuditRepository { return new CustomerAuditRepository(database()); } private function ctRepo(): CustomerTypeRepository { return new CustomerTypeRepository(database()); } }