move service logic from grading and administrator controller
This commit is contained in:
@@ -329,4 +329,167 @@ class Services extends BaseService
|
||||
new \App\Libraries\InvoiceLedgerService()
|
||||
);
|
||||
}
|
||||
|
||||
public static function enrollmentWithdrawal(bool $getShared = true): \App\Services\EnrollmentWithdrawalService
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('enrollmentWithdrawal');
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
return new \App\Services\EnrollmentWithdrawalService(
|
||||
$db,
|
||||
model(\App\Models\StudentModel::class),
|
||||
model(\App\Models\EnrollmentModel::class),
|
||||
model(\App\Models\StudentClassModel::class),
|
||||
model(\App\Models\ClassSectionModel::class),
|
||||
model(\App\Models\UserModel::class),
|
||||
model(\App\Models\InvoiceModel::class),
|
||||
model(\App\Models\RefundModel::class)
|
||||
);
|
||||
}
|
||||
|
||||
public static function teacherSubmissionReport(bool $getShared = true): \App\Services\TeacherSubmissionReportService
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('teacherSubmissionReport');
|
||||
}
|
||||
|
||||
return new \App\Services\TeacherSubmissionReportService(
|
||||
\Config\Database::connect(),
|
||||
model(\App\Models\ConfigurationModel::class),
|
||||
model(\App\Models\StudentClassModel::class),
|
||||
model(\App\Models\ClassSectionModel::class),
|
||||
model(\App\Models\UserModel::class)
|
||||
);
|
||||
}
|
||||
|
||||
public static function administratorDashboard(bool $getShared = true): \App\Services\AdministratorDashboardService
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('administratorDashboard');
|
||||
}
|
||||
|
||||
return new \App\Services\AdministratorDashboardService(
|
||||
\Config\Database::connect(),
|
||||
model(\App\Models\UserModel::class),
|
||||
model(\App\Models\LoginActivityModel::class)
|
||||
);
|
||||
}
|
||||
|
||||
public static function adminNotificationSettings(bool $getShared = true): \App\Services\AdminNotificationSettingsService
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('adminNotificationSettings');
|
||||
}
|
||||
|
||||
return new \App\Services\AdminNotificationSettingsService(
|
||||
\Config\Database::connect(),
|
||||
model(\App\Models\AdminNotificationSubjectModel::class)
|
||||
);
|
||||
}
|
||||
|
||||
public static function administratorDirectory(bool $getShared = true): \App\Services\AdministratorDirectoryService
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('administratorDirectory');
|
||||
}
|
||||
|
||||
return new \App\Services\AdministratorDirectoryService(
|
||||
\Config\Database::connect(),
|
||||
model(\App\Models\StudentClassModel::class),
|
||||
model(\App\Models\UserModel::class),
|
||||
model(\App\Models\UserRoleModel::class),
|
||||
model(\App\Models\InvoiceModel::class),
|
||||
model(\App\Models\StudentModel::class)
|
||||
);
|
||||
}
|
||||
|
||||
public static function gradingScore(bool $getShared = true): \App\Services\GradingScoreService
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('gradingScore');
|
||||
}
|
||||
|
||||
$configModel = model(\App\Models\ConfigurationModel::class);
|
||||
$attendanceCalculator = new \App\Services\Calculators\AttendanceCalculator(
|
||||
model(\App\Models\AttendanceRecordModel::class),
|
||||
$configModel,
|
||||
model(\App\Models\CalendarModel::class)
|
||||
);
|
||||
|
||||
return new \App\Services\GradingScoreService(
|
||||
\Config\Database::connect(),
|
||||
$configModel,
|
||||
model(\App\Models\HomeworkModel::class),
|
||||
model(\App\Models\UserModel::class),
|
||||
model(\App\Models\StudentClassModel::class),
|
||||
model(\App\Models\StudentModel::class),
|
||||
model(\App\Models\TeacherClassModel::class),
|
||||
model(\App\Models\ClassSectionModel::class),
|
||||
$attendanceCalculator,
|
||||
model(\App\Models\GradingLockModel::class),
|
||||
static::semesterScoreService(),
|
||||
(string) $configModel->getConfig('school_year'),
|
||||
(string) getSemester()
|
||||
);
|
||||
}
|
||||
|
||||
public static function placementGrading(bool $getShared = true): \App\Services\PlacementGradingService
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('placementGrading');
|
||||
}
|
||||
|
||||
$configModel = model(\App\Models\ConfigurationModel::class);
|
||||
|
||||
return new \App\Services\PlacementGradingService(
|
||||
\Config\Database::connect(),
|
||||
model(\App\Models\StudentModel::class),
|
||||
model(\App\Models\PlacementLevelModel::class),
|
||||
model(\App\Models\PlacementBatchModel::class),
|
||||
model(\App\Models\PlacementScoreModel::class),
|
||||
(string) $configModel->getConfig('school_year')
|
||||
);
|
||||
}
|
||||
|
||||
public static function belowSixty(bool $getShared = true): \App\Services\BelowSixtyService
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('belowSixty');
|
||||
}
|
||||
|
||||
$configModel = model(\App\Models\ConfigurationModel::class);
|
||||
|
||||
return new \App\Services\BelowSixtyService(
|
||||
\Config\Database::connect(),
|
||||
$configModel,
|
||||
model(\App\Models\StudentModel::class),
|
||||
model(\App\Models\StudentClassModel::class),
|
||||
model(\App\Models\ParentMeetingScheduleModel::class),
|
||||
model(\App\Models\UserModel::class),
|
||||
(string) $configModel->getConfig('school_year'),
|
||||
(string) getSemester()
|
||||
);
|
||||
}
|
||||
|
||||
public static function studentDecision(bool $getShared = true): \App\Services\StudentDecisionService
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('studentDecision');
|
||||
}
|
||||
|
||||
$configModel = model(\App\Models\ConfigurationModel::class);
|
||||
|
||||
return new \App\Services\StudentDecisionService(
|
||||
\Config\Database::connect(),
|
||||
$configModel,
|
||||
model(\App\Models\StudentModel::class),
|
||||
model(\App\Models\StudentClassModel::class),
|
||||
model(\App\Models\UserModel::class),
|
||||
(string) $configModel->getConfig('school_year'),
|
||||
(string) getSemester()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\AdminNotificationSubjectModel;
|
||||
|
||||
class AdminNotificationSettingsService
|
||||
{
|
||||
protected $db;
|
||||
protected $adminNotificationSubjectModel;
|
||||
|
||||
public function __construct(
|
||||
\CodeIgniter\Database\BaseConnection $db,
|
||||
AdminNotificationSubjectModel $adminNotificationSubjectModel
|
||||
) {
|
||||
$this->db = $db;
|
||||
$this->adminNotificationSubjectModel = $adminNotificationSubjectModel;
|
||||
}
|
||||
|
||||
public function alertsPage(): array
|
||||
{
|
||||
$admins = $this->eligibleUsers();
|
||||
$subjects = $this->subjectOptions();
|
||||
$assignedSubjects = [];
|
||||
$tableReady = $this->db->tableExists('admin_notification_subjects');
|
||||
|
||||
if ($tableReady && !empty($admins)) {
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
$rows = $this->adminNotificationSubjectModel
|
||||
->select('id, admin_id, subject')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->findAll();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$adminId = (int) ($row['admin_id'] ?? 0);
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
if ($adminId <= 0 || $subject === '') {
|
||||
continue;
|
||||
}
|
||||
if (!isset($assignedSubjects[$adminId])) {
|
||||
$assignedSubjects[$adminId] = [];
|
||||
}
|
||||
$assignedSubjects[$adminId][$subject] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'admins' => $admins,
|
||||
'subjects' => $subjects,
|
||||
'assignedSubjects' => $assignedSubjects,
|
||||
'tableReady' => $tableReady,
|
||||
];
|
||||
}
|
||||
|
||||
public function saveSubjects($posted): array
|
||||
{
|
||||
if (!$this->db->tableExists('admin_notification_subjects')) {
|
||||
return ['type' => 'error', 'message' => 'Notification subject storage is missing. Run migrations first.'];
|
||||
}
|
||||
|
||||
if (!is_array($posted)) {
|
||||
return ['type' => 'error', 'message' => 'No selections were submitted.'];
|
||||
}
|
||||
|
||||
$subjects = $this->subjectOptions();
|
||||
$allowed = array_keys($subjects);
|
||||
|
||||
$admins = $this->eligibleUsers();
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
if (empty($adminIds)) {
|
||||
return ['type' => 'info', 'message' => 'No admins found to update.'];
|
||||
}
|
||||
|
||||
$existing = $this->adminNotificationSubjectModel
|
||||
->select('id, admin_id, subject')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->findAll();
|
||||
|
||||
$existingMap = [];
|
||||
foreach ($existing as $row) {
|
||||
$adminId = (int) ($row['admin_id'] ?? 0);
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
if ($adminId <= 0 || $subject === '') {
|
||||
continue;
|
||||
}
|
||||
if (!isset($existingMap[$adminId])) {
|
||||
$existingMap[$adminId] = [];
|
||||
}
|
||||
$existingMap[$adminId][$subject] = (int) ($row['id'] ?? 0);
|
||||
}
|
||||
|
||||
$updates = 0;
|
||||
|
||||
foreach ($adminIds as $adminId) {
|
||||
$subjectRaw = $posted[$adminId] ?? [];
|
||||
|
||||
$selected = [];
|
||||
if (is_array($subjectRaw)) {
|
||||
foreach ($subjectRaw as $key => $value) {
|
||||
$candidate = is_string($key) ? $key : $value;
|
||||
if (!is_string($candidate)) {
|
||||
continue;
|
||||
}
|
||||
$candidate = trim($candidate);
|
||||
if ($candidate !== '') {
|
||||
$selected[] = $candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$selected = array_values(array_unique(array_filter($selected, function ($value) use ($allowed) {
|
||||
return in_array($value, $allowed, true);
|
||||
})));
|
||||
|
||||
$current = array_keys($existingMap[$adminId] ?? []);
|
||||
$toDelete = array_values(array_diff($current, $selected));
|
||||
$toInsert = array_values(array_diff($selected, $current));
|
||||
|
||||
if (!empty($toDelete)) {
|
||||
$this->adminNotificationSubjectModel
|
||||
->where('admin_id', $adminId)
|
||||
->whereIn('subject', $toDelete)
|
||||
->delete();
|
||||
$updates += count($toDelete);
|
||||
}
|
||||
|
||||
if (!empty($toInsert)) {
|
||||
$batch = [];
|
||||
foreach ($toInsert as $subject) {
|
||||
$batch[] = [
|
||||
'admin_id' => $adminId,
|
||||
'subject' => $subject,
|
||||
];
|
||||
}
|
||||
$this->adminNotificationSubjectModel->insertBatch($batch);
|
||||
$updates += count($toInsert);
|
||||
}
|
||||
}
|
||||
|
||||
return ['type' => 'success', 'message' => $updates > 0 ? 'Notification subjects updated.' : 'No changes were made.'];
|
||||
}
|
||||
|
||||
public function printRecipientsPage(): array
|
||||
{
|
||||
$admins = $this->eligibleUsers();
|
||||
$tableReady = $this->db->tableExists('admin_notification_subjects');
|
||||
$assigned = [];
|
||||
|
||||
if ($tableReady && !empty($admins)) {
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
$rows = $this->adminNotificationSubjectModel
|
||||
->select('admin_id')
|
||||
->where('subject', 'print_requests')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->findAll();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$adminId = (int) ($row['admin_id'] ?? 0);
|
||||
if ($adminId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$assigned[$adminId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'admins' => $admins,
|
||||
'assigned' => $assigned,
|
||||
'tableReady' => $tableReady,
|
||||
];
|
||||
}
|
||||
|
||||
public function savePrintRecipients(array $posted): array
|
||||
{
|
||||
if (!$this->db->tableExists('admin_notification_subjects')) {
|
||||
return ['type' => 'error', 'message' => 'Notification subject storage is missing. Run migrations first.'];
|
||||
}
|
||||
|
||||
$admins = $this->eligibleUsers();
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
if (empty($adminIds)) {
|
||||
return ['type' => 'info', 'message' => 'No admins found to update.'];
|
||||
}
|
||||
|
||||
$selected = [];
|
||||
foreach ($posted as $key => $value) {
|
||||
$adminId = (int) $key;
|
||||
if ($adminId <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($adminId, $adminIds, true)) {
|
||||
continue;
|
||||
}
|
||||
$selected[] = $adminId;
|
||||
}
|
||||
$selected = array_values(array_unique($selected));
|
||||
|
||||
$existingRows = $this->adminNotificationSubjectModel
|
||||
->select('admin_id')
|
||||
->where('subject', 'print_requests')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->findAll();
|
||||
|
||||
$current = array_values(array_unique(array_map(
|
||||
fn($row) => (int) ($row['admin_id'] ?? 0),
|
||||
$existingRows
|
||||
)));
|
||||
|
||||
$toDelete = array_values(array_diff($current, $selected));
|
||||
$toInsert = array_values(array_diff($selected, $current));
|
||||
|
||||
if (!empty($toDelete)) {
|
||||
$this->adminNotificationSubjectModel
|
||||
->where('subject', 'print_requests')
|
||||
->whereIn('admin_id', $toDelete)
|
||||
->delete();
|
||||
}
|
||||
|
||||
if (!empty($toInsert)) {
|
||||
$batch = [];
|
||||
foreach ($toInsert as $adminId) {
|
||||
$batch[] = [
|
||||
'admin_id' => $adminId,
|
||||
'subject' => 'print_requests',
|
||||
];
|
||||
}
|
||||
$this->adminNotificationSubjectModel->insertBatch($batch);
|
||||
}
|
||||
|
||||
$changes = count($toDelete) + count($toInsert);
|
||||
return ['type' => 'success', 'message' => $changes > 0 ? 'Print notification recipients updated.' : 'No changes were made.'];
|
||||
}
|
||||
|
||||
public function subjectOptions(): array
|
||||
{
|
||||
return [
|
||||
'academics' => 'Academics',
|
||||
'attendance' => 'Attendance',
|
||||
'events' => 'Events',
|
||||
'finance' => 'Finance',
|
||||
'general' => 'General',
|
||||
'print_requests' => 'Print Requests',
|
||||
];
|
||||
}
|
||||
|
||||
public function excludedRoles(): array
|
||||
{
|
||||
return [
|
||||
'parent',
|
||||
'student',
|
||||
'guest',
|
||||
'teacher',
|
||||
'assistant teacher',
|
||||
'teacher assistant',
|
||||
'teacher_assistant',
|
||||
'assistant_teacher',
|
||||
'ta',
|
||||
'authorized_user',
|
||||
];
|
||||
}
|
||||
|
||||
public function eligibleUsers(): array
|
||||
{
|
||||
$excluded = $this->excludedRoles();
|
||||
$excludedList = "'" . implode("','", $excluded) . "'";
|
||||
|
||||
return $this->db->table('users u')
|
||||
->select('u.id, u.firstname, u.lastname, u.email')
|
||||
->join('user_roles ur', 'ur.user_id = u.id', 'inner')
|
||||
->join('roles r', 'r.id = ur.role_id', 'inner')
|
||||
->where('r.name IS NOT NULL', null, false)
|
||||
->where('ur.deleted_at', null)
|
||||
->where("LOWER(r.name) NOT IN ({$excludedList})", null, false)
|
||||
->groupBy('u.id, u.firstname, u.lastname, u.email')
|
||||
->orderBy('u.lastname', 'ASC')
|
||||
->orderBy('u.firstname', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\LoginActivityModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
class AdministratorDashboardService
|
||||
{
|
||||
protected $db;
|
||||
protected $userModel;
|
||||
protected $loginActivityModel;
|
||||
protected string $schoolYear = '';
|
||||
protected string $semester = '';
|
||||
|
||||
public function __construct(
|
||||
\CodeIgniter\Database\BaseConnection $db,
|
||||
UserModel $userModel,
|
||||
LoginActivityModel $loginActivityModel
|
||||
) {
|
||||
$this->db = $db;
|
||||
$this->userModel = $userModel;
|
||||
$this->loginActivityModel = $loginActivityModel;
|
||||
}
|
||||
|
||||
public function metrics(string $schoolYear, string $semester): array
|
||||
{
|
||||
$this->schoolYear = $schoolYear;
|
||||
$this->semester = $semester;
|
||||
$recentActivities = $this->loginActivityModel->getLastActivities(4);
|
||||
if (!is_array($recentActivities)) {
|
||||
$recentActivities = [];
|
||||
}
|
||||
|
||||
$totalAdmins = (int) ($this->userModel->countAdminsBySchoolYear($this->schoolYear) ?? 0);
|
||||
|
||||
$teachers = $this->userModel->getUsersByRoleAndSchoolYear('teacher', $this->schoolYear);
|
||||
$totalTeachers = $this->countUniqueEntities($teachers);
|
||||
|
||||
$teacherAssistants = $this->userModel->getUsersByRoleAndSchoolYear('teacher_assistant', $this->schoolYear);
|
||||
$totalTeacherAssistants = $this->countUniqueEntities($teacherAssistants);
|
||||
|
||||
$parents = $this->userModel->getUsersByRoleAndSchoolYear('parent', $this->schoolYear);
|
||||
$totalParents = $this->countUniqueEntities($parents);
|
||||
|
||||
// Count only students that have a class assigned and exist in student_class for the current school year
|
||||
$totalStudents = (int) (
|
||||
$this->db->table('student_class')
|
||||
->select('COUNT(DISTINCT student_class.student_id) AS cnt')
|
||||
->join('students', 'students.id = student_class.student_id', 'inner')
|
||||
->where('student_class.school_year', $this->schoolYear)
|
||||
->where('student_class.class_section_id IS NOT NULL', null, false)
|
||||
->where('students.is_active', 1)
|
||||
->get()
|
||||
->getRow('cnt')
|
||||
?? 0
|
||||
);
|
||||
|
||||
return [
|
||||
'counts' => [
|
||||
'students' => $totalStudents,
|
||||
'teachers' => $totalTeachers,
|
||||
'teacherAssistants' => $totalTeacherAssistants,
|
||||
'admins' => $totalAdmins,
|
||||
'parents' => $totalParents,
|
||||
],
|
||||
'recentActivities' => array_map(static function ($activity) {
|
||||
if (!is_array($activity)) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'login_time' => $activity['login_time'] ?? null,
|
||||
'email' => $activity['email'] ?? null,
|
||||
];
|
||||
}, $recentActivities),
|
||||
'meta' => [
|
||||
'schoolYear' => $this->schoolYear,
|
||||
'semester' => $this->semester,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function countUniqueEntities($rows): int
|
||||
{
|
||||
if (!is_array($rows) || $rows === []) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($row['id'])) {
|
||||
$ids[] = (int) $row['id'];
|
||||
continue;
|
||||
}
|
||||
if (isset($row['user_id'])) {
|
||||
$ids[] = (int) $row['user_id'];
|
||||
}
|
||||
}
|
||||
|
||||
return count(array_unique($ids));
|
||||
}
|
||||
|
||||
public function search(string $query): array
|
||||
{
|
||||
$q = trim($query);
|
||||
|
||||
if ($q === '') {
|
||||
return [
|
||||
'query' => '',
|
||||
'results' => [],
|
||||
'scope_used' => 'unscoped-raw',
|
||||
'scope_label' => 'all years/semesters (raw)',
|
||||
'total_found' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$db = $this->db;
|
||||
|
||||
// 1) Tokenize input: split by whitespace and punctuation, keep meaningful pieces
|
||||
$rawTokens = preg_split('/[,\s]+/u', $q, -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
$tokens = array_values(array_filter(array_map('trim', $rawTokens)));
|
||||
|
||||
// 2) Build phone variants for any token that looks numeric-ish
|
||||
$phoneMap = []; // token => variants[]
|
||||
foreach ($tokens as $t) {
|
||||
$digits = preg_replace('/\D+/', '', $t);
|
||||
if ($digits === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$v = [];
|
||||
if (strlen($digits) >= 7) {
|
||||
// base forms
|
||||
$v[] = $digits;
|
||||
if (strlen($digits) === 10) {
|
||||
$v[] = sprintf('(%s)-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||||
$v[] = sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||||
$v[] = sprintf('%s %s %s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||||
// country-code forms
|
||||
$v[] = '1' . $digits;
|
||||
$v[] = '+1' . $digits;
|
||||
$v[] = '+1 ' . sprintf('(%s) %s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||||
$v[] = '+1-' . sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||||
} elseif (strlen($digits) === 11 && str_starts_with($digits, '1')) {
|
||||
$ten = substr($digits, 1);
|
||||
$v[] = $ten;
|
||||
$v[] = sprintf('(%s)-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||||
$v[] = sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||||
$v[] = sprintf('%s %s %s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||||
$v[] = '+1' . $ten;
|
||||
$v[] = '+1 ' . sprintf('(%s) %s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||||
$v[] = '+1-' . sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||||
} else {
|
||||
// 7-9 digits: keep as-is (partial phone fragment)
|
||||
$v[] = $digits;
|
||||
}
|
||||
}
|
||||
if (!empty($v)) {
|
||||
$phoneMap[$t] = array_values(array_unique($v));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: ( token1 AND token2 AND ... ), each token may match ANY of $columns,
|
||||
// and phone variants are only applied to the provided $phoneCols for THIS table.
|
||||
$applyMultiTokenLike = function ($qb, array $columns, array $tokens, array $phoneCols = []) use ($phoneMap) {
|
||||
foreach ($tokens as $t) {
|
||||
$qb->groupStart(); // OR across columns for this token
|
||||
foreach ($columns as $i => $col) {
|
||||
if ($i === 0) {
|
||||
$qb->like($col, $t);
|
||||
} else {
|
||||
$qb->orLike($col, $t);
|
||||
}
|
||||
}
|
||||
if (!empty($phoneMap[$t]) && !empty($phoneCols)) {
|
||||
foreach ($phoneMap[$t] as $pv) {
|
||||
foreach ($phoneCols as $pcol) {
|
||||
$qb->orLike($pcol, $pv);
|
||||
}
|
||||
}
|
||||
}
|
||||
$qb->groupEnd();
|
||||
}
|
||||
return $qb;
|
||||
};
|
||||
|
||||
// ===== RAW UNscoped searches (flat arrays) =====
|
||||
|
||||
// USERS (phone col: cellphone)
|
||||
$uCols = ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state'];
|
||||
$uQB = $db->table('users')
|
||||
->select('id, firstname, lastname, email, cellphone, school_id, city, state');
|
||||
$applyMultiTokenLike($uQB, $uCols, $tokens, ['cellphone']);
|
||||
$users = $uQB->limit(150)->get()->getResultArray();
|
||||
|
||||
// STUDENTS (no phone column to search)
|
||||
$sCols = ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender'];
|
||||
$sQB = $db->table('students')
|
||||
->select('id, parent_id, school_id, firstname, lastname, dob, gender, rfid_tag');
|
||||
$applyMultiTokenLike($sQB, $sCols, $tokens, []);
|
||||
$students = $sQB->limit(150)->get()->getResultArray();
|
||||
|
||||
// PARENTS (phone col: secondparent_phone)
|
||||
$pCols = ['secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone'];
|
||||
$pQB = $db->table('parents')
|
||||
->select('id, firstparent_id, secondparent_firstname, secondparent_lastname, secondparent_email, secondparent_phone');
|
||||
$applyMultiTokenLike($pQB, $pCols, $tokens, ['secondparent_phone']);
|
||||
foreach ($tokens as $t) {
|
||||
if (ctype_digit($t)) {
|
||||
$pQB->orWhere('firstparent_id', (int) $t)->orWhere('id', (int) $t);
|
||||
}
|
||||
}
|
||||
$parents = $pQB->limit(150)->get()->getResultArray();
|
||||
|
||||
// STAFF (phone col: phone)
|
||||
$stCols = ['firstname', 'lastname', 'email', 'role_name', 'phone'];
|
||||
$stQB = $db->table('staff')
|
||||
->select('id, user_id, firstname, lastname, email, phone, role_name, active_role');
|
||||
$applyMultiTokenLike($stQB, $stCols, $tokens, ['phone']);
|
||||
$staff = $stQB->limit(150)->get()->getResultArray();
|
||||
|
||||
// EMERGENCY CONTACTS (phone col: cellphone)
|
||||
$ecCols = ['emergency_contact_name', 'relation', 'email', 'cellphone'];
|
||||
$ecQB = $db->table('emergency_contacts')
|
||||
->select('id, parent_id, emergency_contact_name, relation, cellphone, email');
|
||||
$applyMultiTokenLike($ecQB, $ecCols, $tokens, ['cellphone']);
|
||||
$emergency = $ecQB->limit(150)->get()->getResultArray();
|
||||
|
||||
$raw = [
|
||||
'users' => $users,
|
||||
'students' => $students,
|
||||
'parents' => $parents,
|
||||
'staff' => $staff,
|
||||
'emergency_contacts' => $emergency,
|
||||
];
|
||||
|
||||
$total = count($users) + count($students) + count($parents) + count($staff) + count($emergency);
|
||||
|
||||
return [
|
||||
'query' => $q,
|
||||
'results' => $raw,
|
||||
'scope_used' => 'unscoped-raw',
|
||||
'scope_label' => 'all years/semesters (raw, tokenized)',
|
||||
'total_found' => $total,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\InvoiceModel;
|
||||
use App\Models\StudentClassModel;
|
||||
use App\Models\StudentModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\UserRoleModel;
|
||||
use App\Support\Enrollment\EnrollmentEligibility;
|
||||
|
||||
class AdministratorDirectoryService
|
||||
{
|
||||
protected $db;
|
||||
protected $studentClassModel;
|
||||
protected $userModel;
|
||||
protected $userRoleModel;
|
||||
protected $invoiceModel;
|
||||
protected $studentModel;
|
||||
|
||||
public function __construct(
|
||||
\CodeIgniter\Database\BaseConnection $db,
|
||||
StudentClassModel $studentClassModel,
|
||||
UserModel $userModel,
|
||||
UserRoleModel $userRoleModel,
|
||||
InvoiceModel $invoiceModel,
|
||||
StudentModel $studentModel
|
||||
) {
|
||||
$this->db = $db;
|
||||
$this->studentClassModel = $studentClassModel;
|
||||
$this->userModel = $userModel;
|
||||
$this->userRoleModel = $userRoleModel;
|
||||
$this->invoiceModel = $invoiceModel;
|
||||
$this->studentModel = $studentModel;
|
||||
}
|
||||
|
||||
public function studentProfiles(string $selectedYear): array
|
||||
{
|
||||
$db = db_connect();
|
||||
$isPg = ($db->getPlatform() === 'Postgre'); // 'MySQLi', 'Postgre', 'SQLSRV', ...
|
||||
|
||||
// In MySQL, avoid truncation of long lists
|
||||
if (!$isPg) {
|
||||
$db->query('SET SESSION group_concat_max_len = 8192');
|
||||
}
|
||||
|
||||
$b = $db->table('students');
|
||||
|
||||
if ($isPg) {
|
||||
// Postgres: use subqueries for aggregates to avoid GROUP BY on students.*
|
||||
$students = $b->select([
|
||||
'students.*',
|
||||
'users.firstname AS parent_firstname',
|
||||
'users.lastname AS parent_lastname',
|
||||
'users.email AS parent_email',
|
||||
'users.cellphone AS parent_phone',
|
||||
|
||||
// Pick one emergency contact (first by id)
|
||||
"(SELECT ec.emergency_contact_name FROM emergency_contacts ec
|
||||
WHERE ec.parent_id = students.parent_id
|
||||
ORDER BY ec.id ASC LIMIT 1) AS emergency_name",
|
||||
"(SELECT ec.relation FROM emergency_contacts ec
|
||||
WHERE ec.parent_id = students.parent_id
|
||||
ORDER BY ec.id ASC LIMIT 1) AS emergency_relationship",
|
||||
"(SELECT ec.cellphone FROM emergency_contacts ec
|
||||
WHERE ec.parent_id = students.parent_id
|
||||
ORDER BY ec.id ASC LIMIT 1) AS emergency_phone",
|
||||
"(SELECT ec.email FROM emergency_contacts ec
|
||||
WHERE ec.parent_id = students.parent_id
|
||||
ORDER BY ec.id ASC LIMIT 1) AS emergency_email",
|
||||
|
||||
// Aggregated lists
|
||||
"(SELECT STRING_AGG(DISTINCT sa.allergy, ', ' ORDER BY sa.allergy)
|
||||
FROM student_allergies sa WHERE sa.student_id = students.id) AS allergies",
|
||||
"(SELECT STRING_AGG(DISTINCT smc.condition_name, ', ' ORDER BY smc.condition_name)
|
||||
FROM student_medical_conditions smc WHERE smc.student_id = students.id) AS medical_conditions",
|
||||
])
|
||||
->join('users', 'users.id = students.parent_id', 'left')
|
||||
->orderBy('students.lastname', 'ASC')
|
||||
->orderBy('students.firstname', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
} else {
|
||||
// MySQL: GROUP_CONCAT + MIN() for emergency_* to satisfy ONLY_FULL_GROUP_BY
|
||||
$students = $b->select([
|
||||
'students.*',
|
||||
'users.firstname AS parent_firstname',
|
||||
'users.lastname AS parent_lastname',
|
||||
'users.email AS parent_email',
|
||||
'users.cellphone AS parent_phone',
|
||||
|
||||
'MIN(emergency_contacts.emergency_contact_name) AS emergency_name',
|
||||
'MIN(emergency_contacts.relation) AS emergency_relationship',
|
||||
'MIN(emergency_contacts.cellphone) AS emergency_phone',
|
||||
'MIN(emergency_contacts.email) AS emergency_email',
|
||||
|
||||
"GROUP_CONCAT(DISTINCT student_allergies.allergy
|
||||
ORDER BY student_allergies.allergy SEPARATOR ', ') AS allergies",
|
||||
"GROUP_CONCAT(DISTINCT student_medical_conditions.condition_name
|
||||
ORDER BY student_medical_conditions.condition_name SEPARATOR ', ') AS medical_conditions",
|
||||
])
|
||||
->join('users', 'users.id = students.parent_id', 'left')
|
||||
->join('emergency_contacts', 'emergency_contacts.parent_id = students.parent_id', 'left')
|
||||
->join('student_allergies', 'student_allergies.student_id = students.id', 'left')
|
||||
->join('student_medical_conditions', 'student_medical_conditions.student_id = students.id', 'left')
|
||||
->groupBy('students.id')
|
||||
->orderBy('students.lastname', 'ASC')
|
||||
->orderBy('students.firstname', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
$enrollmentStatusByStudentId = [];
|
||||
$studentIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn (array $row): int => (int) ($row['id'] ?? 0),
|
||||
$students
|
||||
))));
|
||||
if ($selectedYear !== '' && !empty($studentIds)) {
|
||||
$enrollmentRows = $db->table('enrollments')
|
||||
->select('student_id, enrollment_status')
|
||||
->whereIn('student_id', $studentIds)
|
||||
->where('school_year', $selectedYear)
|
||||
->orderBy('student_id', 'ASC')
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->orderBy('enrollment_date', 'DESC')
|
||||
->orderBy('id', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($enrollmentRows as $enrollmentRow) {
|
||||
$studentId = (int) ($enrollmentRow['student_id'] ?? 0);
|
||||
if ($studentId > 0 && !isset($enrollmentStatusByStudentId[$studentId])) {
|
||||
$enrollmentStatusByStudentId[$studentId] = (string) ($enrollmentRow['enrollment_status'] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Inject current-year class_section_name from student_class and replace grade ===
|
||||
foreach ($students as $i => $row) {
|
||||
$sid = (int) ($row['id'] ?? 0);
|
||||
if ($sid > 0) {
|
||||
$classSectionName = (string) ($this->studentClassModel->getClassSectionNameByStudentId($sid, $selectedYear) ?? '');
|
||||
|
||||
$students[$i]['class_section_name'] = $classSectionName;
|
||||
$students[$i]['enrollment_status'] = $enrollmentStatusByStudentId[$sid] ?? '';
|
||||
} else {
|
||||
// Keep keys consistent even if id missing
|
||||
$students[$i]['class_section_name'] = '';
|
||||
$students[$i]['enrollment_status'] = '';
|
||||
}
|
||||
|
||||
$studentYear = trim((string) ($row['school_year'] ?? ''));
|
||||
$students[$i]['age'] = EnrollmentEligibility::ageOnSeptemberFirst(
|
||||
$row['dob'] ?? null,
|
||||
$studentYear !== '' ? $studentYear : $selectedYear
|
||||
);
|
||||
}
|
||||
// === end injection ===
|
||||
|
||||
return [
|
||||
'students' => $students,
|
||||
'gradeOptions' => ['K', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Youth'],
|
||||
'genderOptions' => ['Male', 'Female', 'Other'],
|
||||
'selectedYear' => $selectedYear,
|
||||
];
|
||||
}
|
||||
|
||||
public function parentProfiles(): array
|
||||
{
|
||||
$allUsers = $this->userModel->findAll();
|
||||
$parents = [];
|
||||
|
||||
// Fetch roles for users in one query to avoid looping over all users
|
||||
foreach ($allUsers as $user) {
|
||||
// Get the roles of the user directly
|
||||
$roles = $this->userRoleModel->getRolesByUserId($user['id']);
|
||||
$isParent = false;
|
||||
|
||||
// Check if $roles is iterable and contains the 'parent' role
|
||||
if (is_array($roles)) {
|
||||
foreach ($roles as $role) {
|
||||
if (isset($role['role_name']) && $role['role_name'] === 'parent') {
|
||||
$isParent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isParent) {
|
||||
// Retrieve the latest paid amount and balance for this parent
|
||||
$paidAmount = $this->invoiceModel->getLatestInvoicePaidAmount($user['id']) ?? 0;
|
||||
$balance = $this->invoiceModel->getLatestInvoiceBalance($user['id']) ?? 0;
|
||||
|
||||
// Get students for this parent
|
||||
$studentsData = $this->studentModel->where('parent_id', $user['id'])->findAll();
|
||||
$students = [];
|
||||
foreach ($studentsData as $student) {
|
||||
$classSectionName = $this->studentClassModel->getClassSectionNameByStudentId($student['id']) ?? 'N/A';
|
||||
$students[] = [
|
||||
'name' => $student['firstname'] . ' ' . $student['lastname'],
|
||||
'class_section' => $classSectionName
|
||||
];
|
||||
}
|
||||
|
||||
// Add parent data to the array
|
||||
$parents[] = [
|
||||
'id' => $user['id'],
|
||||
'school_id' => $user['school_id'],
|
||||
'firstname' => $user['firstname'],
|
||||
'lastname' => $user['lastname'],
|
||||
'email' => $user['email'],
|
||||
'cellphone' => $user['cellphone'],
|
||||
'gender' => $user['gender'],
|
||||
'created_at' => $user['created_at'],
|
||||
'paid_amount' => $paidAmount,
|
||||
'balance' => $balance,
|
||||
'students' => $students,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ['parents' => $parents];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,437 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\PlacementBatchModel;
|
||||
use App\Models\PlacementLevelModel;
|
||||
use App\Models\PlacementScoreModel;
|
||||
use App\Models\StudentModel;
|
||||
|
||||
class PlacementGradingService
|
||||
{
|
||||
protected $db;
|
||||
protected $studentModel;
|
||||
protected $placementLevelModel;
|
||||
protected $placementBatchModel;
|
||||
protected $placementScoreModel;
|
||||
protected string $schoolYear = '';
|
||||
|
||||
public function __construct(
|
||||
\CodeIgniter\Database\BaseConnection $db,
|
||||
StudentModel $studentModel,
|
||||
PlacementLevelModel $placementLevelModel,
|
||||
PlacementBatchModel $placementBatchModel,
|
||||
PlacementScoreModel $placementScoreModel,
|
||||
string $schoolYear = ''
|
||||
) {
|
||||
$this->db = $db;
|
||||
$this->studentModel = $studentModel;
|
||||
$this->placementLevelModel = $placementLevelModel;
|
||||
$this->placementBatchModel = $placementBatchModel;
|
||||
$this->placementScoreModel = $placementScoreModel;
|
||||
$this->schoolYear = $schoolYear;
|
||||
}
|
||||
|
||||
public function setSchoolYear(string $schoolYear): void
|
||||
{
|
||||
$this->schoolYear = $schoolYear;
|
||||
}
|
||||
|
||||
public function updatePlacementLevel(array $params = [])
|
||||
{
|
||||
$get = $params['get'] ?? [];
|
||||
$post = $params['post'] ?? [];
|
||||
|
||||
$studentId = (int) (($post['student_id'] ?? null) ?? 0);
|
||||
$levelRaw = trim((string) (($post['placement_level'] ?? null) ?? ''));
|
||||
$schoolYear = (string) (($post['school_year'] ?? null) ?? $this->schoolYear);
|
||||
|
||||
if ($studentId <= 0 || $schoolYear === '') {
|
||||
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Missing student or school year.'];
|
||||
}
|
||||
|
||||
$level = $levelRaw === '' ? null : (int) $levelRaw;
|
||||
if ($level !== null && !in_array($level, [1, 2, 3], true)) {
|
||||
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Invalid placement level.'];
|
||||
}
|
||||
|
||||
$existing = $this->placementLevelModel
|
||||
->where('student_id', $studentId)
|
||||
->first();
|
||||
|
||||
if ($level === null) {
|
||||
if ($existing) {
|
||||
$this->placementLevelModel->delete($existing['id']);
|
||||
}
|
||||
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => 'Placement level cleared.'];
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'student_id' => $studentId,
|
||||
'level' => $level,
|
||||
'updated_by' => session()->get('user_id'),
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
$this->placementLevelModel->update($existing['id'], $payload);
|
||||
} else {
|
||||
$payload['created_by'] = session()->get('user_id');
|
||||
$this->placementLevelModel->insert($payload);
|
||||
}
|
||||
|
||||
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => 'Placement level updated.'];
|
||||
}
|
||||
|
||||
public function placementPage(array $params = [])
|
||||
{
|
||||
$get = $params['get'] ?? [];
|
||||
$post = $params['post'] ?? [];
|
||||
|
||||
$classSectionId = (int) (($get['class_section_id'] ?? null) ?? 0);
|
||||
$schoolYear = (string) (($get['school_year'] ?? null) ?? $this->schoolYear);
|
||||
$placementTest = (string) (($get['placement_test'] ?? null) ?? '');
|
||||
$openFlag = (string) (($get['open'] ?? null) ?? '');
|
||||
|
||||
if ($classSectionId <= 0 || $schoolYear === '') {
|
||||
$showStudents = ($placementTest !== '' && $openFlag === '1');
|
||||
$students = $showStudents ? $this->fetchActiveStudentsWithSection($schoolYear) : [];
|
||||
$batches = $this->fetchPlacementBatches($schoolYear);
|
||||
$batchDetails = $this->fetchPlacementBatchDetails($batches, $schoolYear);
|
||||
return ['kind' => 'view', 'view' => 'grading/placement_index', 'data' => [
|
||||
'schoolYear' => $schoolYear,
|
||||
'students' => $students,
|
||||
'batches' => $batches,
|
||||
'batchDetails' => $batchDetails,
|
||||
'placementTest' => $placementTest,
|
||||
'showStudents' => $showStudents,
|
||||
]];
|
||||
}
|
||||
|
||||
$sectionName = $this->classSection->getClassSectionNameBySectionId($classSectionId) ?? '';
|
||||
$classId = $this->classSection->getClassId($classSectionId);
|
||||
|
||||
$students = $this->studentModel->getStudentInfoByClassSectionId($classSectionId, null, $schoolYear);
|
||||
$studentIds = array_values(array_filter(array_map(
|
||||
static fn($row) => (int) ($row['student_id'] ?? 0),
|
||||
$students
|
||||
), static fn($id) => $id > 0));
|
||||
|
||||
$levels = [];
|
||||
if (!empty($studentIds)) {
|
||||
$rows = $this->placementLevelModel
|
||||
->whereIn('student_id', $studentIds)
|
||||
->findAll();
|
||||
foreach ($rows as $row) {
|
||||
$levels[(int) $row['student_id']] = $row['level'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return ['kind' => 'view', 'view' => 'grading/placement', 'data' => [
|
||||
'classSectionId' => $classSectionId,
|
||||
'classSectionName' => $sectionName,
|
||||
'classId' => $classId,
|
||||
'schoolYear' => $schoolYear,
|
||||
'students' => $students,
|
||||
'levels' => $levels,
|
||||
]];
|
||||
}
|
||||
|
||||
public function updatePlacementLevels(array $params = [])
|
||||
{
|
||||
$get = $params['get'] ?? [];
|
||||
$post = $params['post'] ?? [];
|
||||
|
||||
$classSectionId = (int) (($post['class_section_id'] ?? null) ?? 0);
|
||||
$schoolYear = (string) (($post['school_year'] ?? null) ?? $this->schoolYear);
|
||||
$levels = ($post['placement_level'] ?? null) ?? [];
|
||||
|
||||
if ($classSectionId <= 0 || $schoolYear === '') {
|
||||
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Missing class section or school year.'];
|
||||
}
|
||||
|
||||
$students = $this->studentModel->getStudentInfoByClassSectionId($classSectionId, null, $schoolYear);
|
||||
$validIds = array_values(array_filter(array_map(
|
||||
static fn($row) => (int) ($row['student_id'] ?? 0),
|
||||
$students
|
||||
), static fn($id) => $id > 0));
|
||||
|
||||
$validSet = array_flip($validIds);
|
||||
$existingRows = [];
|
||||
if (!empty($validIds)) {
|
||||
$rows = $this->placementLevelModel
|
||||
->whereIn('student_id', $validIds)
|
||||
->findAll();
|
||||
foreach ($rows as $row) {
|
||||
$existingRows[(int) $row['student_id']] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$userId = session()->get('user_id');
|
||||
foreach ($levels as $studentIdRaw => $levelRaw) {
|
||||
$studentId = (int) $studentIdRaw;
|
||||
if (!isset($validSet[$studentId])) {
|
||||
continue;
|
||||
}
|
||||
$levelRaw = trim((string) $levelRaw);
|
||||
$level = $levelRaw === '' ? null : (int) $levelRaw;
|
||||
if ($level !== null && !in_array($level, [1, 2, 3], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($level === null) {
|
||||
if (isset($existingRows[$studentId])) {
|
||||
$this->placementLevelModel->delete($existingRows[$studentId]['id']);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'student_id' => $studentId,
|
||||
'level' => $level,
|
||||
'updated_by' => $userId,
|
||||
];
|
||||
|
||||
if (isset($existingRows[$studentId])) {
|
||||
$this->placementLevelModel->update($existingRows[$studentId]['id'], $payload);
|
||||
} else {
|
||||
$payload['created_by'] = $userId;
|
||||
$this->placementLevelModel->insert($payload);
|
||||
}
|
||||
}
|
||||
|
||||
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'status', 'message' => 'Placement levels updated.'];
|
||||
}
|
||||
|
||||
public function updatePlacementLevelsAll(array $params = [])
|
||||
{
|
||||
$get = $params['get'] ?? [];
|
||||
$post = $params['post'] ?? [];
|
||||
|
||||
$schoolYear = (string) (($post['school_year'] ?? null) ?? $this->schoolYear);
|
||||
$placementTest = (string) (($post['placement_test'] ?? null) ?? '');
|
||||
$levels = ($post['placement_level'] ?? null) ?? [];
|
||||
|
||||
if ($schoolYear === '' || $placementTest === '') {
|
||||
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Missing placement test or school year.'];
|
||||
}
|
||||
|
||||
$students = $this->fetchActiveStudentsWithSection($schoolYear);
|
||||
$validIds = array_values(array_filter(array_map(
|
||||
static fn($row) => (int) ($row['student_id'] ?? 0),
|
||||
$students
|
||||
), static fn($id) => $id > 0));
|
||||
|
||||
$validSet = array_flip($validIds);
|
||||
$userId = session()->get('user_id');
|
||||
|
||||
$batchId = $this->placementBatchModel->insert([
|
||||
'placement_test' => $placementTest,
|
||||
'school_year' => $schoolYear,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
if (!$batchId) {
|
||||
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'Unable to create placement batch.'];
|
||||
}
|
||||
|
||||
$savedCount = 0;
|
||||
foreach ($levels as $studentIdRaw => $levelRaw) {
|
||||
$studentId = (int) $studentIdRaw;
|
||||
if (!isset($validSet[$studentId])) {
|
||||
continue;
|
||||
}
|
||||
$levelRaw = trim((string) $levelRaw);
|
||||
if ($levelRaw === '') {
|
||||
continue;
|
||||
}
|
||||
$level = (int) $levelRaw;
|
||||
if ($level < 0 || $level > 100) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->placementScoreModel->insert([
|
||||
'batch_id' => (int) $batchId,
|
||||
'student_id' => $studentId,
|
||||
'score' => $level,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
$savedCount++;
|
||||
}
|
||||
|
||||
if ($savedCount === 0) {
|
||||
$this->placementBatchModel->delete((int) $batchId);
|
||||
return ['kind' => 'flash', 'redirect' => 'back', 'type' => 'error', 'message' => 'No scores entered. Batch not saved.'];
|
||||
}
|
||||
|
||||
return ['kind' => 'flash', 'redirect' => base_url('grading/placement'), 'type' => 'status', 'message' => 'Placement batch saved.'];
|
||||
}
|
||||
|
||||
public function editPlacementBatch(int $batchId, array $params = [])
|
||||
{
|
||||
$get = $params['get'] ?? [];
|
||||
$post = $params['post'] ?? [];
|
||||
|
||||
$batch = $this->placementBatchModel->find($batchId);
|
||||
if (!$batch) {
|
||||
return ['kind' => 'flash', 'redirect' => base_url('grading/placement'), 'type' => 'error', 'message' => 'Placement batch not found.'];
|
||||
}
|
||||
|
||||
$schoolYear = (string) ($batch['school_year'] ?? $this->schoolYear);
|
||||
$students = $this->fetchActiveStudentsWithSection($schoolYear);
|
||||
$scores = $this->fetchPlacementScoresForBatch($batchId);
|
||||
|
||||
return ['kind' => 'view', 'view' => 'grading/placement_batch', 'data' => [
|
||||
'batch' => $batch,
|
||||
'students' => $students,
|
||||
'scores' => $scores,
|
||||
]];
|
||||
}
|
||||
|
||||
public function updatePlacementBatch(int $batchId, array $params = [])
|
||||
{
|
||||
$get = $params['get'] ?? [];
|
||||
$post = $params['post'] ?? [];
|
||||
|
||||
$batch = $this->placementBatchModel->find($batchId);
|
||||
if (!$batch) {
|
||||
return ['kind' => 'flash', 'redirect' => base_url('grading/placement'), 'type' => 'error', 'message' => 'Placement batch not found.'];
|
||||
}
|
||||
|
||||
$schoolYear = (string) ($batch['school_year'] ?? $this->schoolYear);
|
||||
$levels = ($post['placement_level'] ?? null) ?? [];
|
||||
|
||||
$students = $this->fetchActiveStudentsWithSection($schoolYear);
|
||||
$validIds = array_values(array_filter(array_map(
|
||||
static fn($row) => (int) ($row['student_id'] ?? 0),
|
||||
$students
|
||||
), static fn($id) => $id > 0));
|
||||
|
||||
$validSet = array_flip($validIds);
|
||||
$existing = $this->fetchPlacementScoresForBatch($batchId);
|
||||
|
||||
$userId = session()->get('user_id');
|
||||
foreach ($levels as $studentIdRaw => $scoreRaw) {
|
||||
$studentId = (int) $studentIdRaw;
|
||||
if (!isset($validSet[$studentId])) {
|
||||
continue;
|
||||
}
|
||||
$scoreRaw = trim((string) $scoreRaw);
|
||||
if ($scoreRaw === '') {
|
||||
if (isset($existing[$studentId])) {
|
||||
$this->placementScoreModel->delete($existing[$studentId]['id']);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$score = (int) $scoreRaw;
|
||||
if ($score < 0 || $score > 100) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($existing[$studentId])) {
|
||||
$this->placementScoreModel->update($existing[$studentId]['id'], [
|
||||
'score' => $score,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
} else {
|
||||
$this->placementScoreModel->insert([
|
||||
'batch_id' => $batchId,
|
||||
'student_id' => $studentId,
|
||||
'score' => $score,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->placementBatchModel->update($batchId, [
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
return ['kind' => 'flash', 'redirect' => base_url('grading/placement'), 'type' => 'status', 'message' => 'Placement batch updated.'];
|
||||
}
|
||||
|
||||
public function fetchActiveStudentsWithSection(string $schoolYear): array
|
||||
{
|
||||
return $this->db->table('students s')
|
||||
->select('s.id AS student_id, s.school_id, s.firstname, s.lastname, s.is_active, sc.class_section_id, cs.class_section_name, c.class_name, e.enrollment_status, e.is_withdrawn')
|
||||
->join('student_class sc', 'sc.student_id = s.id AND sc.school_year = ' . $this->db->escape($schoolYear), 'left')
|
||||
->join('enrollments e', 'e.student_id = s.id AND e.school_year = ' . $this->db->escape($schoolYear), 'left')
|
||||
->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left')
|
||||
->join('classes c', 'c.id = cs.class_id', 'left')
|
||||
->groupStart()
|
||||
->where('s.is_active', 1)
|
||||
->orWhere("LOWER(TRIM(e.enrollment_status)) IN ('denied','withdrawn','waitlist')", null, false)
|
||||
->orWhere('e.is_withdrawn', 1)
|
||||
->groupEnd()
|
||||
->orderBy('s.lastname', 'ASC')
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
public function fetchPlacementBatches(string $schoolYear): array
|
||||
{
|
||||
return $this->placementBatchModel
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
public function fetchPlacementBatchDetails(array $batches, string $schoolYear): array
|
||||
{
|
||||
if (empty($batches)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$batchIds = array_values(array_filter(array_map(
|
||||
static fn($row) => (int) ($row['id'] ?? 0),
|
||||
$batches
|
||||
), static fn($id) => $id > 0));
|
||||
|
||||
if (empty($batchIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->db->table('placement_scores ps')
|
||||
->select('ps.batch_id, ps.student_id, ps.score, s.school_id, s.firstname, s.lastname, s.is_active, e.enrollment_status, e.is_withdrawn, cs.class_section_name, c.class_name')
|
||||
->join('students s', 's.id = ps.student_id', 'inner')
|
||||
->join('student_class sc', 'sc.student_id = s.id AND sc.school_year = ' . $this->db->escape($schoolYear), 'left')
|
||||
->join('enrollments e', 'e.student_id = s.id AND e.school_year = ' . $this->db->escape($schoolYear), 'left')
|
||||
->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left')
|
||||
->join('classes c', 'c.id = cs.class_id', 'left')
|
||||
->whereIn('ps.batch_id', $batchIds)
|
||||
->groupStart()
|
||||
->where('s.is_active', 1)
|
||||
->orWhere("LOWER(TRIM(e.enrollment_status)) IN ('denied','withdrawn','waitlist')", null, false)
|
||||
->orWhere('e.is_withdrawn', 1)
|
||||
->groupEnd()
|
||||
->orderBy('ps.batch_id', 'ASC')
|
||||
->orderBy('s.lastname', 'ASC')
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$details = [];
|
||||
foreach ($rows as $row) {
|
||||
$bid = (int) ($row['batch_id'] ?? 0);
|
||||
if ($bid <= 0) continue;
|
||||
$details[$bid][] = $row;
|
||||
}
|
||||
|
||||
return $details;
|
||||
}
|
||||
|
||||
public function fetchPlacementScoresForBatch(int $batchId): array
|
||||
{
|
||||
$rows = $this->placementScoreModel
|
||||
->where('batch_id', $batchId)
|
||||
->findAll();
|
||||
$scores = [];
|
||||
foreach ($rows as $row) {
|
||||
$scores[(int) $row['student_id']] = $row;
|
||||
}
|
||||
return $scores;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user