recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
+269
View File
@@ -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 didnt 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);
}
}