You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

294 line
9.4KB

  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Controllers;
  4. use App\Models\CampaignType;
  5. use App\Repositories\CampaignTypeAuditRepository;
  6. use App\Repositories\CampaignTypeRepository;
  7. use App\ViewModels\CampaignTypeViewModel;
  8. use Core\Controller;
  9. use Core\Request;
  10. use Core\Response;
  11. use Core\Validator;
  12. class CampaignTypeController extends Controller
  13. {
  14. public function index(): Response
  15. {
  16. $request = Request::capture();
  17. $model = new CampaignTypeViewModel();
  18. $model->saved = $request->input('saved') === '1';
  19. $model->deleted = $request->input('deleted') === '1';
  20. return $this->view('campaign-types.index', [
  21. 'model' => $model,
  22. 'pageTitle' => $model->title,
  23. ]);
  24. }
  25. public function data(): Response
  26. {
  27. $rows = $this->repo()->allOrderedByName();
  28. $data = array_map(static function (array $row): array {
  29. $attrs = [];
  30. if (!empty($row['attributes'])) {
  31. $attrs = json_decode((string) $row['attributes'], true) ?? [];
  32. }
  33. return [
  34. 'id' => (int) $row['id'],
  35. 'name' => (string) $row['name'],
  36. 'attribute_count' => count($attrs),
  37. 'attributes_summary' => implode(', ', array_column($attrs, 'name')),
  38. 'created_at' => (string) $row['created_at'],
  39. ];
  40. }, $rows);
  41. return $this->json($data);
  42. }
  43. public function create(): Response
  44. {
  45. $model = new CampaignTypeViewModel();
  46. $model->title = 'New Campaign Type';
  47. return $this->view('campaign-types.create', [
  48. 'model' => $model,
  49. 'pageTitle' => $model->title,
  50. ]);
  51. }
  52. public function store(): Response
  53. {
  54. $request = Request::capture();
  55. [$form, $errors] = $this->validateForm($request);
  56. if (empty($errors) && $this->repo()->findByName($form['name']) !== null) {
  57. $errors['name'][] = 'A campaign type with that name already exists.';
  58. }
  59. if (!empty($errors)) {
  60. $model = new CampaignTypeViewModel();
  61. $model->title = 'New Campaign Type';
  62. $model->form = $form;
  63. $model->errors = $errors;
  64. return $this->view('campaign-types.create', [
  65. 'model' => $model,
  66. 'pageTitle' => $model->title,
  67. ]);
  68. }
  69. $campaignType = new CampaignType();
  70. $campaignType->name = $form['name'];
  71. $campaignType->attributes = $form['attributes'];
  72. $this->repo()->create($campaignType);
  73. // Audit: I — capture the newly inserted row (query by name to get the generated id).
  74. $inserted = $this->repo()->findByName($form['name']);
  75. if ($inserted !== null) {
  76. $this->auditRepo()->log(
  77. (int) $inserted['id'],
  78. 'I',
  79. $this->toAuditFields($inserted),
  80. $this->currentUsername()
  81. );
  82. }
  83. return $this->redirect('/campaign-types?saved=1');
  84. }
  85. public function edit(string $id): Response
  86. {
  87. $row = $this->repo()->find((int) $id);
  88. if ($row === null) {
  89. return $this->redirect('/campaign-types');
  90. }
  91. $model = new CampaignTypeViewModel();
  92. $model->title = 'Edit Campaign Type';
  93. $model->campaignType = $row;
  94. $model->saved = Request::capture()->input('saved') === '1';
  95. $model->form = [
  96. 'name' => (string) $row['name'],
  97. 'attributes' => json_decode((string) ($row['attributes'] ?? '[]'), true) ?? [],
  98. ];
  99. return $this->view('campaign-types.edit', [
  100. 'model' => $model,
  101. 'pageTitle' => $model->title,
  102. ]);
  103. }
  104. public function update(string $id): Response
  105. {
  106. $before = $this->repo()->find((int) $id);
  107. if ($before === null) {
  108. return $this->redirect('/campaign-types');
  109. }
  110. $request = Request::capture();
  111. [$form, $errors] = $this->validateForm($request);
  112. if (empty($errors)) {
  113. $existing = $this->repo()->findByName($form['name']);
  114. if ($existing !== null && (int) $existing['id'] !== (int) $id) {
  115. $errors['name'][] = 'A campaign type with that name already exists.';
  116. }
  117. }
  118. if (!empty($errors)) {
  119. $model = new CampaignTypeViewModel();
  120. $model->title = 'Edit Campaign Type';
  121. $model->campaignType = $before;
  122. $model->form = $form;
  123. $model->errors = $errors;
  124. return $this->view('campaign-types.edit', [
  125. 'model' => $model,
  126. 'pageTitle' => $model->title,
  127. ]);
  128. }
  129. $campaignType = new CampaignType();
  130. $campaignType->id = (int) $id;
  131. $campaignType->name = $form['name'];
  132. $campaignType->attributes = $form['attributes'];
  133. $this->repo()->update($campaignType);
  134. // Audit: U — capture before and after snapshots.
  135. $after = $this->repo()->find((int) $id);
  136. $this->auditRepo()->log(
  137. (int) $id,
  138. 'U',
  139. [
  140. 'before' => $this->toAuditFields($before),
  141. 'after' => $this->toAuditFields($after ?? []),
  142. ],
  143. $this->currentUsername()
  144. );
  145. return $this->redirect('/campaign-types/' . $id . '/edit?saved=1');
  146. }
  147. public function destroy(string $id): Response
  148. {
  149. $row = $this->repo()->find((int) $id);
  150. if ($row !== null) {
  151. $this->repo()->delete((int) $id);
  152. // Audit: D — snapshot of the row at the moment of deletion.
  153. $this->auditRepo()->log(
  154. (int) $row['id'],
  155. 'D',
  156. $this->toAuditFields($row),
  157. $this->currentUsername()
  158. );
  159. }
  160. return $this->redirect('/campaign-types?deleted=1');
  161. }
  162. // ── Helpers ───────────────────────────────────────────────────────────────
  163. /**
  164. * Build the fields payload for an audit entry.
  165. * The attributes column is decoded so the audit JSON nests cleanly.
  166. *
  167. * @param array<string, mixed> $row
  168. * @return array<string, mixed>
  169. */
  170. private function toAuditFields(array $row): array
  171. {
  172. $attrs = [];
  173. if (!empty($row['attributes'])) {
  174. $raw = $row['attributes'];
  175. $attrs = is_string($raw) ? (json_decode($raw, true) ?? []) : (array) $raw;
  176. }
  177. return [
  178. 'name' => (string) ($row['name'] ?? ''),
  179. 'attributes' => $attrs,
  180. 'created_at' => (string) ($row['created_at'] ?? ''),
  181. 'updated_at' => (string) ($row['updated_at'] ?? ''),
  182. ];
  183. }
  184. private function currentUsername(): string
  185. {
  186. return auth()->user()?->username ?? 'system';
  187. }
  188. /**
  189. * @return array{0: array{name: string, attributes: list<array{name: string, type: string}>}, 1: array<string, list<string>>}
  190. */
  191. private function validateForm(Request $request): array
  192. {
  193. $name = trim((string) $request->input('name', ''));
  194. $attributeNames = (array) ($request->input('attribute_name') ?? []);
  195. $attributeTypes = (array) ($request->input('attribute_type') ?? []);
  196. $attributeOrders = (array) ($request->input('attribute_order') ?? []);
  197. $errors = [];
  198. if (!verify_csrf_token((string) $request->input('_token', ''))) {
  199. $errors['_token'][] = 'Your form session expired. Please refresh the page and try again.';
  200. }
  201. $validator = (new Validator())
  202. ->required('name', $name, 'Campaign type name is required.')
  203. ->maxLength('name', $name, 255, 'Name must be 255 characters or fewer.');
  204. $errors = array_merge($errors, $validator->errors());
  205. $attributes = [];
  206. foreach ($attributeNames as $i => $attrName) {
  207. $attrName = trim((string) $attrName);
  208. $attrType = trim((string) ($attributeTypes[$i] ?? 'text'));
  209. if ($attrName === '') {
  210. continue;
  211. }
  212. $attributes[] = [
  213. 'name' => $attrName,
  214. 'type' => in_array($attrType, ['text', 'number', 'date', 'boolean'], true) ? $attrType : 'text',
  215. 'order' => isset($attributeOrders[$i]) && (string) $attributeOrders[$i] !== ''
  216. ? max(1, (int) $attributeOrders[$i])
  217. : count($attributes) + 1,
  218. ];
  219. }
  220. // Sort by the user-supplied order, then renumber sequentially so storage is always clean.
  221. usort($attributes, static fn(array $a, array $b): int => $a['order'] <=> $b['order']);
  222. foreach ($attributes as $seq => &$attr) {
  223. $attr['order'] = $seq + 1;
  224. }
  225. unset($attr);
  226. return [['name' => $name, 'attributes' => $attributes], $errors];
  227. }
  228. private function repo(): CampaignTypeRepository
  229. {
  230. return new CampaignTypeRepository(database());
  231. }
  232. private function auditRepo(): CampaignTypeAuditRepository
  233. {
  234. return new CampaignTypeAuditRepository(database());
  235. }
  236. }

Powered by TurnKey Linux.