days map for a sprint. * Cells with no DB row are simply absent from the map (read as 0 by callers). * * @return array> */ public function grid(int $sprintId): array { $stmt = $this->pdo->prepare( 'SELECT swd.sprint_worker_id, swd.sprint_week_id, swd.days FROM sprint_worker_days swd JOIN sprint_workers sw ON sw.id = swd.sprint_worker_id WHERE sw.sprint_id = ?' ); $stmt->execute([$sprintId]); $out = []; foreach ($stmt as $row) { $swId = (int) $row['sprint_worker_id']; $wkId = (int) $row['sprint_week_id']; $days = (float) $row['days']; $out[$swId][$wkId] = $days; } return $out; } public function find(int $swId, int $weekId): ?SprintWorkerDay { $stmt = $this->pdo->prepare( 'SELECT * FROM sprint_worker_days WHERE sprint_worker_id = ? AND sprint_week_id = ?' ); $stmt->execute([$swId, $weekId]); $row = $stmt->fetch(); return is_array($row) ? self::hydrate($row) : null; } /** * All cells for a given sprint_worker (used by removeWorker to snapshot * rows before the FK cascade wipes them). * * @return list */ public function allForSprintWorker(int $swId): array { $stmt = $this->pdo->prepare( 'SELECT * FROM sprint_worker_days WHERE sprint_worker_id = ?' ); $stmt->execute([$swId]); $out = []; foreach ($stmt as $row) { $out[] = self::hydrate($row); } return $out; } /** * All cells for a given sprint_week (used by weeks-shrink to snapshot * rows before the FK cascade wipes them). * * @return list */ public function allForSprintWeek(int $weekId): array { $stmt = $this->pdo->prepare( 'SELECT * FROM sprint_worker_days WHERE sprint_week_id = ?' ); $stmt->execute([$weekId]); $out = []; foreach ($stmt as $row) { $out[] = self::hydrate($row); } return $out; } /** * @param array $row */ private static function hydrate(array $row): SprintWorkerDay { return new SprintWorkerDay( id: (int) $row['id'], sprintWorkerId: (int) $row['sprint_worker_id'], sprintWeekId: (int) $row['sprint_week_id'], days: (float) $row['days'], ); } /** * Set days for a single (sprint_worker, sprint_week) cell. * * Rules: * - If the row exists and days are unchanged → no DB write, action=NOOP. * - If the row doesn't exist and days === 0 → don't insert, action=NOOP. * - If the row doesn't exist and days > 0 → INSERT, action=CREATE. * - Otherwise → UPDATE, action=UPDATE. * * @return array{action:string, before: ?SprintWorkerDay, after: ?SprintWorkerDay} */ public function upsert(int $swId, int $weekId, float $days): array { $existing = $this->find($swId, $weekId); 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 sprint_worker_days (sprint_worker_id, sprint_week_id, days) VALUES (?, ?, ?)' ); $stmt->execute([$swId, $weekId, $days]); $id = (int) $this->pdo->lastInsertId(); $after = new SprintWorkerDay($id, $swId, $weekId, $days); return ['action' => 'CREATE', 'before' => null, 'after' => $after]; } $stmt = $this->pdo->prepare('UPDATE sprint_worker_days SET days = ? WHERE id = ?'); $stmt->execute([$days, $existing->id]); $after = new SprintWorkerDay($existing->id, $swId, $weekId, $days); return ['action' => 'UPDATE', 'before' => $existing, 'after' => $after]; } }