|
- <?php
-
- declare(strict_types=1);
-
- namespace App\Controllers;
-
- use App\Models\CampaignType;
- use App\Repositories\CampaignTypeAuditRepository;
- use App\Repositories\CampaignTypeRepository;
- use App\ViewModels\CampaignTypeViewModel;
- use Core\Controller;
- use Core\Request;
- use Core\Response;
- use Core\Validator;
-
- class CampaignTypeController extends Controller
- {
- public function index(): Response
- {
- $request = Request::capture();
- $model = new CampaignTypeViewModel();
- $model->saved = $request->input('saved') === '1';
- $model->deleted = $request->input('deleted') === '1';
-
- return $this->view('campaign-types.index', [
- 'model' => $model,
- 'pageTitle' => $model->title,
- ]);
- }
-
- public function data(): Response
- {
- $rows = $this->repo()->allOrderedByName();
-
- $data = array_map(static function (array $row): array {
- $attrs = [];
-
- if (!empty($row['attributes'])) {
- $attrs = 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 CampaignTypeViewModel();
- $model->title = 'New Campaign Type';
-
- return $this->view('campaign-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 campaign type with that name already exists.';
- }
-
- if (!empty($errors)) {
- $model = new CampaignTypeViewModel();
- $model->title = 'New Campaign Type';
- $model->form = $form;
- $model->errors = $errors;
-
- return $this->view('campaign-types.create', [
- 'model' => $model,
- 'pageTitle' => $model->title,
- ]);
- }
-
- $campaignType = new CampaignType();
- $campaignType->name = $form['name'];
- $campaignType->attributes = $form['attributes'];
-
- $this->repo()->create($campaignType);
-
- // Audit: I — capture the newly inserted row (query by name to get the generated id).
- $inserted = $this->repo()->findByName($form['name']);
- if ($inserted !== null) {
- $this->auditRepo()->log(
- (int) $inserted['id'],
- 'I',
- $this->toAuditFields($inserted),
- $this->currentUsername()
- );
- }
-
- return $this->redirect('/campaign-types?saved=1');
- }
-
- public function edit(string $id): Response
- {
- $row = $this->repo()->find((int) $id);
-
- if ($row === null) {
- return $this->redirect('/campaign-types');
- }
-
- $model = new CampaignTypeViewModel();
- $model->title = 'Edit Campaign Type';
- $model->campaignType = $row;
- $model->saved = Request::capture()->input('saved') === '1';
- $model->form = [
- 'name' => (string) $row['name'],
- 'attributes' => json_decode((string) ($row['attributes'] ?? '[]'), true) ?? [],
- ];
-
- return $this->view('campaign-types.edit', [
- 'model' => $model,
- 'pageTitle' => $model->title,
- ]);
- }
-
- public function update(string $id): Response
- {
- $before = $this->repo()->find((int) $id);
-
- if ($before === null) {
- return $this->redirect('/campaign-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 campaign type with that name already exists.';
- }
- }
-
- if (!empty($errors)) {
- $model = new CampaignTypeViewModel();
- $model->title = 'Edit Campaign Type';
- $model->campaignType = $before;
- $model->form = $form;
- $model->errors = $errors;
-
- return $this->view('campaign-types.edit', [
- 'model' => $model,
- 'pageTitle' => $model->title,
- ]);
- }
-
- $campaignType = new CampaignType();
- $campaignType->id = (int) $id;
- $campaignType->name = $form['name'];
- $campaignType->attributes = $form['attributes'];
-
- $this->repo()->update($campaignType);
-
- // Audit: U — capture before and after snapshots.
- $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('/campaign-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);
-
- // Audit: D — snapshot of the row at the moment of deletion.
- $this->auditRepo()->log(
- (int) $row['id'],
- 'D',
- $this->toAuditFields($row),
- $this->currentUsername()
- );
- }
-
- return $this->redirect('/campaign-types?deleted=1');
- }
-
- // ── Helpers ───────────────────────────────────────────────────────────────
-
- /**
- * Build the fields payload for an audit entry.
- * The attributes column is decoded so the audit JSON nests cleanly.
- *
- * @param array<string, mixed> $row
- * @return array<string, mixed>
- */
- 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';
- }
-
- /**
- * @return array{0: array{name: string, attributes: list<array{name: string, type: string}>}, 1: array<string, list<string>>}
- */
- 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 the page and try again.';
- }
-
- $validator = (new Validator())
- ->required('name', $name, 'Campaign type name is required.')
- ->maxLength('name', $name, 255, 'Name must be 255 characters or fewer.');
-
- $errors = array_merge($errors, $validator->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,
- ];
- }
-
- // Sort by the user-supplied order, then renumber sequentially so storage is always clean.
- 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(): CampaignTypeRepository
- {
- return new CampaignTypeRepository(database());
- }
-
- private function auditRepo(): CampaignTypeAuditRepository
- {
- return new CampaignTypeAuditRepository(database());
- }
- }
|