683 lines
23 KiB
PHP
683 lines
23 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Certificates;
|
|
|
|
use App\Models\CertificateRecord;
|
|
use App\Models\Configuration;
|
|
use App\Models\StudentDecision;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class CertificateAdminService
|
|
{
|
|
private bool $hasVerificationTokenColumn;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->hasVerificationTokenColumn = Schema::hasTable('certificate_records')
|
|
&& Schema::hasColumn('certificate_records', 'verification_token');
|
|
}
|
|
|
|
public function dashboard(?string $schoolYear = null): array
|
|
{
|
|
$selectedYear = $this->resolveSchoolYear($schoolYear);
|
|
|
|
$allEnrolled = DB::table('student_class as sc')
|
|
->select(
|
|
's.id as student_id',
|
|
's.firstname',
|
|
's.lastname',
|
|
'sc.class_section_id',
|
|
'cs.class_section_name'
|
|
)
|
|
->join('students as s', 's.id', '=', 'sc.student_id')
|
|
->join('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
|
->where('s.is_active', 1)
|
|
->where('sc.school_year', $selectedYear)
|
|
->orderBy('s.firstname', 'ASC')
|
|
->orderBy('s.lastname', 'ASC')
|
|
->get()
|
|
->map(fn ($row) => (array) $row)
|
|
->all();
|
|
|
|
$studentIds = array_values(array_unique(array_map(
|
|
static fn (array $row): int => (int) ($row['student_id'] ?? 0),
|
|
$allEnrolled
|
|
)));
|
|
$studentIds = array_values(array_filter($studentIds, static fn (int $id): bool => $id > 0));
|
|
|
|
$decisionsByStudent = [];
|
|
if ($studentIds !== []) {
|
|
$decisionRows = StudentDecision::query()
|
|
->whereIn('student_id', $studentIds)
|
|
->where('school_year', $selectedYear)
|
|
->get();
|
|
|
|
foreach ($decisionRows as $decisionRow) {
|
|
$sid = (int) ($decisionRow->student_id ?? 0);
|
|
if ($sid <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$decision = trim((string) ($decisionRow->decision ?? ''));
|
|
$source = trim((string) ($decisionRow->source ?? ''));
|
|
if ($source === '') {
|
|
$source = $decision === '' ? 'pending' : 'manual';
|
|
}
|
|
|
|
$decisionsByStudent[$sid][] = [
|
|
'decision' => $decision,
|
|
'source' => $source,
|
|
'notes' => (string) ($decisionRow->notes ?? ''),
|
|
'year_score' => is_numeric($decisionRow->year_score) ? round((float) $decisionRow->year_score, 2) : null,
|
|
];
|
|
}
|
|
}
|
|
|
|
$certsByStudent = [];
|
|
if ($studentIds !== []) {
|
|
$certRows = DB::table('certificate_records')
|
|
->select('student_id', 'certificate_number', 'issued_at')
|
|
->whereIn('student_id', $studentIds)
|
|
->where('school_year', $selectedYear)
|
|
->orderByDesc('issued_at')
|
|
->get();
|
|
|
|
foreach ($certRows as $record) {
|
|
$sid = (int) ($record->student_id ?? 0);
|
|
if ($sid > 0 && ! isset($certsByStudent[$sid])) {
|
|
$certsByStudent[$sid] = (string) ($record->certificate_number ?? '');
|
|
}
|
|
}
|
|
}
|
|
|
|
$statsPerClass = [];
|
|
$studentsByClass = [];
|
|
|
|
foreach ($allEnrolled as $row) {
|
|
$sid = (int) ($row['student_id'] ?? 0);
|
|
$classSectionId = (int) ($row['class_section_id'] ?? 0);
|
|
$classSectionName = (string) ($row['class_section_name'] ?? ('Section '.$classSectionId));
|
|
|
|
if (! isset($statsPerClass[$classSectionId])) {
|
|
$statsPerClass[$classSectionId] = [
|
|
'name' => $classSectionName,
|
|
'total' => 0,
|
|
'pass' => 0,
|
|
'cert' => 0,
|
|
];
|
|
}
|
|
|
|
$statsPerClass[$classSectionId]['total']++;
|
|
|
|
$studentDecisionRows = $decisionsByStudent[$sid] ?? [];
|
|
$decisionState = $this->normalizeDecisionState($studentDecisionRows);
|
|
|
|
if ($decisionState['eligible']) {
|
|
$statsPerClass[$classSectionId]['pass']++;
|
|
}
|
|
|
|
$certificateNumber = $certsByStudent[$sid] ?? null;
|
|
if ($certificateNumber !== null && $certificateNumber !== '') {
|
|
$statsPerClass[$classSectionId]['cert']++;
|
|
}
|
|
|
|
$studentsByClass[$classSectionId][] = [
|
|
'student_id' => $sid,
|
|
'firstname' => (string) ($row['firstname'] ?? ''),
|
|
'lastname' => (string) ($row['lastname'] ?? ''),
|
|
'class_section_id' => $classSectionId,
|
|
'class_section_name' => $classSectionName,
|
|
'year_score' => $decisionState['year_score'],
|
|
'eligible' => $decisionState['eligible'],
|
|
'decision_state' => $decisionState['status'],
|
|
'decision_labels' => $decisionState['display_decisions'],
|
|
'decision_rows' => $studentDecisionRows,
|
|
'certificate_number' => $certificateNumber,
|
|
];
|
|
}
|
|
|
|
$gradeGroups = $this->buildGradeGroups($statsPerClass, $studentsByClass);
|
|
|
|
return [
|
|
'school_year' => $selectedYear,
|
|
'cert_date' => date('m/d/Y'),
|
|
'grade_groups' => array_values($gradeGroups),
|
|
'stats_per_class' => $statsPerClass,
|
|
'default_group_key' => array_key_first($gradeGroups),
|
|
];
|
|
}
|
|
|
|
public function auditLog(?string $schoolYear = null): array
|
|
{
|
|
$selectedYear = trim((string) ($schoolYear ?? ''));
|
|
|
|
$recordsQuery = DB::table('certificate_records as cr')
|
|
->select(
|
|
'cr.*',
|
|
'u.firstname as admin_firstname',
|
|
'u.lastname as admin_lastname'
|
|
)
|
|
->leftJoin('users as u', 'u.id', '=', 'cr.issued_by')
|
|
->orderByDesc('cr.issued_at');
|
|
|
|
if ($selectedYear !== '') {
|
|
$recordsQuery->where('cr.school_year', $selectedYear);
|
|
}
|
|
|
|
$records = $recordsQuery->get()->map(fn ($row) => (array) $row)->all();
|
|
|
|
$yearSummary = DB::table('certificate_records')
|
|
->select('school_year', DB::raw('COUNT(*) as total'))
|
|
->groupBy('school_year')
|
|
->orderBy('school_year', 'DESC')
|
|
->get()
|
|
->map(fn ($row) => (array) $row)
|
|
->all();
|
|
|
|
return [
|
|
'school_year' => $selectedYear,
|
|
'records' => $records,
|
|
'year_summary' => $yearSummary,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param list<int> $studentIds
|
|
* @return array{students: list<array<string, mixed>>, cert_date_display: string, school_year: string}
|
|
*/
|
|
public function issueCertificates(
|
|
array $studentIds,
|
|
string $certDate,
|
|
?int $classSectionId,
|
|
?string $schoolYear,
|
|
?int $issuedBy
|
|
): array {
|
|
$studentIds = array_values(array_filter(array_map('intval', $studentIds), static fn (int $id): bool => $id > 0));
|
|
if ($studentIds === []) {
|
|
throw new \InvalidArgumentException('Please select at least one student.');
|
|
}
|
|
|
|
$selectedYear = $this->resolveSchoolYear($schoolYear);
|
|
$certDateDisplay = $this->normalizeDisplayCertDate($certDate);
|
|
$certDateStorage = $this->parseCertDate($certDateDisplay);
|
|
$students = $this->fetchStudentsForIssuance($studentIds, $classSectionId, $selectedYear);
|
|
|
|
if ($students === []) {
|
|
throw new \RuntimeException('No valid students found.');
|
|
}
|
|
|
|
$rosterMap = [];
|
|
foreach ($students as $student) {
|
|
$rosterMap[(int) $student['id']] = $student;
|
|
}
|
|
|
|
$existingByStudent = DB::table('certificate_records')
|
|
->whereIn('student_id', array_keys($rosterMap))
|
|
->where('school_year', $selectedYear)
|
|
->orderByDesc('issued_at')
|
|
->get()
|
|
->map(fn ($row) => (array) $row)
|
|
->groupBy('student_id')
|
|
->map(fn ($rows) => $rows[0])
|
|
->all();
|
|
|
|
$baseCount = (int) DB::table('certificate_records')
|
|
->where('school_year', $selectedYear)
|
|
->count();
|
|
$nextOffset = 0;
|
|
|
|
DB::beginTransaction();
|
|
|
|
try {
|
|
foreach ($students as &$student) {
|
|
$sid = (int) ($student['id'] ?? 0);
|
|
$existing = $existingByStudent[$sid] ?? null;
|
|
|
|
if (is_array($existing)) {
|
|
$student['cert_number'] = (string) ($existing['certificate_number'] ?? '');
|
|
$student['verify_token'] = $this->ensureVerificationToken($existing);
|
|
} else {
|
|
$nextOffset++;
|
|
$certificateNumber = $this->formatCertificateNumber($selectedYear, $baseCount + $nextOffset);
|
|
$verificationToken = $this->hasVerificationTokenColumn ? $this->generateVerificationToken() : null;
|
|
$grade = $this->formatGrade((string) ($student['grade'] ?? ''));
|
|
$studentName = trim((string) ($student['firstname'] ?? '').' '.(string) ($student['lastname'] ?? ''));
|
|
|
|
$payload = [
|
|
'certificate_number' => $certificateNumber,
|
|
'student_id' => $sid,
|
|
'student_name' => $studentName,
|
|
'grade' => $grade,
|
|
'cert_date' => $certDateStorage,
|
|
'school_year' => $selectedYear,
|
|
'class_section_id' => $classSectionId,
|
|
'issued_by' => $issuedBy,
|
|
'issued_at' => now(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
];
|
|
|
|
if ($this->hasVerificationTokenColumn) {
|
|
$payload['verification_token'] = $verificationToken;
|
|
}
|
|
|
|
DB::table('certificate_records')->insert($payload);
|
|
|
|
$student['cert_number'] = $certificateNumber;
|
|
$student['verify_token'] = $verificationToken;
|
|
}
|
|
|
|
$student['grade'] = $this->formatGrade((string) ($student['grade'] ?? ''));
|
|
$student['verify_url'] = ! empty($student['verify_token'])
|
|
? url('/api/certificates/verify/'.rawurlencode((string) $student['verify_token']))
|
|
: null;
|
|
}
|
|
unset($student);
|
|
|
|
DB::commit();
|
|
} catch (\Throwable $e) {
|
|
DB::rollBack();
|
|
throw $e;
|
|
}
|
|
|
|
return [
|
|
'students' => $students,
|
|
'cert_date_display' => $certDateDisplay,
|
|
'school_year' => $selectedYear,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{student: array<string, mixed>, cert_date_display: string}|null
|
|
*/
|
|
public function reprintPayload(string $certificateNumber): ?array
|
|
{
|
|
$record = DB::table('certificate_records')
|
|
->where('certificate_number', strtoupper(trim($certificateNumber)))
|
|
->first();
|
|
|
|
if (! $record) {
|
|
return null;
|
|
}
|
|
|
|
$row = (array) $record;
|
|
$studentName = trim((string) ($row['student_name'] ?? ''));
|
|
[$firstname, $lastname] = $this->splitStudentName($studentName);
|
|
$verifyToken = $this->ensureVerificationToken($row);
|
|
|
|
return [
|
|
'student' => [
|
|
'id' => (int) ($row['student_id'] ?? 0),
|
|
'firstname' => $firstname,
|
|
'lastname' => $lastname,
|
|
'grade' => (string) ($row['grade'] ?? ''),
|
|
'cert_number' => (string) ($row['certificate_number'] ?? ''),
|
|
'verify_token' => $verifyToken,
|
|
'verify_url' => $verifyToken !== '' ? url('/api/certificates/verify/'.rawurlencode($verifyToken)) : null,
|
|
],
|
|
'cert_date_display' => $this->displayDateFromStorage($row['cert_date'] ?? null),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function verifyPayload(string $tokenOrNumber): ?array
|
|
{
|
|
$lookup = strtoupper(trim($tokenOrNumber));
|
|
|
|
$query = DB::table('certificate_records as cr')
|
|
->select(
|
|
'cr.certificate_number',
|
|
'cr.student_name',
|
|
'cr.grade',
|
|
'cr.cert_date',
|
|
'cr.school_year',
|
|
'cr.issued_at',
|
|
'u.firstname as admin_firstname',
|
|
'u.lastname as admin_lastname'
|
|
)
|
|
->leftJoin('users as u', 'u.id', '=', 'cr.issued_by');
|
|
|
|
if ($this->hasVerificationTokenColumn) {
|
|
$query->where(function ($builder) use ($tokenOrNumber, $lookup): void {
|
|
$builder->where('cr.verification_token', trim($tokenOrNumber))
|
|
->orWhere('cr.certificate_number', $lookup);
|
|
});
|
|
} else {
|
|
$query->where('cr.certificate_number', $lookup);
|
|
}
|
|
|
|
$record = $query->first();
|
|
if (! $record) {
|
|
return null;
|
|
}
|
|
|
|
return (array) $record;
|
|
}
|
|
|
|
private function resolveSchoolYear(?string $schoolYear): string
|
|
{
|
|
$selectedYear = trim((string) ($schoolYear ?? ''));
|
|
|
|
if ($selectedYear !== '') {
|
|
return $selectedYear;
|
|
}
|
|
|
|
return trim((string) (Configuration::getConfig('school_year') ?? ''));
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array{name: string, total: int, pass: int, cert: int}> $statsPerClass
|
|
* @param array<int, list<array<string, mixed>>> $studentsByClass
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
private function buildGradeGroups(array $statsPerClass, array $studentsByClass): array
|
|
{
|
|
$gradeGroups = [];
|
|
|
|
foreach ($statsPerClass as $classSectionId => $stats) {
|
|
[$label, $sortKey, $slug] = $this->classGroupMeta((string) ($stats['name'] ?? ''));
|
|
|
|
if (! isset($gradeGroups[$sortKey])) {
|
|
$gradeGroups[$sortKey] = [
|
|
'key' => $sortKey,
|
|
'label' => $label,
|
|
'slug' => $slug,
|
|
'total' => 0,
|
|
'pass' => 0,
|
|
'cert' => 0,
|
|
'sections' => [],
|
|
];
|
|
}
|
|
|
|
$total = (int) ($stats['total'] ?? 0);
|
|
$pass = (int) ($stats['pass'] ?? 0);
|
|
$cert = (int) ($stats['cert'] ?? 0);
|
|
|
|
$gradeGroups[$sortKey]['total'] += $total;
|
|
$gradeGroups[$sortKey]['pass'] += $pass;
|
|
$gradeGroups[$sortKey]['cert'] += $cert;
|
|
$gradeGroups[$sortKey]['sections'][] = [
|
|
'section_id' => (int) $classSectionId,
|
|
'section_name' => (string) ($stats['name'] ?? ''),
|
|
'student_count' => $total,
|
|
'pass_count' => $pass,
|
|
'cert_count' => $cert,
|
|
'remaining_count' => max(0, $pass - $cert),
|
|
'students' => $studentsByClass[$classSectionId] ?? [],
|
|
];
|
|
}
|
|
|
|
ksort($gradeGroups);
|
|
|
|
foreach ($gradeGroups as &$group) {
|
|
$group['fully_done'] = $group['pass'] > 0 && $group['cert'] >= $group['pass'];
|
|
$group['has_pass'] = $group['pass'] > 0;
|
|
}
|
|
unset($group);
|
|
|
|
return $gradeGroups;
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $studentDecisionRows
|
|
* @return array{eligible: bool, status: string, display_decisions: list<string>, year_score: float|null}
|
|
*/
|
|
private function normalizeDecisionState(array $studentDecisionRows): array
|
|
{
|
|
if ($studentDecisionRows === []) {
|
|
return [
|
|
'eligible' => false,
|
|
'status' => 'pending',
|
|
'display_decisions' => [],
|
|
'year_score' => null,
|
|
];
|
|
}
|
|
|
|
$allDecisions = array_map(
|
|
static fn (array $row): string => trim((string) ($row['decision'] ?? '')),
|
|
$studentDecisionRows
|
|
);
|
|
|
|
$hasPending = in_array('', $allDecisions, true);
|
|
$unique = array_values(array_unique(array_filter(
|
|
$allDecisions,
|
|
static fn (string $decision): bool => $decision !== ''
|
|
)));
|
|
|
|
$eligible = ! $hasPending && $unique === ['Pass'];
|
|
$displayDecisions = $eligible
|
|
? ['Pass']
|
|
: array_values(array_filter($unique, static fn (string $decision): bool => $decision !== 'Pass'));
|
|
|
|
$yearScore = null;
|
|
foreach ($studentDecisionRows as $row) {
|
|
if (array_key_exists('year_score', $row) && $row['year_score'] !== null && $row['year_score'] !== '') {
|
|
$yearScore = round((float) $row['year_score'], 2);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'eligible' => $eligible,
|
|
'status' => ($hasPending || $displayDecisions === []) ? 'pending' : ($eligible ? 'pass' : 'decision'),
|
|
'display_decisions' => $displayDecisions,
|
|
'year_score' => $yearScore,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param list<int> $studentIds
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
private function fetchStudentsForIssuance(array $studentIds, ?int $classSectionId, string $schoolYear): array
|
|
{
|
|
$dashboard = $this->dashboard($schoolYear);
|
|
$eligibleMap = [];
|
|
|
|
foreach ($dashboard['grade_groups'] as $group) {
|
|
foreach (($group['sections'] ?? []) as $section) {
|
|
foreach (($section['students'] ?? []) as $student) {
|
|
$sid = (int) ($student['student_id'] ?? 0);
|
|
if ($sid > 0) {
|
|
$eligibleMap[$sid] = $student;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$filtered = [];
|
|
foreach ($studentIds as $studentId) {
|
|
$student = $eligibleMap[$studentId] ?? null;
|
|
if (! is_array($student) || empty($student['eligible'])) {
|
|
continue;
|
|
}
|
|
|
|
if ($classSectionId !== null && (int) ($student['class_section_id'] ?? 0) !== $classSectionId) {
|
|
continue;
|
|
}
|
|
|
|
$filtered[] = [
|
|
'id' => $studentId,
|
|
'firstname' => (string) ($student['firstname'] ?? ''),
|
|
'lastname' => (string) ($student['lastname'] ?? ''),
|
|
'grade' => (string) ($student['class_section_name'] ?? ''),
|
|
];
|
|
}
|
|
|
|
return $filtered;
|
|
}
|
|
|
|
private function formatGrade(string $raw): string
|
|
{
|
|
$clean = trim($raw);
|
|
$lower = strtolower($clean);
|
|
|
|
if (preg_match('/^grade\s*(\d+)/i', $clean, $matches)) {
|
|
return 'Grade '.(int) $matches[1];
|
|
}
|
|
|
|
if ($lower === 'youth') {
|
|
return 'Youth';
|
|
}
|
|
|
|
if ($lower === 'kg' || $lower === 'kindergarten') {
|
|
return 'Kindergarten';
|
|
}
|
|
|
|
if (preg_match('/^(\d+)([- ][A-Za-z0-9]+)?$/', $clean, $matches)) {
|
|
return 'Grade '.(int) $matches[1];
|
|
}
|
|
|
|
return $clean;
|
|
}
|
|
|
|
private function normalizeDisplayCertDate(string $certDate): string
|
|
{
|
|
$clean = trim($certDate);
|
|
if ($clean === '') {
|
|
return date('m/d/Y');
|
|
}
|
|
|
|
if (preg_match('#^\d{4}-\d{2}-\d{2}$#', $clean) === 1) {
|
|
$timestamp = strtotime($clean);
|
|
if ($timestamp !== false) {
|
|
return date('m/d/Y', $timestamp);
|
|
}
|
|
}
|
|
|
|
if (preg_match('#^\d{2}/\d{2}/\d{4}$#', $clean) === 1) {
|
|
return $clean;
|
|
}
|
|
|
|
return date('m/d/Y');
|
|
}
|
|
|
|
private function parseCertDate(string $certDate): ?string
|
|
{
|
|
if (preg_match('#^(\d{2})/(\d{2})/(\d{4})$#', $certDate, $matches) === 1) {
|
|
return $matches[3].'-'.$matches[1].'-'.$matches[2];
|
|
}
|
|
|
|
if (preg_match('#^\d{4}-\d{2}-\d{2}$#', $certDate) === 1) {
|
|
return $certDate;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function displayDateFromStorage(mixed $value): string
|
|
{
|
|
if ($value === null || trim((string) $value) === '') {
|
|
return date('m/d/Y');
|
|
}
|
|
|
|
$timestamp = strtotime((string) $value);
|
|
|
|
return $timestamp !== false ? date('m/d/Y', $timestamp) : date('m/d/Y');
|
|
}
|
|
|
|
private function formatCertificateNumber(string $schoolYear, int $sequence): string
|
|
{
|
|
return 'ARSS-'.$schoolYear.'-'.str_pad((string) $sequence, 4, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
private function generateVerificationToken(): string
|
|
{
|
|
if (! $this->hasVerificationTokenColumn) {
|
|
return '';
|
|
}
|
|
|
|
do {
|
|
$token = bin2hex(random_bytes(16));
|
|
$exists = DB::table('certificate_records')
|
|
->where('verification_token', $token)
|
|
->exists();
|
|
} while ($exists);
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $record
|
|
*/
|
|
private function ensureVerificationToken(array $record): string
|
|
{
|
|
if (! $this->hasVerificationTokenColumn) {
|
|
return '';
|
|
}
|
|
|
|
$token = trim((string) ($record['verification_token'] ?? ''));
|
|
if ($token !== '') {
|
|
return $token;
|
|
}
|
|
|
|
$recordId = (int) ($record['id'] ?? 0);
|
|
if ($recordId <= 0) {
|
|
return '';
|
|
}
|
|
|
|
$token = $this->generateVerificationToken();
|
|
DB::table('certificate_records')
|
|
->where('id', $recordId)
|
|
->update([
|
|
'verification_token' => $token,
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* @return array{0: string, 1: string, 2: string}
|
|
*/
|
|
private function classGroupMeta(string $name): array
|
|
{
|
|
$lower = strtolower(trim($name));
|
|
|
|
if (preg_match('/grade\s*(\d+)/i', $name, $matches) === 1) {
|
|
$grade = (int) $matches[1];
|
|
return [
|
|
'Grade '.$grade,
|
|
'1_'.str_pad((string) $grade, 3, '0', STR_PAD_LEFT),
|
|
'grade'.$grade,
|
|
];
|
|
}
|
|
|
|
if (str_contains($lower, 'kg') || str_contains($lower, 'kindergarten')) {
|
|
return ['KG', '0_kg', 'kg'];
|
|
}
|
|
|
|
if (str_contains($lower, 'arabic')) {
|
|
return ['Arabic', '2_arabic', 'arabic'];
|
|
}
|
|
|
|
if (str_contains($lower, 'youth')) {
|
|
return ['Youth', '5_youth', 'youth'];
|
|
}
|
|
|
|
return [
|
|
$name,
|
|
'3_'.$lower,
|
|
(string) preg_replace('/[^a-z0-9]+/', '-', $lower),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{0: string, 1: string}
|
|
*/
|
|
private function splitStudentName(string $studentName): array
|
|
{
|
|
$parts = preg_split('/\s+/', trim($studentName), 2) ?: [];
|
|
|
|
return [
|
|
$parts[0] ?? '',
|
|
$parts[1] ?? '',
|
|
];
|
|
}
|
|
}
|