saved = $request->input('saved') === '1'; $model->deleted = $request->input('deleted') === '1'; return $this->view('campaigns.index', [ 'model' => $model, 'pageTitle' => $model->title, ]); } public function data(): Response { $rows = $this->repo()->allWithType(); $data = array_map(static function (array $row): array { $attrValues = []; $campaignTypeAttributes = []; if (!empty($row['attribute_values'])) { $attrValues = json_decode((string) $row['attribute_values'], true) ?? []; } if (!empty($row['campaign_type_attributes'])) { $campaignTypeAttributes = json_decode((string) $row['campaign_type_attributes'], true) ?? []; } $summary = implode(', ', array_map( static fn($k, $v) => "{$k}: {$v}", array_keys($attrValues), array_values($attrValues) )); return [ 'id' => (int) $row['id'], 'campaign_type_id' => (int) $row['campaign_type_id'], 'campaign_type_name' => (string) $row['campaign_type_name'], 'campaign_type_attributes' => $campaignTypeAttributes, 'attribute_values' => $attrValues, 'attributes_summary' => $summary, 'created_at' => (string) $row['created_at'], ]; }, $rows); return $this->json($data); } public function create(): Response { $model = new CampaignViewModel(); $model->title = 'New Campaign'; $model->campaignTypes = $this->loadCampaignTypes(); return $this->view('campaigns.create', [ 'model' => $model, 'pageTitle' => $model->title, ]); } public function store(): Response { $request = Request::capture(); $model = new CampaignViewModel(); $model->title = 'New Campaign'; $model->campaignTypes = $this->loadCampaignTypes(); [$form, $errors] = $this->validateForm($request, $model->campaignTypes); if (!empty($errors)) { $model->form = $form; $model->errors = $errors; return $this->view('campaigns.create', [ 'model' => $model, 'pageTitle' => $model->title, ]); } $campaign = new Campaign(); $campaign->campaignTypeId = (int) $form['campaign_type_id']; $campaign->attributeValues = $form['attribute_values']; $this->repo()->create($campaign); // Audit: I — query back the inserted row to capture the generated id. $inserted = $this->repo()->findLatestByType($campaign->campaignTypeId); if ($inserted !== null) { $this->auditRepo()->log( (int) $inserted['id'], 'I', $this->toAuditFields($inserted), $this->currentUsername() ); } return $this->redirect('/campaigns?saved=1'); } public function edit(string $id): Response { $row = $this->repo()->findWithType((int) $id); if ($row === null) { return $this->redirect('/campaigns'); } $storedValues = []; if (!empty($row['attribute_values'])) { $storedValues = json_decode((string) $row['attribute_values'], true) ?? []; } $model = new CampaignViewModel(); $model->title = 'Edit Campaign'; $model->campaign = $row; $model->saved = Request::capture()->input('saved') === '1'; $model->campaignTypes = $this->loadCampaignTypes(); $model->form = [ 'campaign_type_id' => (int) $row['campaign_type_id'], 'attribute_values' => $storedValues, ]; return $this->view('campaigns.edit', [ 'model' => $model, 'pageTitle' => $model->title, ]); } public function update(string $id): Response { $before = $this->repo()->findWithType((int) $id); if ($before === null) { return $this->redirect('/campaigns'); } $request = Request::capture(); $model = new CampaignViewModel(); $model->title = 'Edit Campaign'; $model->campaign = $before; $model->campaignTypes = $this->loadCampaignTypes(); [$form, $errors] = $this->validateForm($request, $model->campaignTypes); if (!empty($errors)) { $model->form = $form; $model->errors = $errors; return $this->view('campaigns.edit', [ 'model' => $model, 'pageTitle' => $model->title, ]); } $campaign = new Campaign(); $campaign->id = (int) $id; $campaign->campaignTypeId = (int) $form['campaign_type_id']; $campaign->attributeValues = $form['attribute_values']; $this->repo()->update($campaign); // Audit: U — capture before and after snapshots. $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('/campaigns/' . $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('/campaigns?deleted=1'); } // ── Helpers ─────────────────────────────────────────────────────────────── /** * @return list}> */ private function loadCampaignTypes(): 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()); } /** * @param list }> $types * @return list */ private function attributesForType(int $typeId, array $types): array { foreach ($types as $type) { if ($type['id'] === $typeId) { return $type['attributes']; } } return []; } /** * @param list }> $campaignTypes * @return array{0: array{campaign_type_id: int|string, attribute_values: array}, 1: array>} */ private function validateForm(Request $request, array $campaignTypes): array { $campaignTypeId = (int) $request->input('campaign_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 the page and try again.'; } if ($campaignTypeId === 0) { $errors['campaign_type_id'][] = 'Please select a campaign type.'; } // Build attribute values — only keep keys that belong to the selected type. $typeAttributes = $this->attributesForType($campaignTypeId, $campaignTypes); $attributeValues = []; foreach ($typeAttributes as $attr) { $attributeValues[$attr['name']] = trim((string) ($submittedValues[$attr['name']] ?? '')); } $form = [ 'campaign_type_id' => $campaignTypeId, 'attribute_values' => $attributeValues, ]; return [$form, $errors]; } /** * @param array $row * @return array */ 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 [ 'campaign_type_id' => (int) ($row['campaign_type_id'] ?? 0), 'campaign_type_name' => (string) ($row['campaign_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(): CampaignRepository { return new CampaignRepository(database()); } private function auditRepo(): CampaignAuditRepository { return new CampaignAuditRepository(database()); } private function ctRepo(): CampaignTypeRepository { return new CampaignTypeRepository(database()); } }