@@ -595,9 +595,10 @@ final class SchoolYearClosingService
|
||||
$builder = $this->db->table('student_class sc')
|
||||
->select('sc.student_id, sc.class_section_id, sc.created_at, sc.updated_at')
|
||||
->select('s.school_id, s.firstname, s.lastname')
|
||||
->select('cs.class_section_name')
|
||||
->select('cs.class_section_name, cs.class_id, c.class_name')
|
||||
->join('students s', 's.id = sc.student_id', 'inner')
|
||||
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left')
|
||||
->join('classes c', 'c.id = cs.class_id', 'left')
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->where('sc.class_section_id IS NOT NULL', null, false);
|
||||
|
||||
@@ -639,6 +640,8 @@ final class SchoolYearClosingService
|
||||
'school_id' => (string) ($row['school_id'] ?? ''),
|
||||
'student_name' => trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')),
|
||||
'class_section_id' => (int) ($row['class_section_id'] ?? 0),
|
||||
'class_id' => (int) ($row['class_id'] ?? 0),
|
||||
'class_name' => (string) ($row['class_name'] ?? ''),
|
||||
'class_section_name' => (string) ($row['class_section_name'] ?? ''),
|
||||
'dob' => (string) ($row['dob'] ?? ''),
|
||||
'registration_grade' => (string) ($row['registration_grade'] ?? ''),
|
||||
@@ -703,6 +706,14 @@ final class SchoolYearClosingService
|
||||
$student['target_section'] = $queue['target_section'];
|
||||
}
|
||||
|
||||
if ($this->isKgStudent($student)) {
|
||||
$kgPlacement = $this->kgPlacementLabelByTargetYearStartCutoff((string) ($student['dob'] ?? ''), $targetSchoolYear);
|
||||
if ($kgPlacement !== '') {
|
||||
$student['target_section'] = '';
|
||||
$student['target_class'] = $kgPlacement;
|
||||
}
|
||||
}
|
||||
|
||||
$summary['total_students']++;
|
||||
if ($student['status'] === 'missing') {
|
||||
$summary['missing_decision']++;
|
||||
@@ -712,10 +723,22 @@ final class SchoolYearClosingService
|
||||
$summary['with_decision']++;
|
||||
if (($student['normalized_decision'] ?? null) === DeliberationDecision::PASSED) {
|
||||
$summary['pass']++;
|
||||
if ($student['target_class'] === '') {
|
||||
$student['target_class'] = $this->nextClassLabelForPromotionPreview(
|
||||
(string) ($student['class_name'] ?: $student['class_section_name']),
|
||||
$targetSchoolYear
|
||||
);
|
||||
}
|
||||
if ($queue === null && $student['auto_kg_pass'] !== true) {
|
||||
$summary['missing_queue']++;
|
||||
}
|
||||
} else {
|
||||
if (($student['normalized_decision'] ?? null) === DeliberationDecision::REPEAT_CLASS) {
|
||||
$student['target_section'] = '';
|
||||
$student['target_class'] = $student['target_class'] !== ''
|
||||
? $student['target_class']
|
||||
: $this->classOnlyLabel((string) ($student['class_name'] ?: $student['class_section_name']));
|
||||
}
|
||||
$summary['other_decision']++;
|
||||
}
|
||||
}
|
||||
@@ -740,9 +763,9 @@ final class SchoolYearClosingService
|
||||
|
||||
private function isKgStudent(array $student): bool
|
||||
{
|
||||
foreach (['class_section_name', 'registration_grade'] as $field) {
|
||||
foreach (['class_name', 'class_section_name'] as $field) {
|
||||
$value = strtoupper(trim((string) ($student[$field] ?? '')));
|
||||
if ($value === 'KG' || str_starts_with($value, 'KG-') || str_contains($value, 'KINDERGARTEN')) {
|
||||
if (preg_match('/(^|[^A-Z0-9])KG([^A-Z0-9]|$)/', $value) === 1 || str_contains($value, 'KINDERGARTEN')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -753,13 +776,13 @@ final class SchoolYearClosingService
|
||||
private function kgAgeStatusByTargetYearStartCutoff(string $dob, ?string $targetSchoolYear): string
|
||||
{
|
||||
$dob = trim($dob);
|
||||
if ($dob === '' || $targetSchoolYear === null || ! preg_match('/^(\d{4})-\d{4}$/', $targetSchoolYear, $matches)) {
|
||||
if ($dob === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
$birthDate = new \DateTimeImmutable($dob);
|
||||
$cutoff = new \DateTimeImmutable($matches[1] . '-09-01');
|
||||
$cutoff = $this->septemberFirstCutoff($targetSchoolYear);
|
||||
} catch (\Throwable) {
|
||||
return '';
|
||||
}
|
||||
@@ -776,6 +799,27 @@ final class SchoolYearClosingService
|
||||
return 'keep_kg';
|
||||
}
|
||||
|
||||
private function kgPlacementLabelByTargetYearStartCutoff(string $dob, ?string $targetSchoolYear): string
|
||||
{
|
||||
return match ($this->kgAgeStatusByTargetYearStartCutoff($dob, $targetSchoolYear)) {
|
||||
'pass' => '1',
|
||||
'keep_kg' => 'KG',
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
private function septemberFirstCutoff(?string $targetSchoolYear): \DateTimeImmutable
|
||||
{
|
||||
if ($targetSchoolYear !== null && preg_match('/^(\d{4})-\d{4}$/', $targetSchoolYear, $matches) === 1) {
|
||||
return new \DateTimeImmutable($matches[1] . '-09-01');
|
||||
}
|
||||
|
||||
$today = new \DateTimeImmutable('today');
|
||||
$cutoff = new \DateTimeImmutable($today->format('Y') . '-09-01');
|
||||
|
||||
return $today <= $cutoff ? $cutoff : $cutoff->modify('+1 year');
|
||||
}
|
||||
|
||||
private function promotionDecisionRows(array $studentIds, string $schoolYear): array
|
||||
{
|
||||
if (! $this->db->tableExists('student_decisions')) {
|
||||
@@ -880,6 +924,83 @@ final class SchoolYearClosingService
|
||||
return $queueRows;
|
||||
}
|
||||
|
||||
private function nextClassLabelForPromotionPreview(string $sourceClassName, ?string $targetSchoolYear): string
|
||||
{
|
||||
$base = $this->classBaseName($sourceClassName);
|
||||
$target = match (true) {
|
||||
$base === 'KG' || str_contains($base, 'KINDERGARTEN') => '1',
|
||||
ctype_digit($base) => (string) ((int) $base + 1),
|
||||
$base === 'YOUTH' => 'YOUTH',
|
||||
default => '',
|
||||
};
|
||||
|
||||
if ($target === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($targetSchoolYear !== null && $targetSchoolYear !== '' && $this->db->tableExists('classes')) {
|
||||
$targetClass = $this->classByNameForPromotionPreview($target, $targetSchoolYear);
|
||||
if ($targetClass !== null) {
|
||||
return (string) ($targetClass['class_name'] ?? $target);
|
||||
}
|
||||
|
||||
if (ctype_digit($base) && (int) $base >= 9) {
|
||||
$youthClass = $this->classByNameForPromotionPreview('YOUTH', $targetSchoolYear);
|
||||
if ($youthClass !== null) {
|
||||
return (string) ($youthClass['class_name'] ?? 'YOUTH');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $target;
|
||||
}
|
||||
|
||||
private function classOnlyLabel(string $className): string
|
||||
{
|
||||
return trim((string) preg_replace('/-.+$/', '', $className));
|
||||
}
|
||||
|
||||
private function classBaseName(string $className): string
|
||||
{
|
||||
$base = strtoupper(trim((string) preg_replace('/-.+$/', '', $className)));
|
||||
$base = preg_replace('/\b(CLASS|GRADE)\b/i', '', $base) ?? $base;
|
||||
$base = trim(preg_replace('/\s+/', ' ', $base) ?? $base);
|
||||
|
||||
if (str_contains($base, 'KINDERGARTEN')) {
|
||||
return 'KG';
|
||||
}
|
||||
|
||||
if (preg_match('/(^|[^A-Z0-9])KG([^A-Z0-9]|$)/', $base) === 1) {
|
||||
return 'KG';
|
||||
}
|
||||
|
||||
if (preg_match('/\d+/', $base, $matches) === 1) {
|
||||
return (string) (int) $matches[0];
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
|
||||
private function classByNameForPromotionPreview(string $className, string $schoolYear): ?array
|
||||
{
|
||||
$builder = $this->db->table('classes')->where('UPPER(class_name)', strtoupper($className));
|
||||
if ($this->db->fieldExists('school_year', 'classes')) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$row = $builder->orderBy('id', 'DESC')->limit(1)->get()->getRowArray();
|
||||
if ($row !== null || ! $this->db->fieldExists('school_year', 'classes')) {
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
return $this->db->table('classes')
|
||||
->where('UPPER(class_name)', strtoupper($className))
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(1)
|
||||
->get()
|
||||
->getRowArray() ?: null;
|
||||
}
|
||||
|
||||
private function countBySchoolYear(string $table, string $schoolYear, string $distinctField): int
|
||||
{
|
||||
if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {
|
||||
|
||||
Reference in New Issue
Block a user