TaskAssignmentRepository.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Repositories;
  4. use App\Domain\TaskAssignment;
  5. use InvalidArgumentException;
  6. use PDO;
  7. final class TaskAssignmentRepository
  8. {
  9. public function __construct(private readonly PDO $pdo)
  10. {
  11. }
  12. /** @return list<TaskAssignment> rows for a single task */
  13. public function allForTask(int $taskId): array
  14. {
  15. $stmt = $this->pdo->prepare(
  16. 'SELECT * FROM task_assignments WHERE task_id = ?'
  17. );
  18. $stmt->execute([$taskId]);
  19. $out = [];
  20. foreach ($stmt as $row) {
  21. $out[] = self::hydrate($row);
  22. }
  23. return $out;
  24. }
  25. /**
  26. * All assignments for a given sprint_worker (used by removeWorker to
  27. * snapshot rows before the FK cascade wipes them).
  28. *
  29. * @return list<TaskAssignment>
  30. */
  31. public function allForSprintWorker(int $swId): array
  32. {
  33. $stmt = $this->pdo->prepare(
  34. 'SELECT * FROM task_assignments WHERE sprint_worker_id = ?'
  35. );
  36. $stmt->execute([$swId]);
  37. $out = [];
  38. foreach ($stmt as $row) {
  39. $out[] = self::hydrate($row);
  40. }
  41. return $out;
  42. }
  43. public function find(int $taskId, int $swId): ?TaskAssignment
  44. {
  45. $stmt = $this->pdo->prepare(
  46. 'SELECT * FROM task_assignments WHERE task_id = ? AND sprint_worker_id = ?'
  47. );
  48. $stmt->execute([$taskId, $swId]);
  49. $row = $stmt->fetch();
  50. return is_array($row) ? self::hydrate($row) : null;
  51. }
  52. /**
  53. * Full grid for a sprint: [task_id][sw_id] => days. Tasks with no rows
  54. * are simply absent from the outer map.
  55. *
  56. * @return array<int, array<int, float>>
  57. */
  58. public function gridForSprint(int $sprintId): array
  59. {
  60. $stmt = $this->pdo->prepare(
  61. 'SELECT ta.task_id, ta.sprint_worker_id, ta.days
  62. FROM task_assignments ta
  63. JOIN tasks t ON t.id = ta.task_id
  64. WHERE t.sprint_id = ?'
  65. );
  66. $stmt->execute([$sprintId]);
  67. $out = [];
  68. foreach ($stmt as $row) {
  69. $tid = (int) $row['task_id'];
  70. $sw = (int) $row['sprint_worker_id'];
  71. $d = (float) $row['days'];
  72. $out[$tid][$sw] = $d;
  73. }
  74. return $out;
  75. }
  76. /**
  77. * Status grid for a sprint: [task_id][sw_id] => status. Mirrors
  78. * gridForSprint(); cells with no row are absent (default to
  79. * STATUS_ZUGEWIESEN at the call site).
  80. *
  81. * @return array<int, array<int, string>>
  82. */
  83. public function statusGridForSprint(int $sprintId): array
  84. {
  85. $stmt = $this->pdo->prepare(
  86. 'SELECT ta.task_id, ta.sprint_worker_id, ta.status
  87. FROM task_assignments ta
  88. JOIN tasks t ON t.id = ta.task_id
  89. WHERE t.sprint_id = ?'
  90. );
  91. $stmt->execute([$sprintId]);
  92. $out = [];
  93. foreach ($stmt as $row) {
  94. $tid = (int) $row['task_id'];
  95. $sw = (int) $row['sprint_worker_id'];
  96. $out[$tid][$sw] = (string) $row['status'];
  97. }
  98. return $out;
  99. }
  100. /**
  101. * Σ task_assignments.days where task.priority = 1, grouped by sprint_worker_id.
  102. * Used by CapacityCalculator via SprintController::show.
  103. *
  104. * @return array<int, float>
  105. */
  106. public function committedPrio1BySprint(int $sprintId): array
  107. {
  108. $stmt = $this->pdo->prepare(
  109. 'SELECT ta.sprint_worker_id, SUM(ta.days) AS committed
  110. FROM task_assignments ta
  111. JOIN tasks t ON t.id = ta.task_id
  112. WHERE t.sprint_id = ? AND t.priority = 1
  113. GROUP BY ta.sprint_worker_id'
  114. );
  115. $stmt->execute([$sprintId]);
  116. $out = [];
  117. foreach ($stmt as $row) {
  118. $out[(int) $row['sprint_worker_id']] = (float) $row['committed'];
  119. }
  120. return $out;
  121. }
  122. /**
  123. * Set days for one (task, sprint_worker) cell with the same four-case
  124. * semantics as SprintWorkerDayRepository::upsert:
  125. * - empty cell, days=0 -> NOOP
  126. * - empty cell, days>0 -> CREATE (status seeded with default)
  127. * - existing, unchanged -> NOOP
  128. * - existing, changed -> UPDATE (row kept when zeroed; status preserved)
  129. *
  130. * @return array{action:string, before: ?TaskAssignment, after: ?TaskAssignment}
  131. */
  132. public function upsert(int $taskId, int $swId, float $days): array
  133. {
  134. $existing = $this->find($taskId, $swId);
  135. if ($existing !== null && abs($existing->days - $days) < 1e-9) {
  136. return ['action' => 'NOOP', 'before' => $existing, 'after' => $existing];
  137. }
  138. if ($existing === null) {
  139. if (abs($days) < 1e-9) {
  140. return ['action' => 'NOOP', 'before' => null, 'after' => null];
  141. }
  142. $stmt = $this->pdo->prepare(
  143. 'INSERT INTO task_assignments (task_id, sprint_worker_id, days) VALUES (?, ?, ?)'
  144. );
  145. $stmt->execute([$taskId, $swId, $days]);
  146. $id = (int) $this->pdo->lastInsertId();
  147. $after = new TaskAssignment($id, $taskId, $swId, $days, TaskAssignment::STATUS_ZUGEWIESEN);
  148. return ['action' => 'CREATE', 'before' => null, 'after' => $after];
  149. }
  150. $stmt = $this->pdo->prepare('UPDATE task_assignments SET days = ? WHERE id = ?');
  151. $stmt->execute([$days, $existing->id]);
  152. $after = new TaskAssignment($existing->id, $taskId, $swId, $days, $existing->status);
  153. return ['action' => 'UPDATE', 'before' => $existing, 'after' => $after];
  154. }
  155. /**
  156. * Set status for one (task, sprint_worker) cell. Creates a row with
  157. * days=0 when none exists so a state can be tracked even before any
  158. * days are assigned.
  159. *
  160. * @return array{action:string, before: ?TaskAssignment, after: ?TaskAssignment}
  161. */
  162. public function upsertStatus(int $taskId, int $swId, string $status): array
  163. {
  164. if (!TaskAssignment::isValidStatus($status)) {
  165. throw new InvalidArgumentException("invalid status: {$status}");
  166. }
  167. $existing = $this->find($taskId, $swId);
  168. if ($existing !== null && $existing->status === $status) {
  169. return ['action' => 'NOOP', 'before' => $existing, 'after' => $existing];
  170. }
  171. if ($existing === null) {
  172. // Default status on a fresh row would be 'zugewiesen', so writing
  173. // that is a no-op — saves an audit row for cells that never had
  174. // a non-default state.
  175. if ($status === TaskAssignment::STATUS_ZUGEWIESEN) {
  176. return ['action' => 'NOOP', 'before' => null, 'after' => null];
  177. }
  178. $stmt = $this->pdo->prepare(
  179. 'INSERT INTO task_assignments (task_id, sprint_worker_id, days, status)
  180. VALUES (?, ?, 0, ?)'
  181. );
  182. $stmt->execute([$taskId, $swId, $status]);
  183. $id = (int) $this->pdo->lastInsertId();
  184. $after = new TaskAssignment($id, $taskId, $swId, 0.0, $status);
  185. return ['action' => 'CREATE', 'before' => null, 'after' => $after];
  186. }
  187. $stmt = $this->pdo->prepare('UPDATE task_assignments SET status = ? WHERE id = ?');
  188. $stmt->execute([$status, $existing->id]);
  189. $after = new TaskAssignment(
  190. $existing->id, $taskId, $swId, $existing->days, $status,
  191. );
  192. return ['action' => 'UPDATE', 'before' => $existing, 'after' => $after];
  193. }
  194. /**
  195. * @param array<string,mixed> $row
  196. */
  197. private static function hydrate(array $row): TaskAssignment
  198. {
  199. return new TaskAssignment(
  200. id: (int) $row['id'],
  201. taskId: (int) $row['task_id'],
  202. sprintWorkerId: (int) $row['sprint_worker_id'],
  203. days: (float) $row['days'],
  204. status: (string) ($row['status'] ?? TaskAssignment::STATUS_ZUGEWIESEN),
  205. );
  206. }
  207. }