diff --git a/.gitignore b/.gitignore index 8c70e19..4ab77ac 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,11 @@ node_modules/ npm-debug.log* yarn-error.log* -.claude/ -.abacusai/ \ No newline at end of file +.claude/* +.abacusai/* +graphify-out/* +.graphify_ast_extract.py +.graphify_ast.json +.graphify_detect.json +.graphify_python +.graphify_uncached.txt \ No newline at end of file diff --git a/app/Controllers/CustomerController.php b/app/Controllers/CustomerController.php index 37e82a4..b52d321 100644 --- a/app/Controllers/CustomerController.php +++ b/app/Controllers/CustomerController.php @@ -91,6 +91,21 @@ class CustomerController extends Controller ]); } + $encodedValues = json_encode($form['attribute_values'], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); + $duplicate = $this->repo()->findDuplicate((int) $form['customer_type_id'], $encodedValues); + + if ($duplicate !== null) { + $model->form = $form; + $model->errors['_duplicate'] = [ + 'A customer with these exact values already exists: Customer #' . (int) $duplicate['id'] . ' (' . htmlspecialchars((string) $duplicate['customer_type_name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ').', + ]; + + 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']; @@ -159,6 +174,21 @@ class CustomerController extends Controller ]); } + $encodedValues = json_encode($form['attribute_values'], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); + $duplicate = $this->repo()->findDuplicate((int) $form['customer_type_id'], $encodedValues, (int) $id); + + if ($duplicate !== null) { + $model->form = $form; + $model->errors['_duplicate'] = [ + 'These values are identical to an existing customer: Customer #' . (int) $duplicate['id'] . ' (' . htmlspecialchars((string) $duplicate['customer_type_name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ').', + ]; + + return $this->view('customers.edit', [ + 'model' => $model, + 'pageTitle' => $model->title, + ]); + } + $customer = new Customer(); $customer->id = (int) $id; $customer->customerTypeId = (int) $form['customer_type_id']; diff --git a/app/Repositories/CustomerRepository.php b/app/Repositories/CustomerRepository.php index bf263c4..6a85055 100644 --- a/app/Repositories/CustomerRepository.php +++ b/app/Repositories/CustomerRepository.php @@ -88,6 +88,24 @@ class CustomerRepository extends Repository ); } + /** Returns an existing customer with the same type and attribute values, excluding $excludeId (use for update). */ + public function findDuplicate(int $typeId, string $attributeValuesJson, int $excludeId = 0): ?array + { + $sql = 'SELECT TOP (1) c.id, ct.name AS customer_type_name + FROM customer c + INNER JOIN customer_type ct ON c.customer_type_id = ct.id + WHERE c.customer_type_id = :type_id + AND c.attribute_values = :attribute_values'; + $params = ['type_id' => $typeId, 'attribute_values' => $attributeValuesJson]; + + if ($excludeId > 0) { + $sql .= ' AND c.id != :exclude_id'; + $params['exclude_id'] = $excludeId; + } + + return $this->database->first($sql, $params); + } + /** Used after INSERT to recover the generated id for audit logging. */ public function findLatestByType(int $typeId): ?array { diff --git a/app/Views/customers/create.php b/app/Views/customers/create.php index c8d38c0..f2ad9d8 100644 --- a/app/Views/customers/create.php +++ b/app/Views/customers/create.php @@ -26,6 +26,10 @@ window.__initialCtVals = form['attribute_values'], JSON_
errors['_token'][0]) ?>
+ errors['_duplicate'])): ?> +
errors['_duplicate'][0] ?>
+ +
diff --git a/app/Views/customers/edit.php b/app/Views/customers/edit.php index 0e73b26..a50c9d3 100644 --- a/app/Views/customers/edit.php +++ b/app/Views/customers/edit.php @@ -27,6 +27,10 @@ window.__initialCtVals = form['attribute_values'], JSON_
errors['_token'][0]) ?>
+ errors['_duplicate'])): ?> +
errors['_duplicate'][0] ?>
+ +