*/ public function all(): array { $stmt = $this->pdo->query( 'SELECT * FROM workers ORDER BY is_active DESC, LOWER(name) ASC' ); $out = []; foreach ($stmt as $row) { $out[] = self::hydrate($row); } return $out; } public function find(int $id): ?Worker { $stmt = $this->pdo->prepare('SELECT * FROM workers WHERE id = ?'); $stmt->execute([$id]); $row = $stmt->fetch(); return is_array($row) ? self::hydrate($row) : null; } public function findByName(string $name): ?Worker { $stmt = $this->pdo->prepare('SELECT * FROM workers WHERE name = ?'); $stmt->execute([$name]); $row = $stmt->fetch(); return is_array($row) ? self::hydrate($row) : null; } /** * Active workers that are NOT yet members of the given sprint. Used by * the sprint-settings page to populate the "available" list. * * @return list */ public function activeNotInSprint(int $sprintId): array { $stmt = $this->pdo->prepare( 'SELECT w.* FROM workers w WHERE w.is_active = 1 AND w.id NOT IN ( SELECT sw.worker_id FROM sprint_workers sw WHERE sw.sprint_id = ? ) ORDER BY LOWER(w.name) ASC' ); $stmt->execute([$sprintId]); $out = []; foreach ($stmt as $row) { $out[] = self::hydrate($row); } return $out; } /** * Insert a new worker. Throws if the unique name constraint is violated. */ public function create(string $name, bool $isActive, float $defaultRtb): Worker { $now = gmdate('Y-m-d\TH:i:s\Z'); $stmt = $this->pdo->prepare( 'INSERT INTO workers (name, is_active, default_rtb, created_at, updated_at) VALUES (?, ?, ?, ?, ?)' ); $stmt->execute([$name, $isActive ? 1 : 0, $defaultRtb, $now, $now]); $id = (int) $this->pdo->lastInsertId(); $worker = $this->find($id); if ($worker === null) { throw new RuntimeException('Inserted worker not found'); } return $worker; } /** * Apply the given changes (only whitelisted columns). Returns before/after * snapshots so the caller can drive the AuditLogger. * * @param array $changes subset of {name, is_active, default_rtb} * @return array{before: Worker, after: Worker} */ public function update(int $id, array $changes): array { $before = $this->find($id); if ($before === null) { throw new RuntimeException("Worker {$id} not found"); } $changes = array_intersect_key($changes, array_flip(self::UPDATABLE)); if ($changes === []) { return ['before' => $before, 'after' => $before]; } $sets = []; $vals = []; foreach ($changes as $col => $v) { $sets[] = "{$col} = ?"; if ($col === 'is_active') { $vals[] = (bool) $v ? 1 : 0; } elseif ($col === 'default_rtb') { $vals[] = (float) $v; } else { $vals[] = (string) $v; } } $sets[] = 'updated_at = ?'; $vals[] = gmdate('Y-m-d\TH:i:s\Z'); $vals[] = $id; $stmt = $this->pdo->prepare( 'UPDATE workers SET ' . implode(', ', $sets) . ' WHERE id = ?' ); $stmt->execute($vals); $after = $this->find($id) ?? $before; return ['before' => $before, 'after' => $after]; } /** * @param array $row */ private static function hydrate(array $row): Worker { return new Worker( id: (int) $row['id'], name: (string) $row['name'], isActive: ((int) $row['is_active']) === 1, defaultRtb: (float) $row['default_rtb'], createdAt: (string) $row['created_at'], updatedAt: (string) $row['updated_at'], ); } }