withCount([ 'attendances as present_count' => fn ($q) => $q->where('in_class', true), ]) ->where('unit_id', $unitId) ->orderBy('date_time_start') ->get() ->each(function (SchoolClass $class) { $class->students_count = $this->activeStudentsQuery($class->class_package_unit_id, $class->unit_id)->count(); }); } public function findByIdForUnit(int $id, int $unitId): SchoolClass { return SchoolClass::with('packageUnit') ->where('unit_id', $unitId) ->findOrFail($id); } public function create(array $data): SchoolClass { return SchoolClass::create($data)->load('packageUnit'); } public function update(int $id, int $unitId, array $data): SchoolClass { $model = $this->findByIdForUnit($id, $unitId); $model->update($data); return $model->fresh('packageUnit'); } public function delete(int $id, int $unitId): bool { $model = $this->findByIdForUnit($id, $unitId); return (bool) $model->delete(); } /** * Lista os alunos com contrato ATIVO no pacote vinculado à aula, * já mesclando a presença registrada (se houver) para aquela aula. */ public function getStudentsForClass(int $classId, int $unitId): array { $class = $this->findByIdForUnit($classId, $unitId); $students = $this->activeStudentsQuery($class->class_package_unit_id, $class->unit_id) ->orderBy('students.name') ->get(); // Presenças já registradas nesta aula, indexadas por aluno $attendances = ClassAttendance::where('class_id', $classId) ->get() ->keyBy('student_id'); // Total de aulas presenciadas por aluno (em qualquer aula) $attendedCounts = ClassAttendance::select('student_id', DB::raw('COUNT(*) as total')) ->whereIn('student_id', $students->pluck('id')) ->where('in_class', true) ->groupBy('student_id') ->pluck('total', 'student_id'); return $students->map(function (Student $student) use ($attendances, $attendedCounts) { $attendance = $attendances->get($student->id); return [ 'student_id' => $student->id, 'name' => $student->name, 'photo_url' => $student->photo_url, 'classes_count' => (int) ($attendedCounts[$student->id] ?? 0), 'in_class' => (bool) ($attendance?->in_class ?? false), 'justified' => (bool) ($attendance?->justified ?? false), 'notes' => $attendance?->notes, ]; })->all(); } /** * Salva a presença da aula em lote (upsert por aluno). * * @param array $items */ public function saveAttendance(int $classId, int $unitId, array $items): void { $class = $this->findByIdForUnit($classId, $unitId); $allowedStudentIds = $this->activeStudentsQuery($class->class_package_unit_id, $unitId) ->whereIn('students.id', collect($items)->pluck('student_id')) ->pluck('students.id'); abort_unless($allowedStudentIds->count() === count($items), 422, 'A lista contém alunos que não pertencem a esta aula.'); DB::transaction(function () use ($classId, $items) { foreach ($items as $item) { $inClass = (bool) ($item['in_class'] ?? false); // Justificativa só vale para faltas; presença nunca fica justificada. $justified = $inClass ? false : (bool) ($item['justified'] ?? false); ClassAttendance::updateOrCreate( [ 'class_id' => $classId, 'student_id' => $item['student_id'], ], [ 'in_class' => $inClass, 'justified' => $justified, 'notes' => $justified ? ($item['notes'] ?? null) : null, ], ); } }); } /** * Query base: alunos com contrato ativo no pacote informado (e na unidade). */ private function activeStudentsQuery(?int $classPackageUnitId, int $unitId) { return Student::query() ->whereHas('contracts', function ($q) use ($classPackageUnitId, $unitId) { $q->where('status', 'active') ->where('unit_id', $unitId); if ($classPackageUnitId) { $q->where('class_package_unit_id', $classPackageUnitId); } }); } }