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 $row * @return array */ 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}, 1: array>} */ 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()); } }