| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181 |
- <?php
- /*
- * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
- * SPDX-License-Identifier: Apache-2.0
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * See the LICENSE file in the project root for the full license text.
- */
- declare(strict_types=1);
- namespace App\Controllers;
- use App\Auth\SessionGuard;
- use App\Http\Request;
- use App\Http\Response;
- use App\Http\View;
- use App\Repositories\UserRepository;
- use App\Repositories\WorkerRepository;
- use App\Services\AuditLogger;
- use PDO;
- use PDOException;
- use Throwable;
- /**
- * /workers — master data for the people to whom sprint tasks are assigned.
- *
- * Access: admin only (all methods). Workers are independent from users: a
- * worker is not required to ever log in.
- */
- final class WorkerController
- {
- public function __construct(
- private readonly PDO $pdo,
- private readonly UserRepository $users,
- private readonly WorkerRepository $workers,
- private readonly AuditLogger $audit,
- private readonly View $view,
- ) {
- }
- /** GET /workers */
- public function index(Request $req): Response
- {
- $actor = SessionGuard::requireAdmin($this->users);
- if ($actor instanceof Response) {
- return $actor;
- }
- return Response::html($this->view->render('workers/index', [
- 'title' => 'Workers',
- 'currentUser' => $actor,
- 'csrfToken' => SessionGuard::csrfToken(),
- 'workers' => $this->workers->all(),
- 'flash' => $req->queryString('flash'),
- 'error' => $req->queryString('error'),
- ]));
- }
- /** POST /workers — create a worker. */
- public function create(Request $req): Response
- {
- $actor = SessionGuard::requireAdmin($this->users);
- if ($actor instanceof Response) {
- return $actor;
- }
- if (!SessionGuard::verifyCsrf($req)) {
- return Response::text('CSRF token invalid', 403);
- }
- $name = trim($req->postString('name'));
- $rtbRaw = $req->postString('default_rtb');
- $rtb = $rtbRaw === '' ? 0.0 : (float) $rtbRaw;
- if ($name === '') {
- return Response::redirect('/workers?error=name_required');
- }
- if ($rtb < 0.0 || $rtb > 1.0) {
- return Response::redirect('/workers?error=rtb_out_of_range');
- }
- $this->pdo->beginTransaction();
- try {
- $worker = $this->workers->create($name, true, $rtb);
- $this->audit->recordForRequest(
- action: 'CREATE',
- entityType: 'worker',
- entityId: $worker->id,
- before: null,
- after: $worker->toAuditSnapshot(),
- req: $req,
- actor: $actor,
- );
- $this->pdo->commit();
- } catch (PDOException $e) {
- $this->pdo->rollBack();
- // UNIQUE(name) violation or similar.
- return Response::redirect('/workers?error=' . urlencode(
- str_contains(strtolower($e->getMessage()), 'unique') ? 'name_taken' : 'db_error'
- ));
- } catch (Throwable) {
- $this->pdo->rollBack();
- return Response::redirect('/workers?error=db_error');
- }
- return Response::redirect('/workers?flash=created');
- }
- /** POST /workers/{id} (form) — update a worker's editable fields. */
- public function update(Request $req, array $params): Response
- {
- $actor = SessionGuard::requireAdmin($this->users);
- if ($actor instanceof Response) {
- return $actor;
- }
- if (!SessionGuard::verifyCsrf($req)) {
- return Response::text('CSRF token invalid', 403);
- }
- $id = (int) $params['id'];
- $existing = $this->workers->find($id);
- if ($existing === null) {
- return Response::text('Not Found', 404);
- }
- $changes = [];
- $name = trim($req->postString('name'));
- if ($name !== '' && $name !== $existing->name) {
- $changes['name'] = $name;
- }
- $rtbRaw = $req->postString('default_rtb');
- if ($rtbRaw !== '') {
- $rtb = (float) $rtbRaw;
- if ($rtb < 0.0 || $rtb > 1.0) {
- return Response::redirect('/workers?error=rtb_out_of_range');
- }
- if (abs($rtb - $existing->defaultRtb) > 1e-9) {
- $changes['default_rtb'] = $rtb;
- }
- }
- // is_active comes via checkbox — absent means "off".
- $isActive = isset($req->post['is_active'])
- && (string) $req->post['is_active'] !== '0';
- if ($isActive !== $existing->isActive) {
- $changes['is_active'] = $isActive;
- }
- if ($changes === []) {
- return Response::redirect('/workers?flash=noop');
- }
- $this->pdo->beginTransaction();
- try {
- $result = $this->workers->update($id, $changes);
- $this->audit->recordForRequest(
- action: 'UPDATE',
- entityType: 'worker',
- entityId: $id,
- before: $result['before']->toAuditSnapshot(),
- after: $result['after']->toAuditSnapshot(),
- req: $req,
- actor: $actor,
- );
- $this->pdo->commit();
- } catch (PDOException $e) {
- $this->pdo->rollBack();
- return Response::redirect('/workers?error=' . urlencode(
- str_contains(strtolower($e->getMessage()), 'unique') ? 'name_taken' : 'db_error'
- ));
- } catch (Throwable) {
- $this->pdo->rollBack();
- return Response::redirect('/workers?error=db_error');
- }
- return Response::redirect('/workers?flash=updated');
- }
- }
|