| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- <?php
- declare(strict_types=1);
- namespace App\Repositories;
- use App\Domain\TaskAssignment;
- use InvalidArgumentException;
- use PDO;
- final class TaskAssignmentRepository
- {
- public function __construct(private readonly PDO $pdo)
- {
- }
- /** @return list<TaskAssignment> rows for a single task */
- public function allForTask(int $taskId): array
- {
- $stmt = $this->pdo->prepare(
- 'SELECT * FROM task_assignments WHERE task_id = ?'
- );
- $stmt->execute([$taskId]);
- $out = [];
- foreach ($stmt as $row) {
- $out[] = self::hydrate($row);
- }
- return $out;
- }
- /**
- * All assignments for a given sprint_worker (used by removeWorker to
- * snapshot rows before the FK cascade wipes them).
- *
- * @return list<TaskAssignment>
- */
- public function allForSprintWorker(int $swId): array
- {
- $stmt = $this->pdo->prepare(
- 'SELECT * FROM task_assignments WHERE sprint_worker_id = ?'
- );
- $stmt->execute([$swId]);
- $out = [];
- foreach ($stmt as $row) {
- $out[] = self::hydrate($row);
- }
- return $out;
- }
- public function find(int $taskId, int $swId): ?TaskAssignment
- {
- $stmt = $this->pdo->prepare(
- 'SELECT * FROM task_assignments WHERE task_id = ? AND sprint_worker_id = ?'
- );
- $stmt->execute([$taskId, $swId]);
- $row = $stmt->fetch();
- return is_array($row) ? self::hydrate($row) : null;
- }
- /**
- * Full grid for a sprint: [task_id][sw_id] => days. Tasks with no rows
- * are simply absent from the outer map.
- *
- * @return array<int, array<int, float>>
- */
- public function gridForSprint(int $sprintId): array
- {
- $stmt = $this->pdo->prepare(
- 'SELECT ta.task_id, ta.sprint_worker_id, ta.days
- FROM task_assignments ta
- JOIN tasks t ON t.id = ta.task_id
- WHERE t.sprint_id = ?'
- );
- $stmt->execute([$sprintId]);
- $out = [];
- foreach ($stmt as $row) {
- $tid = (int) $row['task_id'];
- $sw = (int) $row['sprint_worker_id'];
- $d = (float) $row['days'];
- $out[$tid][$sw] = $d;
- }
- return $out;
- }
- /**
- * Status grid for a sprint: [task_id][sw_id] => status. Mirrors
- * gridForSprint(); cells with no row are absent (default to
- * STATUS_ZUGEWIESEN at the call site).
- *
- * @return array<int, array<int, string>>
- */
- public function statusGridForSprint(int $sprintId): array
- {
- $stmt = $this->pdo->prepare(
- 'SELECT ta.task_id, ta.sprint_worker_id, ta.status
- FROM task_assignments ta
- JOIN tasks t ON t.id = ta.task_id
- WHERE t.sprint_id = ?'
- );
- $stmt->execute([$sprintId]);
- $out = [];
- foreach ($stmt as $row) {
- $tid = (int) $row['task_id'];
- $sw = (int) $row['sprint_worker_id'];
- $out[$tid][$sw] = (string) $row['status'];
- }
- return $out;
- }
- /**
- * Σ task_assignments.days where task.priority = 1, grouped by sprint_worker_id.
- * Used by CapacityCalculator via SprintController::show.
- *
- * @return array<int, float>
- */
- public function committedPrio1BySprint(int $sprintId): array
- {
- $stmt = $this->pdo->prepare(
- 'SELECT ta.sprint_worker_id, SUM(ta.days) AS committed
- FROM task_assignments ta
- JOIN tasks t ON t.id = ta.task_id
- WHERE t.sprint_id = ? AND t.priority = 1
- GROUP BY ta.sprint_worker_id'
- );
- $stmt->execute([$sprintId]);
- $out = [];
- foreach ($stmt as $row) {
- $out[(int) $row['sprint_worker_id']] = (float) $row['committed'];
- }
- return $out;
- }
- /**
- * Set days for one (task, sprint_worker) cell with the same four-case
- * semantics as SprintWorkerDayRepository::upsert:
- * - empty cell, days=0 -> NOOP
- * - empty cell, days>0 -> CREATE (status seeded with default)
- * - existing, unchanged -> NOOP
- * - existing, changed -> UPDATE (row kept when zeroed; status preserved)
- *
- * @return array{action:string, before: ?TaskAssignment, after: ?TaskAssignment}
- */
- public function upsert(int $taskId, int $swId, float $days): array
- {
- $existing = $this->find($taskId, $swId);
- if ($existing !== null && abs($existing->days - $days) < 1e-9) {
- return ['action' => 'NOOP', 'before' => $existing, 'after' => $existing];
- }
- if ($existing === null) {
- if (abs($days) < 1e-9) {
- return ['action' => 'NOOP', 'before' => null, 'after' => null];
- }
- $stmt = $this->pdo->prepare(
- 'INSERT INTO task_assignments (task_id, sprint_worker_id, days) VALUES (?, ?, ?)'
- );
- $stmt->execute([$taskId, $swId, $days]);
- $id = (int) $this->pdo->lastInsertId();
- $after = new TaskAssignment($id, $taskId, $swId, $days, TaskAssignment::STATUS_ZUGEWIESEN);
- return ['action' => 'CREATE', 'before' => null, 'after' => $after];
- }
- $stmt = $this->pdo->prepare('UPDATE task_assignments SET days = ? WHERE id = ?');
- $stmt->execute([$days, $existing->id]);
- $after = new TaskAssignment($existing->id, $taskId, $swId, $days, $existing->status);
- return ['action' => 'UPDATE', 'before' => $existing, 'after' => $after];
- }
- /**
- * Set status for one (task, sprint_worker) cell. Creates a row with
- * days=0 when none exists so a state can be tracked even before any
- * days are assigned.
- *
- * @return array{action:string, before: ?TaskAssignment, after: ?TaskAssignment}
- */
- public function upsertStatus(int $taskId, int $swId, string $status): array
- {
- if (!TaskAssignment::isValidStatus($status)) {
- throw new InvalidArgumentException("invalid status: {$status}");
- }
- $existing = $this->find($taskId, $swId);
- if ($existing !== null && $existing->status === $status) {
- return ['action' => 'NOOP', 'before' => $existing, 'after' => $existing];
- }
- if ($existing === null) {
- // Default status on a fresh row would be 'zugewiesen', so writing
- // that is a no-op — saves an audit row for cells that never had
- // a non-default state.
- if ($status === TaskAssignment::STATUS_ZUGEWIESEN) {
- return ['action' => 'NOOP', 'before' => null, 'after' => null];
- }
- $stmt = $this->pdo->prepare(
- 'INSERT INTO task_assignments (task_id, sprint_worker_id, days, status)
- VALUES (?, ?, 0, ?)'
- );
- $stmt->execute([$taskId, $swId, $status]);
- $id = (int) $this->pdo->lastInsertId();
- $after = new TaskAssignment($id, $taskId, $swId, 0.0, $status);
- return ['action' => 'CREATE', 'before' => null, 'after' => $after];
- }
- $stmt = $this->pdo->prepare('UPDATE task_assignments SET status = ? WHERE id = ?');
- $stmt->execute([$status, $existing->id]);
- $after = new TaskAssignment(
- $existing->id, $taskId, $swId, $existing->days, $status,
- );
- return ['action' => 'UPDATE', 'before' => $existing, 'after' => $after];
- }
- /**
- * @param array<string,mixed> $row
- */
- private static function hydrate(array $row): TaskAssignment
- {
- return new TaskAssignment(
- id: (int) $row['id'],
- taskId: (int) $row['task_id'],
- sprintWorkerId: (int) $row['sprint_worker_id'],
- days: (float) $row['days'],
- status: (string) ($row['status'] ?? TaskAssignment::STATUS_ZUGEWIESEN),
- );
- }
- }
|