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,
]);
}
$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'];
$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,
]);
}
$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'];
$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');
}
// ── 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());
}
}