371 lines
12 KiB
PHP
371 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Services\AttendanceTracking;
|
|
|
|
use App\Models\AttendanceData;
|
|
use App\Models\Student;
|
|
use App\Models\StudentClass;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class AttendanceViolationStudentResolverService
|
|
{
|
|
public function __construct(
|
|
protected Student $studentModel,
|
|
protected StudentClass $studentClassModel,
|
|
protected AttendanceData $attendanceDataModel
|
|
) {}
|
|
|
|
public function resolveForSchoolYear(string $schoolYear, ?string $semester = null): array
|
|
{
|
|
$debug = [
|
|
'class_students' => 0,
|
|
'student_ids' => 0,
|
|
];
|
|
|
|
$classStudentsQuery = $this->studentClassModel->query();
|
|
if (method_exists($this->studentClassModel, 'scopeActive')) {
|
|
$classStudentsQuery->active();
|
|
}
|
|
|
|
$classStudents = $classStudentsQuery
|
|
->where('student_class.school_year', $schoolYear)
|
|
->get()
|
|
->toArray();
|
|
|
|
$debug['class_students'] = count($classStudents);
|
|
|
|
if (empty($classStudents)) {
|
|
$latestTerm = DB::table('student_class')
|
|
->select('school_year', 'semester')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
if ($latestTerm && ! empty($latestTerm->school_year)) {
|
|
$schoolYear = (string) $latestTerm->school_year;
|
|
if (! empty($latestTerm->semester)) {
|
|
$semester = (string) $latestTerm->semester;
|
|
}
|
|
|
|
$classStudentsQuery = $this->studentClassModel->query();
|
|
if (method_exists($this->studentClassModel, 'scopeActive')) {
|
|
$classStudentsQuery->active();
|
|
}
|
|
|
|
$classStudents = $classStudentsQuery
|
|
->where('student_class.school_year', $schoolYear)
|
|
->get()
|
|
->toArray();
|
|
|
|
$debug['class_students'] = count($classStudents);
|
|
}
|
|
}
|
|
|
|
[$studentIds, $studentCodes] = $this->extractStudentIdentifiers($classStudents);
|
|
|
|
if (empty($studentIds) && empty($studentCodes)) {
|
|
[$studentIds, $studentCodes] = $this->deriveIdentifiersFromAttendanceData($schoolYear);
|
|
}
|
|
|
|
[$studentIds, $studentCodes] = $this->filterInactiveIdentifiers($studentIds, $studentCodes);
|
|
|
|
$debug['student_ids'] = count($studentIds) + count($studentCodes);
|
|
|
|
if (empty($studentIds) && empty($studentCodes)) {
|
|
return [
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'student_ids' => [],
|
|
'student_codes' => [],
|
|
'students' => [],
|
|
'student_code_to_id' => [],
|
|
'existing_ids' => [],
|
|
'debug' => $debug,
|
|
];
|
|
}
|
|
|
|
$students = $this->loadActiveStudents($studentIds, $studentCodes);
|
|
$students = $this->addPlaceholderStudentsForMissingCodes($students, $studentCodes);
|
|
$students = $this->ensureEntriesForAllIdentifiers($students, $studentIds, $studentCodes);
|
|
|
|
[$studentCodeToId, $existingIds] = $this->buildLookupMaps($students);
|
|
|
|
return [
|
|
'school_year' => $schoolYear,
|
|
'semester' => $semester,
|
|
'student_ids' => $studentIds,
|
|
'student_codes' => $studentCodes,
|
|
'students' => $students,
|
|
'student_code_to_id' => $studentCodeToId,
|
|
'existing_ids' => $existingIds,
|
|
'debug' => $debug,
|
|
];
|
|
}
|
|
|
|
public function ensureAttendanceStudentExists(
|
|
array &$students,
|
|
array &$studentCodeToId,
|
|
array &$existingIds,
|
|
mixed $rawStudentId
|
|
): ?int {
|
|
if (is_numeric($rawStudentId)) {
|
|
$sid = (int) $rawStudentId;
|
|
if ($sid <= 0) {
|
|
return null;
|
|
}
|
|
|
|
if (! isset($existingIds[$sid])) {
|
|
$students[] = [
|
|
'id' => $sid,
|
|
'school_id' => (string) $sid,
|
|
'firstname' => (string) $sid,
|
|
'lastname' => '',
|
|
'parent_id' => 0,
|
|
];
|
|
$existingIds[$sid] = true;
|
|
}
|
|
|
|
return $sid;
|
|
}
|
|
|
|
if (is_string($rawStudentId)) {
|
|
$code = trim($rawStudentId);
|
|
if ($code === '') {
|
|
return null;
|
|
}
|
|
|
|
$sid = $studentCodeToId[$code] ?? null;
|
|
if ($sid === null) {
|
|
$sid = abs(crc32($code)) ?: random_int(3000000, 3999999);
|
|
$studentCodeToId[$code] = $sid;
|
|
|
|
if (! isset($existingIds[$sid])) {
|
|
$students[] = [
|
|
'id' => $sid,
|
|
'school_id' => $code,
|
|
'firstname' => $code,
|
|
'lastname' => '',
|
|
'parent_id' => 0,
|
|
];
|
|
$existingIds[$sid] = true;
|
|
}
|
|
}
|
|
|
|
return $sid;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function extractStudentIdentifiers(array $classStudents): array
|
|
{
|
|
$studentIds = [];
|
|
$studentCodes = [];
|
|
|
|
foreach ($classStudents as $row) {
|
|
$raw = $row['student_id'] ?? null;
|
|
|
|
if (is_numeric($raw)) {
|
|
$studentIds[] = (int) $raw;
|
|
} elseif (is_string($raw) && $raw !== '') {
|
|
$studentCodes[] = trim($raw);
|
|
}
|
|
}
|
|
|
|
$studentIds = array_values(array_unique(array_filter($studentIds, fn ($v) => $v > 0)));
|
|
$studentCodes = array_values(array_unique(array_filter($studentCodes, fn ($v) => $v !== '')));
|
|
|
|
return [$studentIds, $studentCodes];
|
|
}
|
|
|
|
protected function deriveIdentifiersFromAttendanceData(string $schoolYear): array
|
|
{
|
|
$studentIds = [];
|
|
$studentCodes = [];
|
|
|
|
$attendanceStudents = DB::table($this->attendanceDataModel->getTable())
|
|
->selectRaw('DISTINCT student_id')
|
|
->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
|
|
->orderBy('student_id')
|
|
->get();
|
|
|
|
foreach ($attendanceStudents as $r) {
|
|
$raw = $r->student_id ?? null;
|
|
|
|
if (is_numeric($raw)) {
|
|
$studentIds[] = (int) $raw;
|
|
} elseif (is_string($raw) && $raw !== '') {
|
|
$studentCodes[] = trim($raw);
|
|
}
|
|
}
|
|
|
|
$studentIds = array_values(array_unique(array_filter($studentIds, fn ($v) => $v > 0)));
|
|
$studentCodes = array_values(array_unique(array_filter($studentCodes, fn ($v) => $v !== '')));
|
|
|
|
return [$studentIds, $studentCodes];
|
|
}
|
|
|
|
protected function filterInactiveIdentifiers(array $studentIds, array $studentCodes): array
|
|
{
|
|
if (! empty($studentIds)) {
|
|
$inactiveRows = $this->studentModel->query()
|
|
->select('id')
|
|
->whereIn('id', $studentIds)
|
|
->where('is_active', 0)
|
|
->get()
|
|
->toArray();
|
|
|
|
$inactiveIdSet = array_flip(array_map(
|
|
static fn ($r) => (int) ($r['id'] ?? 0),
|
|
$inactiveRows
|
|
));
|
|
|
|
$studentIds = array_values(array_filter(
|
|
$studentIds,
|
|
static fn ($id) => ! isset($inactiveIdSet[$id])
|
|
));
|
|
}
|
|
|
|
if (! empty($studentCodes)) {
|
|
$inactiveCodeRows = $this->studentModel->query()
|
|
->select('school_id')
|
|
->whereIn('school_id', $studentCodes)
|
|
->where('is_active', 0)
|
|
->get()
|
|
->toArray();
|
|
|
|
$inactiveCodeSet = array_flip(array_map(
|
|
static fn ($r) => (string) ($r['school_id'] ?? ''),
|
|
$inactiveCodeRows
|
|
));
|
|
|
|
$studentCodes = array_values(array_filter(
|
|
$studentCodes,
|
|
static fn ($code) => $code !== '' && ! isset($inactiveCodeSet[$code])
|
|
));
|
|
}
|
|
|
|
return [$studentIds, $studentCodes];
|
|
}
|
|
|
|
protected function loadActiveStudents(array $studentIds, array $studentCodes): array
|
|
{
|
|
$students = [];
|
|
|
|
if (! empty($studentIds)) {
|
|
$students = $this->studentModel->query()
|
|
->whereIn('id', $studentIds)
|
|
->where('is_active', 1)
|
|
->get()
|
|
->toArray();
|
|
}
|
|
|
|
if (! empty($studentCodes)) {
|
|
$byCode = $this->studentModel->query()
|
|
->whereIn('school_id', $studentCodes)
|
|
->where('is_active', 1)
|
|
->get()
|
|
->toArray();
|
|
|
|
$seen = array_flip(array_map(fn ($s) => (int) ($s['id'] ?? 0), $students));
|
|
|
|
foreach ($byCode as $s) {
|
|
$sid = (int) ($s['id'] ?? 0);
|
|
if ($sid > 0 && ! isset($seen[$sid])) {
|
|
$students[] = $s;
|
|
$seen[$sid] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $students;
|
|
}
|
|
|
|
protected function addPlaceholderStudentsForMissingCodes(array $students, array $studentCodes): array
|
|
{
|
|
$knownCodes = [];
|
|
|
|
foreach ($students as $s) {
|
|
$code = trim((string) ($s['school_id'] ?? ''));
|
|
if ($code !== '') {
|
|
$knownCodes[$code] = true;
|
|
}
|
|
}
|
|
|
|
foreach ($studentCodes as $code) {
|
|
if (! isset($knownCodes[$code])) {
|
|
$virtualId = abs(crc32($code)) ?: random_int(1000000, 1999999);
|
|
$students[] = [
|
|
'id' => $virtualId,
|
|
'school_id' => $code,
|
|
'firstname' => $code,
|
|
'lastname' => '',
|
|
'parent_id' => 0,
|
|
'class_name' => '',
|
|
];
|
|
}
|
|
}
|
|
|
|
return $students;
|
|
}
|
|
|
|
protected function ensureEntriesForAllIdentifiers(array $students, array $studentIds, array $studentCodes): array
|
|
{
|
|
[$studentCodeToId, $existingIds] = $this->buildLookupMaps($students);
|
|
|
|
foreach (array_merge($studentIds, $studentCodes) as $sidOrCode) {
|
|
if (is_numeric($sidOrCode)) {
|
|
$sid = (int) $sidOrCode;
|
|
|
|
if ($sid > 0 && ! isset($existingIds[$sid])) {
|
|
$students[] = [
|
|
'id' => $sid,
|
|
'school_id' => (string) $sid,
|
|
'firstname' => (string) $sid,
|
|
'lastname' => '',
|
|
'parent_id' => 0,
|
|
'class_name' => '',
|
|
];
|
|
$existingIds[$sid] = true;
|
|
}
|
|
} else {
|
|
$code = (string) $sidOrCode;
|
|
|
|
if ($code !== '' && ! isset($studentCodeToId[$code])) {
|
|
$virtualId = abs(crc32($code)) ?: random_int(2000000, 2999999);
|
|
$students[] = [
|
|
'id' => $virtualId,
|
|
'school_id' => $code,
|
|
'firstname' => $code,
|
|
'lastname' => '',
|
|
'parent_id' => 0,
|
|
'class_name' => '',
|
|
];
|
|
|
|
$studentCodeToId[$code] = $virtualId;
|
|
$existingIds[$virtualId] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $students;
|
|
}
|
|
|
|
protected function buildLookupMaps(array $students): array
|
|
{
|
|
$studentCodeToId = [];
|
|
foreach ($students as $stu) {
|
|
$code = trim((string) ($stu['student_code'] ?? $stu['school_id'] ?? ''));
|
|
if ($code !== '') {
|
|
$studentCodeToId[$code] = (int) ($stu['id'] ?? 0);
|
|
}
|
|
}
|
|
|
|
$existingIds = array_flip(array_map(
|
|
fn ($s) => (int) ($s['id'] ?? 0),
|
|
$students
|
|
));
|
|
|
|
return [$studentCodeToId, $existingIds];
|
|
}
|
|
}
|