merge prod to main

This commit is contained in:
root
2026-05-16 13:44:12 -04:00
parent 663c0cdbda
commit b32fb853f6
901 changed files with 11241 additions and 1340 deletions
+203 -11
View File
@@ -28,6 +28,7 @@ use App\Models\PlacementLevelModel;
use App\Models\PlacementBatchModel;
use App\Models\PlacementScoreModel;
use App\Models\GradingLockModel;
use App\Services\NavbarService;
//use App\Models\ScoreModel;
@@ -277,7 +278,7 @@ class GradingController extends Controller
$semEsc = $this->db->escape($semester);
$yrEsc = $this->db->escape($schoolYear);
$rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear);
$rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear, $semester);
// Preload quiz/homework/project/participation/midterm score counts to distinguish true zeros from empty scores
$quizCounts = [];
@@ -423,7 +424,7 @@ class GradingController extends Controller
}
}
// Reload rows after refresh
$rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear);
$rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear, $semester);
}
// Build structures keyed by BUSINESS section id
@@ -1079,19 +1080,22 @@ class GradingController extends Controller
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
$rows = $this->fetchBelowSixtyRows($schoolYear, $semester);
$canViewGrading = $this->userHasMenuUrl('grading');
return view('grading/below_sixty', [
'rows' => $rows,
'semester' => $semester,
'schoolYear' => $schoolYear,
'schoolYears' => $schoolYears,
'canViewGrading' => $canViewGrading,
]);
}
public function sendBelowSixtyEmail()
public function editBelowSixtyEmail()
{
$studentId = (int)$this->request->getPost('student_id');
$semester = trim((string)$this->request->getPost('semester'));
$schoolYear = trim((string)$this->request->getPost('school_year'));
$studentId = (int)$this->request->getGet('student_id');
$semester = trim((string)$this->request->getGet('semester'));
$schoolYear = trim((string)$this->request->getGet('school_year'));
if ($studentId <= 0 || $semester === '' || $schoolYear === '') {
return redirect()->back()->with('error', 'Missing student or term.');
@@ -1103,6 +1107,65 @@ class GradingController extends Controller
}
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
$subject = $this->buildBelowSixtySubject($studentName, $semester, $schoolYear);
$parentName = $this->fetchBelowSixtyParentName($studentId);
$scores = [
'homework_avg' => $row['homework_avg'] ?? null,
'project_avg' => $row['project_avg'] ?? null,
'participation_score' => $row['participation_score'] ?? null,
'test_avg' => $row['test_avg'] ?? null,
'ptap_score' => $row['ptap_score'] ?? null,
'attendance_score' => $row['attendance_score'] ?? null,
'midterm_exam_score' => $row['midterm_exam_score'] ?? null,
'semester_score' => $row['semester_score'] ?? null,
];
$emailData = [
'title' => $subject,
'parent_name' => $parentName,
'student_name' => $studentName !== '' ? $studentName : 'your student',
'class_section_name' => $row['class_section_name'] ?? '',
'semester' => $semester,
'school_year' => $schoolYear,
'scores' => $scores,
'comment' => $row['comment'] ?? '',
'sent_at' => utc_now(),
];
$html = view('emails/below_sixty_performance', $emailData, ['saveData' => true]);
return view('grading/below_sixty_email_editor', [
'studentId' => $studentId,
'studentName' => $studentName,
'semester' => $semester,
'schoolYear' => $schoolYear,
'subject' => $subject,
'html' => $html,
]);
}
public function sendBelowSixtyEmail()
{
$studentId = (int)$this->request->getPost('student_id');
$semester = trim((string)$this->request->getPost('semester'));
$schoolYear = trim((string)$this->request->getPost('school_year'));
$subjectInput = trim((string)$this->request->getPost('subject'));
$htmlInput = (string)($this->request->getPost('html') ?? '');
if ($studentId <= 0 || $semester === '' || $schoolYear === '') {
return redirect()->back()->with('error', 'Missing student or term.');
}
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
if (empty($row)) {
return redirect()->back()->with('error', 'Student record not found for the selected term.');
}
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
$subject = $subjectInput !== ''
? $subjectInput
: $this->buildBelowSixtySubject($studentName, $semester, $schoolYear);
$scores = [
'homework_avg' => $row['homework_avg'] ?? null,
'project_avg' => $row['project_avg'] ?? null,
@@ -1122,8 +1185,13 @@ class GradingController extends Controller
'school_year' => $schoolYear,
'scores' => $scores,
'comment' => $row['comment'] ?? '',
'subject' => $subject,
];
if (trim($htmlInput) !== '') {
$payload['html'] = $htmlInput;
}
Events::trigger('below60.email', $payload);
return redirect()->back()->with('status', 'Email sent to parent(s).');
@@ -1148,6 +1216,14 @@ class GradingController extends Controller
$flagModel = new CurrentFlagModel();
$semKey = strtolower(trim($semester));
$redirectUrl = base_url('grading/below-60');
$query = http_build_query([
'semester' => $semester,
'school_year' => $schoolYear,
]);
if ($query !== '') {
$redirectUrl .= '?' . $query;
}
$existing = $flagModel
->where('student_id', $studentId)
@@ -1158,11 +1234,14 @@ class GradingController extends Controller
$userId = (int)(session()->get('user_id') ?? 0) ?: null;
$now = utc_now();
$ok = true;
if ($existing) {
$data = [
'flag_state' => $status,
'flag_datetime' => $now,
'semester' => $semester,
'school_year' => $schoolYear,
'updated_at' => $now,
];
if ($status === 'Open') {
@@ -1178,7 +1257,7 @@ class GradingController extends Controller
$data['close_description'] = trim($prev . PHP_EOL . $note);
}
}
$flagModel->update((int)$existing['id'], $data);
$ok = (bool) $flagModel->update((int)$existing['id'], $data);
} else {
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
@@ -1201,10 +1280,21 @@ class GradingController extends Controller
$data['updated_by_closed'] = $userId;
if ($note !== '') $data['close_description'] = $note;
}
$flagModel->insert($data);
$ok = (bool) $flagModel->insert($data);
}
return redirect()->back()->with('status', 'Status updated.');
if (!$ok) {
log_message('error', 'updateBelowSixtyStatus failed', [
'student_id' => $studentId,
'semester' => $semester,
'school_year' => $schoolYear,
'status' => $status,
'errors' => $flagModel->errors(),
]);
return redirect()->to($redirectUrl)->with('error', 'Failed to update status.');
}
return redirect()->to($redirectUrl)->with('status', 'Status updated.');
}
public function scheduleBelowSixty()
@@ -1487,7 +1577,7 @@ class GradingController extends Controller
* @param string $schoolYear Raw school year value for filtering student_class
* @return array
*/
private function buildGradingRows(string $semEsc, string $yrEsc, string $schoolYear): array
private function buildGradingRows(string $semEsc, string $yrEsc, string $schoolYear, string $semesterRaw): array
{
$builder = $this->db->table('student_class sc')
->select([
@@ -1515,6 +1605,7 @@ class GradingController extends Controller
'ss_b.class_section_id AS matched_biz_csid',
'ss_p.class_section_id AS matched_pk_csid'
])
->distinct()
->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left')
->join('students s', 's.id = sc.student_id', 'inner')
->join(
@@ -1729,9 +1820,10 @@ class GradingController extends Controller
}
$statusMap = [];
$noteMap = [];
if (!empty($studentIds)) {
$flagRows = $this->db->table('current_flag')
->select('student_id, flag_state')
->select('student_id, flag_state, open_description, close_description')
->where('flag', 'grade')
->where('school_year', $schoolYear)
->where("LOWER(TRIM(semester))", $semesterKey)
@@ -1742,6 +1834,12 @@ class GradingController extends Controller
$sid = (int)($row['student_id'] ?? 0);
if ($sid <= 0) continue;
$statusMap[$sid] = (string)($row['flag_state'] ?? '');
$openNote = trim((string)($row['open_description'] ?? ''));
$closeNote = trim((string)($row['close_description'] ?? ''));
$noteMap[$sid] = [
'open' => $openNote,
'closed' => $closeNote,
];
}
}
@@ -1750,6 +1848,15 @@ class GradingController extends Controller
$row['comment'] = $commentMap[$sid] ?? '';
$flagState = strtolower(trim((string)($statusMap[$sid] ?? '')));
$row['status'] = ($flagState === 'closed' || $flagState === 'canceled') ? 'Closed' : 'Open';
$noteBag = $noteMap[$sid] ?? ['open' => '', 'closed' => ''];
$rawNote = $row['status'] === 'Closed' ? (string)$noteBag['closed'] : (string)$noteBag['open'];
if ($rawNote !== '') {
$lines = preg_split('/\R/', $rawNote);
$lines = array_values(array_filter(array_map('trim', $lines), static fn($val) => $val !== ''));
$row['note'] = $lines ? end($lines) : '';
} else {
$row['note'] = '';
}
}
unset($row);
@@ -1799,6 +1906,91 @@ class GradingController extends Controller
return $row;
}
private function userHasMenuUrl(string $needle): bool
{
$needle = strtolower(trim($needle));
if ($needle === '') {
return false;
}
$rawRole = session()->get('role');
$roles = is_array($rawRole) ? $rawRole : [$rawRole ?? 'guest'];
$roles = array_values(array_filter(array_map('strval', $roles)));
if (empty($roles)) {
return false;
}
$service = new NavbarService();
$menu = $service->getMenuForRoles($roles);
if (empty($menu)) {
return false;
}
$normalize = static function (string $url) use ($needle): string {
$url = strtolower(trim($url));
if ($url === '') return '';
$url = preg_replace('#^https?://[^/]+/#i', '', $url);
$url = ltrim($url, '/');
return $url;
};
$target = $normalize($needle);
$stack = $menu;
while (!empty($stack)) {
$node = array_shift($stack);
if (!empty($node['url'])) {
$url = $normalize((string)$node['url']);
if ($url !== '' && $url === $target) {
return true;
}
}
if (!empty($node['children']) && is_array($node['children'])) {
foreach ($node['children'] as $child) {
$stack[] = $child;
}
}
}
return false;
}
private function fetchBelowSixtyParentName(int $studentId): string
{
$parentName = 'Parent/Guardian';
try {
$rows = $this->db->query(
"SELECT u.firstname, u.lastname
FROM family_students fs
JOIN family_guardians fg ON fg.family_id = fs.family_id
JOIN users u ON u.id = fg.user_id
WHERE fs.student_id = ?
ORDER BY fg.is_primary DESC, u.lastname, u.firstname
LIMIT 1",
[$studentId]
)->getResultArray();
if (!empty($rows[0])) {
$candidate = trim((string)($rows[0]['firstname'] ?? '') . ' ' . (string)($rows[0]['lastname'] ?? ''));
if ($candidate !== '') {
$parentName = $candidate;
}
}
} catch (\Throwable $e) {
}
return $parentName;
}
private function buildBelowSixtySubject(string $studentName, string $semester, string $schoolYear): string
{
$subject = 'Student Performance Alert';
if ($studentName !== '') {
$subject .= ' — ' . $studentName;
}
if ($semester !== '' || $schoolYear !== '') {
$subject .= ' (' . trim($semester . ' ' . $schoolYear) . ')';
}
return $subject;
}
private function fetchBelowSixtyMeetingContext(int $studentId, string $schoolYear, string $semester): array
{
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);