add grading, attendnace management
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Attendance;
|
||||
|
||||
use App\Http\Controllers\Api\Core\BaseApiController;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\EarlyDismissalSignature;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EarlyDismissalsController extends BaseApiController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/attendance/early-dismissals
|
||||
* Returns early dismissal records grouped by report_date, plus a signature map.
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$schoolYear = $request->query('school_year');
|
||||
$semester = $request->query('semester');
|
||||
|
||||
$query = DB::table('parent_attendance_reports as par')
|
||||
->select([
|
||||
'par.id',
|
||||
'par.report_date',
|
||||
'par.dismiss_time',
|
||||
'par.reason',
|
||||
'par.status',
|
||||
'par.school_year',
|
||||
'par.semester',
|
||||
'par.class_section_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'cs.class_section_name',
|
||||
])
|
||||
->join('students as s', 's.id', '=', 'par.student_id')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'par.class_section_id')
|
||||
->where('par.type', 'early_dismissal')
|
||||
->orderBy('par.report_date', 'desc')
|
||||
->orderByRaw("CONCAT(s.firstname, ' ', s.lastname) ASC");
|
||||
|
||||
if ($schoolYear) {
|
||||
$query->where('par.school_year', $schoolYear);
|
||||
}
|
||||
if ($semester) {
|
||||
$query->where('par.semester', $semester);
|
||||
}
|
||||
|
||||
$rows = $query->get();
|
||||
|
||||
$groups = [];
|
||||
foreach ($rows as $row) {
|
||||
$date = $row->report_date ?? 'Unknown';
|
||||
$groups[$date][] = [
|
||||
'id' => $row->id,
|
||||
'firstname' => $row->firstname,
|
||||
'lastname' => $row->lastname,
|
||||
'class_section_name' => $row->class_section_name,
|
||||
'dismiss_time' => $row->dismiss_time,
|
||||
'reason' => $row->reason,
|
||||
'status' => $row->status,
|
||||
'school_year' => $row->school_year,
|
||||
'semester' => $row->semester,
|
||||
];
|
||||
}
|
||||
|
||||
$dates = array_keys($groups);
|
||||
$signatures = EarlyDismissalSignature::query()
|
||||
->whereIn('report_date', $dates)
|
||||
->get(['report_date', 'filename', 'original_name']);
|
||||
|
||||
$signatureByDate = [];
|
||||
foreach ($signatures as $sig) {
|
||||
$signatureByDate[(string) $sig->report_date] = [
|
||||
'filename' => $sig->filename,
|
||||
'original_name' => $sig->original_name,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'groups' => $groups,
|
||||
'signatureByDate' => $signatureByDate,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/attendance/early-dismissals/student-options
|
||||
* Returns active students with their current class section.
|
||||
*/
|
||||
public function studentOptions(): JsonResponse
|
||||
{
|
||||
$students = DB::table('students as s')
|
||||
->select([
|
||||
's.id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'cs.class_section_name',
|
||||
])
|
||||
->leftJoin('student_class as sc', function ($j) {
|
||||
$j->on('sc.student_id', '=', 's.id')
|
||||
->where('sc.school_year', '=', Configuration::getConfigValueByKey('school_year'));
|
||||
})
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->where('s.is_active', 1)
|
||||
->orderByRaw("CONCAT(s.firstname, ' ', s.lastname) ASC")
|
||||
->get();
|
||||
|
||||
return $this->success(['students' => $students]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/attendance/early-dismissals
|
||||
* Creates a new early dismissal record.
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'student_id' => ['required', 'integer', 'min:1', 'exists:students,id'],
|
||||
'date' => ['required', 'date_format:Y-m-d'],
|
||||
'dismiss_time' => ['required', 'date_format:H:i'],
|
||||
'reason' => ['nullable', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->respondValidationError($validator->errors()->toArray());
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
|
||||
$schoolYear = (string) Configuration::getConfigValueByKey('school_year');
|
||||
$semester = (string) Configuration::getConfigValueByKey('semester');
|
||||
|
||||
$sc = DB::table('student_class')
|
||||
->where('student_id', $data['student_id'])
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('id')
|
||||
->first();
|
||||
|
||||
$classSectionId = $sc?->class_section_id ?? null;
|
||||
|
||||
$parentId = DB::table('students')
|
||||
->where('id', $data['student_id'])
|
||||
->value('parent_id') ?? Auth::id();
|
||||
|
||||
DB::table('parent_attendance_reports')->insert([
|
||||
'parent_id' => $parentId ?? Auth::id(),
|
||||
'student_id' => $data['student_id'],
|
||||
'class_section_id' => $classSectionId,
|
||||
'report_date' => $data['date'],
|
||||
'type' => 'early_dismissal',
|
||||
'dismiss_time' => $data['dismiss_time'],
|
||||
'reason' => $data['reason'] ?? null,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'status' => 'new',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $this->success([], 'Early dismissal saved.', Response::HTTP_CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/attendance/early-dismissals/signature
|
||||
* Uploads (or replaces) the signature file for a report date.
|
||||
*/
|
||||
public function uploadSignature(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'report_date' => ['required', 'date_format:Y-m-d'],
|
||||
'school_year' => ['nullable', 'string', 'max:16'],
|
||||
'semester' => ['nullable', 'string', 'max:32'],
|
||||
'signature_file' => ['required', 'file', 'mimes:jpg,jpeg,png,webp,gif,pdf', 'max:5120'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->respondValidationError($validator->errors()->toArray());
|
||||
}
|
||||
|
||||
$file = $request->file('signature_file');
|
||||
$reportDate = $request->input('report_date');
|
||||
$schoolYear = $request->input('school_year') ?: (string) Configuration::getConfigValueByKey('school_year');
|
||||
$semester = $request->input('semester') ?: (string) Configuration::getConfigValueByKey('semester');
|
||||
|
||||
$dir = storage_path('uploads/early_dismissal_signatures');
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$existing = EarlyDismissalSignature::query()
|
||||
->where('report_date', $reportDate)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->first();
|
||||
|
||||
if ($existing?->filename && file_exists($dir . '/' . $existing->filename)) {
|
||||
unlink($dir . '/' . $existing->filename);
|
||||
}
|
||||
|
||||
$filename = uniqid('sig_', true) . '.' . $file->getClientOriginalExtension();
|
||||
$file->move($dir, $filename);
|
||||
|
||||
EarlyDismissalSignature::updateOrCreate(
|
||||
['report_date' => $reportDate, 'school_year' => $schoolYear, 'semester' => $semester],
|
||||
[
|
||||
'filename' => $filename,
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'mime_type' => $file->getClientMimeType(),
|
||||
'file_size' => $file->getSize(),
|
||||
'uploaded_by' => Auth::id(),
|
||||
]
|
||||
);
|
||||
|
||||
return $this->success(['filename' => $filename], 'Signature uploaded.');
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ class GradingController extends BaseApiController
|
||||
'scores' => GradingScoreResource::collection($data['scores']),
|
||||
'type' => $data['type'],
|
||||
'class_section_id' => $data['class_section_id'],
|
||||
'class_section_name' => $data['class_section_name'],
|
||||
'semester' => $data['semester'],
|
||||
'scores_locked' => $data['scores_locked'],
|
||||
]);
|
||||
|
||||
@@ -67,6 +67,7 @@ class FinalController extends BaseApiController
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'students' => ScoreStudentResource::collection($data['students']),
|
||||
'class_section_name' => $data['classSectionName'],
|
||||
'semester' => $data['semester'],
|
||||
'school_year' => $data['schoolYear'],
|
||||
'scores_locked' => $data['scoresLocked'],
|
||||
|
||||
@@ -73,6 +73,7 @@ class HomeworkController extends BaseApiController
|
||||
'ok' => true,
|
||||
'students' => ScoreStudentResource::collection($data['students']),
|
||||
'headers' => $data['headers'],
|
||||
'class_section_name' => $data['classSectionName'],
|
||||
'semester' => $data['semester'],
|
||||
'school_year' => $data['schoolYear'],
|
||||
'scores_locked' => $data['scoresLocked'],
|
||||
|
||||
@@ -67,6 +67,7 @@ class MidtermController extends BaseApiController
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'students' => ScoreStudentResource::collection($data['students']),
|
||||
'class_section_name' => $data['classSectionName'],
|
||||
'semester' => $data['semester'],
|
||||
'school_year' => $data['schoolYear'],
|
||||
'scores_locked' => $data['scoresLocked'],
|
||||
|
||||
@@ -71,6 +71,7 @@ class ParticipationController extends BaseApiController
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'students' => ScoreStudentResource::collection($data['students']),
|
||||
'class_section_name' => $data['classSectionName'],
|
||||
'semester' => $data['semester'],
|
||||
'school_year' => $data['schoolYear'],
|
||||
'scores_locked' => $data['scoresLocked'],
|
||||
|
||||
@@ -73,6 +73,7 @@ class ProjectController extends BaseApiController
|
||||
'ok' => true,
|
||||
'students' => ScoreStudentResource::collection($data['students']),
|
||||
'headers' => $data['headers'],
|
||||
'class_section_name' => $data['classSectionName'],
|
||||
'semester' => $data['semester'],
|
||||
'school_year' => $data['schoolYear'],
|
||||
'scores_locked' => $data['scoresLocked'],
|
||||
|
||||
@@ -73,6 +73,7 @@ class QuizController extends BaseApiController
|
||||
'ok' => true,
|
||||
'students' => ScoreStudentResource::collection($data['students']),
|
||||
'headers' => $data['headers'],
|
||||
'class_section_name' => $data['classSectionName'],
|
||||
'semester' => $data['semester'],
|
||||
'school_year' => $data['schoolYear'],
|
||||
'scores_locked' => $data['scoresLocked'],
|
||||
|
||||
@@ -66,6 +66,7 @@ class ScoreCommentController extends BaseApiController
|
||||
'semester' => $data['semester'],
|
||||
'school_year' => $data['schoolYear'],
|
||||
'class_section_id' => $data['classSectionId'],
|
||||
'class_section_name' => $data['classSectionName'],
|
||||
'scores_locked' => $data['scoresLocked'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class DailyAttendanceResource extends JsonResource
|
||||
{
|
||||
public bool $preserveKeys = true;
|
||||
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -14,6 +14,7 @@ class ScoreStudentResource extends JsonResource
|
||||
'firstname' => $this->resource['firstname'] ?? null,
|
||||
'lastname' => $this->resource['lastname'] ?? null,
|
||||
'school_id' => $this->resource['school_id'] ?? null,
|
||||
'score' => $this->resource['score'] ?? null,
|
||||
'scores' => $this->resource['scores'] ?? [],
|
||||
'comments' => $this->resource['comments'] ?? null,
|
||||
];
|
||||
|
||||
@@ -16,6 +16,7 @@ class StudentClass extends BaseModel
|
||||
'school_id',
|
||||
'class_section_id',
|
||||
'school_year',
|
||||
'semester',
|
||||
'is_event_only',
|
||||
'description',
|
||||
'updated_by',
|
||||
|
||||
@@ -159,8 +159,8 @@ class AssignmentService
|
||||
$currentSemester = $this->getCurrentSemester();
|
||||
$currentSchoolYear = $this->getCurrentSchoolYear();
|
||||
|
||||
$teacherClasses = $this->dataLoader->loadTeacherClasses();
|
||||
$studentClasses = $this->dataLoader->loadStudentClasses();
|
||||
$teacherClasses = $this->dataLoader->loadTeacherClasses($currentSchoolYear);
|
||||
$studentClasses = $this->dataLoader->loadStudentClasses($currentSchoolYear);
|
||||
|
||||
$teacherBySection = $teacherClasses->groupBy('class_section_id');
|
||||
$studentBySection = $studentClasses->groupBy('class_section_id');
|
||||
|
||||
@@ -515,43 +515,74 @@ class AttendanceQueryService
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bulk-load student profiles for the whole section in one query
|
||||
$studentsArray = is_array($students) ? $students : $students->toArray();
|
||||
$studentIds = array_map(fn($sc) => (int)($sc['student_id'] ?? $sc->student_id ?? 0), $studentsArray);
|
||||
$studentProfiles = $this->student
|
||||
->query()
|
||||
->select(['id', 'firstname', 'lastname', 'school_id'])
|
||||
->whereIn('id', $studentIds)
|
||||
->where('is_active', 1)
|
||||
->get()
|
||||
->keyBy('id')
|
||||
->map(fn($row) => $row->toArray())
|
||||
->all();
|
||||
|
||||
$activeIds = array_keys($studentProfiles);
|
||||
if (empty($activeIds)) {
|
||||
unset($studentsBySection[$secCode], $datesBySection[$secCode], $attendanceData[$secCode], $attendanceRecord[$secCode]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bulk-load all attendance entries for the section in one query
|
||||
$allEntries = AttendanceData::query()
|
||||
->whereIn('student_id', $activeIds)
|
||||
->where(function ($q) use ($secCode, $secPk) {
|
||||
$q->where('class_section_id', $secCode);
|
||||
if ($secPk && (string)$secPk !== (string)$secCode) {
|
||||
$q->orWhere('class_section_id', $secPk);
|
||||
}
|
||||
})
|
||||
->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester))
|
||||
->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear))
|
||||
->orderBy('date')
|
||||
->get()
|
||||
->map(fn($row) => $row->toArray())
|
||||
->groupBy('student_id')
|
||||
->map(fn($group) => $group->values()->all())
|
||||
->all();
|
||||
|
||||
// Bulk-load all attendance records (summaries) for the section in one query
|
||||
$allRecords = AttendanceRecord::query()
|
||||
->whereIn('student_id', $activeIds)
|
||||
->where(function ($q) use ($secCode, $secPk) {
|
||||
$q->where('class_section_id', $secCode);
|
||||
if ($secPk && (string)$secPk !== (string)$secCode) {
|
||||
$q->orWhere('class_section_id', $secPk);
|
||||
}
|
||||
})
|
||||
->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester))
|
||||
->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear))
|
||||
->get()
|
||||
->keyBy('student_id')
|
||||
->map(fn($row) => $row->toArray())
|
||||
->all();
|
||||
|
||||
$hasRoster = false;
|
||||
|
||||
foreach ($students as $sc) {
|
||||
$studentId = (int)$sc['student_id'];
|
||||
|
||||
$student = $this->student
|
||||
->query()
|
||||
->select(['id', 'firstname', 'lastname', 'school_id'])
|
||||
->where('id', $studentId)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
foreach ($studentIds as $studentId) {
|
||||
$student = $studentProfiles[$studentId] ?? null;
|
||||
if (!$student) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$student = $student->toArray();
|
||||
$studentsBySection[$secCode][] = $student;
|
||||
$studentSchoolMap[$studentId] = (string)($student['school_id'] ?? '');
|
||||
$hasRoster = true;
|
||||
|
||||
$entries = AttendanceData::query()
|
||||
->where('student_id', $studentId)
|
||||
->where(function ($q) use ($secCode, $secPk) {
|
||||
$q->where('class_section_id', $secCode);
|
||||
if ($secPk && (string)$secPk !== (string)$secCode) {
|
||||
$q->orWhere('class_section_id', $secPk);
|
||||
}
|
||||
})
|
||||
->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester))
|
||||
->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear))
|
||||
->orderBy('date')
|
||||
->get()
|
||||
->map(fn($row) => $row->toArray())
|
||||
->all();
|
||||
|
||||
$entries = $this->attendanceService->normalizeAttendanceEntries($entries);
|
||||
$entries = $this->attendanceService->normalizeAttendanceEntries(
|
||||
array_map(fn($r) => (array)$r, $allEntries[$studentId] ?? [])
|
||||
);
|
||||
$attendanceData[$secCode][$studentId] = $entries;
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
@@ -561,19 +592,7 @@ class AttendanceQueryService
|
||||
}
|
||||
}
|
||||
|
||||
$summary = AttendanceRecord::query()
|
||||
->where('student_id', $studentId)
|
||||
->where(function ($q) use ($secCode, $secPk) {
|
||||
$q->where('class_section_id', $secCode);
|
||||
if ($secPk && (string)$secPk !== (string)$secCode) {
|
||||
$q->orWhere('class_section_id', $secPk);
|
||||
}
|
||||
})
|
||||
->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester))
|
||||
->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear))
|
||||
->first();
|
||||
|
||||
$attendanceRecord[$secCode][$studentId] = $summary?->toArray() ?? [
|
||||
$attendanceRecord[$secCode][$studentId] = $allRecords[$studentId] ?? [
|
||||
'total_presence' => 0,
|
||||
'total_late' => 0,
|
||||
'total_absence' => 0,
|
||||
@@ -627,7 +646,7 @@ class AttendanceQueryService
|
||||
foreach ($events as $event) {
|
||||
$d = substr((string)($event['date'] ?? ''), 0, 10);
|
||||
if ($d !== '') {
|
||||
$noSchoolDays[$d] = true;
|
||||
$noSchoolDays[$d] = trim((string)($event['title'] ?? 'No School')) ?: 'No School';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class SemesterRangeService
|
||||
@@ -19,6 +20,16 @@ class SemesterRangeService
|
||||
|
||||
public function getSchoolYearRange(string $schoolYear): array
|
||||
{
|
||||
$fallStart = Configuration::getConfig('fall_semester_start');
|
||||
$lastDay = Configuration::getConfig('last_school_day');
|
||||
|
||||
if ($fallStart && $lastDay) {
|
||||
return [
|
||||
Carbon::parse($fallStart)->toDateString(),
|
||||
Carbon::parse($lastDay)->toDateString(),
|
||||
];
|
||||
}
|
||||
|
||||
[$startYear, $endYear] = $this->parseSchoolYear($schoolYear);
|
||||
|
||||
return [
|
||||
@@ -29,9 +40,29 @@ class SemesterRangeService
|
||||
|
||||
public function getSemesterRange(string $schoolYear, string $semester): ?array
|
||||
{
|
||||
[$startYear, $endYear] = $this->parseSchoolYear($schoolYear);
|
||||
$semester = $this->normalizeSemester($semester);
|
||||
|
||||
$fallStart = Configuration::getConfig('fall_semester_start');
|
||||
$springStart = Configuration::getConfig('spring_semester_start');
|
||||
$lastDay = Configuration::getConfig('last_school_day');
|
||||
|
||||
if ($semester === 'Fall' && $fallStart && $springStart) {
|
||||
return [
|
||||
Carbon::parse($fallStart)->toDateString(),
|
||||
Carbon::parse($springStart)->subDay()->toDateString(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($semester === 'Spring' && $springStart && $lastDay) {
|
||||
return [
|
||||
Carbon::parse($springStart)->toDateString(),
|
||||
Carbon::parse($lastDay)->toDateString(),
|
||||
];
|
||||
}
|
||||
|
||||
// Fallback to hardcoded logic when config keys are missing
|
||||
[$startYear, $endYear] = $this->parseSchoolYear($schoolYear);
|
||||
|
||||
return match ($semester) {
|
||||
'Fall' => [
|
||||
Carbon::create($startYear, 9, 21)->toDateString(),
|
||||
|
||||
@@ -2,27 +2,14 @@
|
||||
|
||||
namespace App\Services\Grading;
|
||||
|
||||
use App\Models\AttendanceRecord;
|
||||
use App\Models\CalendarEvent;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\GradingLock;
|
||||
use App\Models\Student;
|
||||
use App\Services\Scores\AttendanceCalculator;
|
||||
use App\Services\Scores\SemesterScoreService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GradingOverviewService
|
||||
{
|
||||
private AttendanceCalculator $attendanceCalculator;
|
||||
|
||||
public function __construct(private SemesterScoreService $semesterScoreService)
|
||||
public function __construct()
|
||||
{
|
||||
$this->attendanceCalculator = new AttendanceCalculator(
|
||||
new AttendanceRecord(),
|
||||
new Configuration(),
|
||||
new CalendarEvent()
|
||||
);
|
||||
}
|
||||
|
||||
public function overview(?int $classId, ?string $semester, ?string $schoolYear): array
|
||||
@@ -38,62 +25,9 @@ class GradingOverviewService
|
||||
$scoresReleasedFall = $this->getParentScoresReleasedForSemester('Fall');
|
||||
$scoresReleasedSpring = $this->getParentScoresReleasedForSemester('Spring');
|
||||
|
||||
if ($classId && $this->semesterScoreService) {
|
||||
$sectionIds = ClassSection::query()
|
||||
->select('class_section_id')
|
||||
->where('class_id', $classId)
|
||||
->pluck('class_section_id')
|
||||
->map(fn ($v) => (int) $v)
|
||||
->all();
|
||||
foreach ($sectionIds as $sectionId) {
|
||||
$studentTeacherInfo = Student::getStudentInfoByClassSectionId(
|
||||
$sectionId,
|
||||
$semester,
|
||||
$schoolYear
|
||||
);
|
||||
if (empty($studentTeacherInfo)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rows = $this->buildGradingRows($semester, $schoolYear);
|
||||
|
||||
$counts = $this->loadScoreCounts($rows, $semester, $schoolYear);
|
||||
|
||||
$sectionsNeedingRefresh = [];
|
||||
foreach ($rows as $row) {
|
||||
$sectionId = (int) ($row['section_id'] ?? 0);
|
||||
if ($sectionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($row['ss_ptap_score'] === null || $row['ss_semester_score'] === null) {
|
||||
$sectionsNeedingRefresh[$sectionId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($sectionsNeedingRefresh)) {
|
||||
foreach (array_keys($sectionsNeedingRefresh) as $sectionId) {
|
||||
$studentTeacherInfo = Student::getStudentInfoByClassSectionId(
|
||||
$sectionId,
|
||||
$semester,
|
||||
$schoolYear
|
||||
);
|
||||
if (empty($studentTeacherInfo)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
$rows = $this->buildGradingRows($semester, $schoolYear);
|
||||
}
|
||||
|
||||
$grades = [];
|
||||
$studentsBySection = [];
|
||||
foreach ($rows as $row) {
|
||||
@@ -126,18 +60,7 @@ class GradingOverviewService
|
||||
continue;
|
||||
}
|
||||
|
||||
$attendanceScore = $this->calculateAttendanceScoreForStudent(
|
||||
$studentId,
|
||||
$semester,
|
||||
$schoolYear,
|
||||
$sectionId
|
||||
);
|
||||
if ($attendanceScore === null) {
|
||||
$rawAttendance = $row['ss_attendance_score'] ?? null;
|
||||
if ($rawAttendance !== null && $rawAttendance !== '') {
|
||||
$attendanceScore = round((float) $rawAttendance, 2);
|
||||
}
|
||||
}
|
||||
$attendanceScore = $this->normalizeScore($row['ss_attendance_score'] ?? null);
|
||||
|
||||
$studentRow = [
|
||||
'id' => $studentId,
|
||||
@@ -320,21 +243,6 @@ class GradingOverviewService
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function calculateAttendanceScoreForStudent(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): ?float
|
||||
{
|
||||
try {
|
||||
$result = $this->attendanceCalculator->calculate($studentId, $semester, $schoolYear, $classSectionId);
|
||||
$score = $result['attendance_score'] ?? null;
|
||||
if ($score === null || $score === '') {
|
||||
return null;
|
||||
}
|
||||
return round((float) $score, 2);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function normalizeScore($value): ?float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Models\Quiz;
|
||||
use App\Models\ScoreComment;
|
||||
use App\Models\SemesterScore;
|
||||
use App\Models\Student;
|
||||
use App\Models\ClassSection;
|
||||
use App\Services\Scores\SemesterScoreService;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -44,6 +45,9 @@ class GradingScoreService
|
||||
'scores' => $scores,
|
||||
'type' => $type,
|
||||
'class_section_id' => $classSectionId,
|
||||
'class_section_name' => $classSectionId > 0
|
||||
? (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? '')
|
||||
: '',
|
||||
'semester' => $semester,
|
||||
'scores_locked' => $scoresLocked,
|
||||
];
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\GradingLock;
|
||||
use App\Models\MissingScoreOverride;
|
||||
use App\Models\Student;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\ClassSection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ExamScoreService
|
||||
@@ -79,6 +80,7 @@ class ExamScoreService
|
||||
|
||||
return [
|
||||
'students' => $rows,
|
||||
'classSectionName' => (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? ''),
|
||||
'semester' => $semester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'scoresLocked' => GradingLock::isLocked($classSectionId, $semester, $schoolYear),
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\MissingScoreOverride;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\ClassSection;
|
||||
|
||||
class HomeworkScoreService
|
||||
{
|
||||
@@ -84,6 +85,7 @@ class HomeworkScoreService
|
||||
->get()
|
||||
->map(fn ($row) => [
|
||||
'student_id' => (int) $row->id,
|
||||
'school_id' => (string) $row->school_id,
|
||||
'firstname' => (string) $row->firstname,
|
||||
'lastname' => (string) $row->lastname,
|
||||
'scores' => [],
|
||||
@@ -115,6 +117,7 @@ class HomeworkScoreService
|
||||
return [
|
||||
'students' => $students,
|
||||
'headers' => $headers,
|
||||
'classSectionName' => (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? ''),
|
||||
'semester' => $semester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'scoresLocked' => $this->isLockedForAnySchoolYear($classSectionId, $semester, $schoolYearVariants),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Services\Scores;
|
||||
|
||||
use App\Models\GradingLock;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\MissingScoreOverride;
|
||||
use App\Models\Participation;
|
||||
use App\Models\Student;
|
||||
@@ -95,6 +96,7 @@ class ParticipationScoreService
|
||||
|
||||
return [
|
||||
'students' => $students,
|
||||
'classSectionName' => (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? ''),
|
||||
'semester' => $semester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'scoresLocked' => GradingLock::isLocked($classSectionId, $semester, $schoolYear),
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Project;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\ClassSection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ProjectScoreService
|
||||
@@ -76,6 +77,7 @@ class ProjectScoreService
|
||||
->get()
|
||||
->map(fn ($row) => [
|
||||
'student_id' => (int) $row->id,
|
||||
'school_id' => (string) $row->school_id,
|
||||
'firstname' => (string) $row->firstname,
|
||||
'lastname' => (string) $row->lastname,
|
||||
'scores' => [],
|
||||
@@ -107,6 +109,7 @@ class ProjectScoreService
|
||||
return [
|
||||
'students' => $students,
|
||||
'headers' => $headers,
|
||||
'classSectionName' => (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? ''),
|
||||
'semester' => $semester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'scoresLocked' => GradingLock::isLocked($classSectionId, $semester, $schoolYear),
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Quiz;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\ClassSection;
|
||||
|
||||
class QuizScoreService
|
||||
{
|
||||
@@ -75,6 +76,7 @@ class QuizScoreService
|
||||
->get()
|
||||
->map(fn ($row) => [
|
||||
'student_id' => (int) $row->id,
|
||||
'school_id' => (string) $row->school_id,
|
||||
'firstname' => (string) $row->firstname,
|
||||
'lastname' => (string) $row->lastname,
|
||||
'scores' => [],
|
||||
@@ -106,6 +108,7 @@ class QuizScoreService
|
||||
return [
|
||||
'students' => $students,
|
||||
'headers' => $headers,
|
||||
'classSectionName' => (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? ''),
|
||||
'semester' => $semester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'scoresLocked' => GradingLock::isLocked($classSectionId, $semester, $schoolYear),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Services\Scores;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\GradingLock;
|
||||
use App\Models\MissingScoreOverride;
|
||||
use App\Models\ScoreComment;
|
||||
@@ -111,6 +112,9 @@ class ScoreCommentService
|
||||
'semester' => $activeSemester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'classSectionId' => $isAllSections ? 'allsections' : $classSectionInt,
|
||||
'classSectionName' => (!$isAllSections && $classSectionInt)
|
||||
? (string) (ClassSection::query()->where('class_section_id', $classSectionInt)->value('class_section_name') ?? '')
|
||||
: '',
|
||||
'scoresLocked' => (!$isAllSections && $classSectionInt)
|
||||
? GradingLock::isLocked($classSectionInt, $activeSemester, $schoolYear)
|
||||
: false,
|
||||
|
||||
@@ -9,11 +9,14 @@ class ScorePredictorDataService
|
||||
public function classSections(string $schoolYear): array
|
||||
{
|
||||
return DB::table('classSection as cs')
|
||||
->select('cs.*')
|
||||
->select([
|
||||
'cs.class_section_id',
|
||||
'cs.class_section_name',
|
||||
])
|
||||
->join('student_class as sc', 'sc.class_section_id', '=', 'cs.class_section_id')
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->where('cs.class_section_name', 'not like', 'KG%')
|
||||
->groupBy('cs.id')
|
||||
->distinct()
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
@@ -28,9 +31,9 @@ class ScorePredictorDataService
|
||||
's.school_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'fall.semester_score as fall_score',
|
||||
'spring.semester_score as spring_score',
|
||||
'sc.class_section_id as class_section_id',
|
||||
DB::raw('MAX(fall.semester_score) as fall_score'),
|
||||
DB::raw('MAX(spring.semester_score) as spring_score'),
|
||||
DB::raw('MAX(sc.class_section_id) as class_section_id'),
|
||||
])
|
||||
->leftJoin('student_class as sc', function ($join) use ($schoolYear) {
|
||||
$join->on('sc.student_id', '=', 's.id')
|
||||
@@ -58,7 +61,7 @@ class ScorePredictorDataService
|
||||
}
|
||||
|
||||
return $builder
|
||||
->groupBy('s.id')
|
||||
->groupBy('s.id', 's.school_id', 's.firstname', 's.lastname')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
@@ -90,6 +90,7 @@ class StudentAssignmentService
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'is_event_only' => $eventFlag,
|
||||
'updated_by' => $userId,
|
||||
'updated_at' => now(),
|
||||
|
||||
@@ -147,19 +147,19 @@ class WhatsappContactService
|
||||
}
|
||||
|
||||
$rows = DB::table('student_class as sc')
|
||||
->select("
|
||||
sc.class_section_id,
|
||||
u.id AS primary_id,
|
||||
u.firstname AS primary_firstname,
|
||||
u.lastname AS primary_lastname,
|
||||
u.email AS primary_email,
|
||||
u.cellphone AS primary_phone,
|
||||
sp.id AS second_id,
|
||||
sp.secondparent_firstname AS second_firstname,
|
||||
sp.secondparent_lastname AS second_lastname,
|
||||
sp.secondparent_email AS second_email,
|
||||
sp.secondparent_phone AS second_phone
|
||||
")
|
||||
->select([
|
||||
'sc.class_section_id',
|
||||
'u.id as primary_id',
|
||||
'u.firstname as primary_firstname',
|
||||
'u.lastname as primary_lastname',
|
||||
'u.email as primary_email',
|
||||
'u.cellphone as primary_phone',
|
||||
'sp.id as second_id',
|
||||
'sp.secondparent_firstname as second_firstname',
|
||||
'sp.secondparent_lastname as second_lastname',
|
||||
'sp.secondparent_email as second_email',
|
||||
'sp.secondparent_phone as second_phone',
|
||||
])
|
||||
->join('students as s', 's.id', '=', 'sc.student_id')
|
||||
->join('users as u', 'u.id', '=', 's.parent_id')
|
||||
->leftJoin('parents as sp', function ($join) {
|
||||
|
||||
@@ -58,6 +58,7 @@ use App\Http\Controllers\Api\Inventory\InventoryCategoryController;
|
||||
use App\Http\Controllers\Api\Inventory\InventoryMovementController;
|
||||
use App\Http\Controllers\Api\Auth\IpBanController;
|
||||
use App\Http\Controllers\Api\Frontend\LandingPageController;
|
||||
use App\Http\Controllers\Api\Attendance\EarlyDismissalsController;
|
||||
use App\Http\Controllers\Api\Attendance\LateSlipLogsController;
|
||||
use App\Http\Controllers\Api\Messaging\MessagesController;
|
||||
use App\Http\Controllers\Api\Incidents\IncidentController as IncidentApiController;
|
||||
@@ -433,6 +434,13 @@ Route::prefix('v1')->group(function () {
|
||||
Route::get('{id}', [LateSlipLogsController::class, 'show']);
|
||||
Route::delete('{id}', [LateSlipLogsController::class, 'destroy']);
|
||||
});
|
||||
|
||||
Route::prefix('early-dismissals')->group(function () {
|
||||
Route::get('/', [EarlyDismissalsController::class, 'index']);
|
||||
Route::get('/student-options', [EarlyDismissalsController::class, 'studentOptions']);
|
||||
Route::post('/', [EarlyDismissalsController::class, 'store']);
|
||||
Route::post('/signature', [EarlyDismissalsController::class, 'uploadSignature']);
|
||||
});
|
||||
});
|
||||
|
||||
Route::middleware('auth:api')->prefix('attendance-tracking')->group(function () {
|
||||
|
||||
Reference in New Issue
Block a user