recreate project
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Interfaces\ScoreCalculatorInterface;
|
||||
use App\Models\SemesterScoreModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\FinalExamModel;
|
||||
use App\Models\MidtermExamModel;
|
||||
use App\Models\ParticipationModel;
|
||||
use RuntimeException;
|
||||
|
||||
class SemesterScoreService
|
||||
{
|
||||
private SemesterScoreModel $semesterScoreModel;
|
||||
protected ConfigurationModel $configModel;
|
||||
|
||||
protected string $semester;
|
||||
protected string $schoolYear;
|
||||
|
||||
/** Who is performing the update (nullable if unknown) */
|
||||
private ?int $actorId = null;
|
||||
|
||||
/** @var array<string, ScoreCalculatorInterface> */
|
||||
private array $scoreCalculators = [];
|
||||
|
||||
public function __construct(
|
||||
SemesterScoreModel $semesterScoreModel,
|
||||
ConfigurationModel $configModel,
|
||||
ScoreCalculatorInterface ...$scoreCalculators
|
||||
) {
|
||||
$this->semesterScoreModel = $semesterScoreModel;
|
||||
$this->configModel = $configModel;
|
||||
|
||||
$this->semester = (string) $this->configModel->getConfig('semester');
|
||||
$this->schoolYear = (string) $this->configModel->getConfig('school_year');
|
||||
|
||||
// Default actor from session (can be overridden later)
|
||||
$this->actorId = (int) (session()->get('user_id') ?? 0) ?: null;
|
||||
|
||||
foreach ($scoreCalculators as $calculator) {
|
||||
$this->scoreCalculators[get_class($calculator)] = $calculator;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scores for multiple students.
|
||||
* - If $semester/$schoolYear not provided, they default from service config.
|
||||
* - If $updatedBy is provided, it overrides the default actor for this batch.
|
||||
* - Each student can carry its own 'updated_by'; that per-student value wins.
|
||||
*/
|
||||
public function updateScoresForStudents(
|
||||
array $students,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): void {
|
||||
// Defaults from service config if omitted
|
||||
$semester = $semester ?? $this->semester;
|
||||
$schoolYear = $schoolYear ?? $this->schoolYear;
|
||||
|
||||
foreach ($students as $student) {
|
||||
$this->updateStudentScores($student, $semester, $schoolYear);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update (compute + persist) the scores for a single student.
|
||||
*/
|
||||
public function updateStudentScores(array $student, string $semester, string $schoolYear): void
|
||||
{
|
||||
// Validate required student data
|
||||
foreach (['student_id', 'school_id', 'class_section_id'] as $field) {
|
||||
if (!isset($student[$field])) {
|
||||
throw new RuntimeException("Missing required student field: {$field}");
|
||||
}
|
||||
}
|
||||
|
||||
// Per-student actor resolution:
|
||||
// 1) student['updated_by'] if present,
|
||||
// 2) batch/service actorId if set,
|
||||
// 3) session user as last resort,
|
||||
// 4) null if none
|
||||
$updatedBy = isset($student['updated_by'])
|
||||
? (int) $student['updated_by']
|
||||
: ($this->actorId ?? ((int) (session()->get('user_id') ?? 0) ?: null));
|
||||
|
||||
// Compute all score components
|
||||
$scoreData = $this->calculateAllScores(
|
||||
(int) $student['student_id'],
|
||||
$semester,
|
||||
$schoolYear,
|
||||
(int) ($student['class_section_id'] ?? 0) ?: null
|
||||
);
|
||||
|
||||
// Persist (upsert) the scores
|
||||
$this->saveStudentScores(
|
||||
(int) $student['student_id'],
|
||||
(string) $student['school_id'],
|
||||
(int) $student['class_section_id'],
|
||||
$updatedBy, // nullable OK
|
||||
$semester,
|
||||
$schoolYear,
|
||||
$scoreData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates all components needed for the semester score.
|
||||
* Returns an associative array of score columns => values (flat).
|
||||
*/
|
||||
private function calculateAllScores(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): array
|
||||
{
|
||||
$scoreData = [
|
||||
'updated_at' => utc_now(),
|
||||
];
|
||||
|
||||
// Calculate individual components via injected calculators
|
||||
foreach ($this->scoreCalculators as $calculator) {
|
||||
try {
|
||||
$scoreData = array_merge(
|
||||
$scoreData,
|
||||
$calculator->calculate($studentId, $semester, $schoolYear, $classSectionId)
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
log_message(
|
||||
'error',
|
||||
'SemesterScoreService calculator failed for student '
|
||||
. $studentId . ' (' . get_class($calculator) . '): ' . $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Participation fallback if calculator didn’t provide it
|
||||
if (!isset($scoreData['participation_score'])) {
|
||||
$participationModel = new ParticipationModel();
|
||||
$participationScore = $participationModel->getParticipationScore($studentId, $semester, $schoolYear, $classSectionId);
|
||||
$scoreData['participation_score'] = $participationScore !== null ? (float) $participationScore : null;
|
||||
}
|
||||
|
||||
// Composite scores
|
||||
$scoreData['ptap_score'] = $this->calculatePTAPScore($scoreData);
|
||||
$midfinal = $this->calculateSemesterScore($scoreData, $semester, $studentId, $schoolYear, $classSectionId);
|
||||
|
||||
return array_merge($scoreData, [
|
||||
'semester_score' => $midfinal['semester_score'],
|
||||
'midterm_exam_score' => $midfinal['midterm_score'],
|
||||
'final_exam_score' => $midfinal['final_score'],
|
||||
'participation_score' => $scoreData['participation_score'], // explicit keep
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PTAP weighting rules (tests are represented by quiz averages):
|
||||
* - No tests and no projects: 50% homework, 50% participation.
|
||||
* - No projects: 40% homework, 40% participation, 20% tests.
|
||||
* - No tests: 34% homework, 33% participation, 33% projects.
|
||||
* - All available: 30% homework, 30% participation, 30% projects, 10% tests.
|
||||
*/
|
||||
private function calculatePTAPScore(array $scores): ?float
|
||||
{
|
||||
$avgHomework = $scores['homework_avg'] ?? null;
|
||||
$avgQuiz = $scores['quiz_avg'] ?? null;
|
||||
$avgProject = $scores['project_avg'] ?? null;
|
||||
$avgParticipation = $scores['participation_score'] ?? null;
|
||||
|
||||
$hasTests = $avgQuiz !== null;
|
||||
$hasProjects = $avgProject !== null;
|
||||
|
||||
if (!$hasTests && !$hasProjects) {
|
||||
if ($avgHomework === null || $avgParticipation === null) {
|
||||
return null;
|
||||
}
|
||||
return round(($avgHomework * 0.5) + ($avgParticipation * 0.5), 1);
|
||||
}
|
||||
|
||||
if (!$hasProjects) {
|
||||
if ($avgHomework === null || $avgParticipation === null || $avgQuiz === null) {
|
||||
return null;
|
||||
}
|
||||
return round(($avgHomework * 0.4) + ($avgParticipation * 0.4) + ($avgQuiz * 0.2), 1);
|
||||
}
|
||||
|
||||
if (!$hasTests) {
|
||||
if ($avgHomework === null || $avgParticipation === null || $avgProject === null) {
|
||||
return null;
|
||||
}
|
||||
return round(($avgHomework * 0.34) + ($avgParticipation * 0.33) + ($avgProject * 0.33), 1);
|
||||
}
|
||||
|
||||
if ($avgHomework === null || $avgParticipation === null || $avgProject === null || $avgQuiz === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round(
|
||||
($avgHomework * 0.3) + ($avgParticipation * 0.3) + ($avgProject * 0.3) + ($avgQuiz * 0.1),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates semester score using:
|
||||
* - Fall: 0.2 * attendance + 0.2 * ptap + 0.6 * midterm
|
||||
* - Spring: 0.2 * attendance + 0.2 * ptap + 0.6 * final
|
||||
* Returns null when a required component is missing.
|
||||
*/
|
||||
private function calculateSemesterScore(array $scores, string $semester, int $studentId, string $schoolYear, ?int $classSectionId): array
|
||||
{
|
||||
$ptap = $scores['ptap_score'] ?? null;
|
||||
$attendance = $scores['attendance_score'] ?? null;
|
||||
|
||||
$out = [
|
||||
'midterm_score' => null,
|
||||
'final_score' => null,
|
||||
'semester_score' => null,
|
||||
];
|
||||
|
||||
if (strcasecmp($semester, 'Fall') === 0) {
|
||||
$mid = $scores['midterm_exam_score'] ?? (new MidtermExamModel())->getMidtermExamScore($studentId, $semester, $schoolYear, $classSectionId);
|
||||
$out['midterm_score'] = $mid !== null ? round((float) $mid, 1) : null;
|
||||
if ($ptap !== null && $attendance !== null && $out['midterm_score'] !== null) {
|
||||
$out['semester_score'] = round(($attendance * 0.2) + ($ptap * 0.2) + ($out['midterm_score'] * 0.6), 1);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
if (strcasecmp($semester, 'Spring') === 0) {
|
||||
$fin = $scores['final_exam_score'] ?? (new FinalExamModel())->getFinalExamScore($studentId, $semester, $schoolYear, $classSectionId);
|
||||
$out['final_score'] = $fin !== null ? round((float) $fin, 1) : null;
|
||||
if ($ptap !== null && $attendance !== null && $out['final_score'] !== null) {
|
||||
$out['semester_score'] = round(($attendance * 0.2) + ($ptap * 0.2) + ($out['final_score'] * 0.6), 1);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist (upsert) semester scores for a student.
|
||||
* Ensure SemesterScoreModel::$allowedFields includes ALL keys we pass here.
|
||||
*/
|
||||
private function saveStudentScores(
|
||||
int $studentId,
|
||||
string $schoolId,
|
||||
int $classSectionId,
|
||||
?int $updatedBy,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
array $scoreData
|
||||
): void {
|
||||
$base = [
|
||||
'student_id' => $studentId,
|
||||
'school_id' => $schoolId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'updated_at' => utc_now(),
|
||||
];
|
||||
if ($updatedBy !== null) {
|
||||
$base['updated_by'] = $updatedBy;
|
||||
}
|
||||
|
||||
// Merge in the computed score fields (e.g., homework_avg, quiz_avg, project_avg, attendance_score, etc.)
|
||||
$payload = array_merge($base, $scoreData);
|
||||
|
||||
// Upsert using your model (make sure allowedFields has all keys in $payload)
|
||||
$this->semesterScoreModel->upsert($payload);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user