Files
database-kesiswaan/app/Http/Controllers/PublicStudentLookupController.php
T

228 lines
8.4 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\DocumentType;
use App\Models\Student;
use App\Models\StudentDocument;
use App\Models\StudentGraduation;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;
class PublicStudentLookupController extends Controller
{
public function index(Request $request): View
{
$query = trim((string) $request->get('q'));
$students = collect();
$selectedStudent = null;
$requiredDocumentSummary = null;
$matchedBy = null;
$selectedStudentId = $request->integer('student');
if ($query !== '') {
$students = Student::query()
->with([
'academicYear',
'department',
'classRoom',
'documents.documentType',
'achievements',
])
->where(function ($builder) use ($query) {
$builder->where('student_number', 'like', "%{$query}%")
->orWhere('nisn', 'like', "%{$query}%")
->orWhere('full_name', 'like', "%{$query}%");
})
->orderByRaw('CASE WHEN student_number = ? THEN 0 WHEN nisn = ? THEN 1 WHEN full_name = ? THEN 2 ELSE 3 END', [$query, $query, $query])
->orderBy('full_name')
->limit(10)
->get();
$selectedStudent = $students->firstWhere('id', $selectedStudentId) ?? $students->first();
if ($selectedStudent) {
$requiredDocumentSummary = $this->buildRequiredDocumentSummary($selectedStudent);
$matchedBy = $this->detectMatchedBy($selectedStudent, $query);
$selectedStudent->public_graduation_record = $selectedStudent->status === 'graduated'
? StudentGraduation::query()->where('student_id', $selectedStudent->id)->first()
: null;
}
$students->each(function (Student $student) {
$student->public_photo_url = $this->resolvePhotoUrl($student);
});
}
return view('public.student-lookup', [
'query' => $query,
'students' => $students,
'student' => $selectedStudent,
'requiredDocumentSummary' => $requiredDocumentSummary,
'matchedBy' => $matchedBy,
'selectedStudentId' => $selectedStudent?->id,
]);
}
protected function detectMatchedBy(Student $student, string $query): ?string
{
if ($student->student_number && strcasecmp($student->student_number, $query) === 0) {
return 'NIS';
}
if ($student->nisn && strcasecmp($student->nisn, $query) === 0) {
return 'NISN';
}
return 'Nama';
}
protected function resolvePhotoUrl(Student $student): ?string
{
$photoDocument = $student->documents
->filter(fn ($document) => $this->isPhotoDocument($document))
->sortByDesc(function ($document) {
return sprintf(
'%d-%s',
$document->verification_status === 'valid' ? 1 : 0,
optional($document->created_at)?->format('YmdHis') ?? '00000000000000'
);
})
->first();
if (! $photoDocument && $student->relationLoaded('documents') === false) {
$photoDocument = StudentDocument::query()
->with('documentType')
->where('student_id', $student->id)
->get()
->filter(fn ($document) => $this->isPhotoDocument($document))
->sortByDesc(function ($document) {
return sprintf(
'%d-%s',
$document->verification_status === 'valid' ? 1 : 0,
optional($document->created_at)?->format('YmdHis') ?? '00000000000000'
);
})
->first();
}
if (! $photoDocument?->file_path) {
return null;
}
if (! Storage::disk('public')->exists($photoDocument->file_path)) {
return null;
}
return asset('storage/' . ltrim($photoDocument->file_path, '/'));
}
protected function isPhotoDocument(StudentDocument $document): bool
{
$code = strtoupper((string) ($document->documentType?->code ?? ''));
$name = strtolower((string) ($document->documentType?->name ?? ''));
$fileName = strtolower((string) ($document->file_name ?? ''));
$mimeType = strtolower((string) ($document->mime_type ?? ''));
$looksLikePhoto = $code === 'PAS_FOTO'
|| $code === 'PASFOTO'
|| str_contains($name, 'pas foto')
|| str_contains($name, 'pasfoto')
|| str_contains($fileName, 'pas foto')
|| str_contains($fileName, 'pasfoto');
if ($looksLikePhoto) {
return true;
}
return str_starts_with($mimeType, 'image/')
&& (str_contains($name, 'foto') || str_contains($fileName, 'foto'));
}
public function downloadGraduationDocument(Student $student, string $document): StreamedResponse
{
abort_unless($student->status === 'graduated', 404);
abort_unless(in_array($document, ['skl', 'ijazah', 'transcript', 'ukk-certificate', 'lsp-certificate'], true), 404);
$graduation = StudentGraduation::query()->where('student_id', $student->id)->firstOrFail();
$documentConfig = [
'skl' => ['path' => 'skl_file_path', 'name' => 'skl_file_name'],
'ijazah' => ['path' => 'ijazah_file_path', 'name' => 'ijazah_file_name'],
'transcript' => ['path' => 'transcript_file_path', 'name' => 'transcript_file_name'],
'ukk-certificate' => ['path' => 'ukk_certificate_file_path', 'name' => 'ukk_certificate_file_name'],
'lsp-certificate' => ['path' => 'lsp_certificate_file_path', 'name' => 'lsp_certificate_file_name'],
][$document];
$filePathField = $documentConfig['path'];
$fileNameField = $documentConfig['name'];
abort_unless($graduation->{$filePathField} && Storage::disk('public')->exists($graduation->{$filePathField}), 404);
return Storage::disk('public')->download($graduation->{$filePathField}, $graduation->{$fileNameField} ?: basename($graduation->{$filePathField}));
}
protected function buildRequiredDocumentSummary(Student $student): array
{
$requiredTypes = DocumentType::query()
->where('is_active', true)
->where('is_required', true)
->get();
if ($requiredTypes->isEmpty()) {
return [
'total' => 0,
'uploaded' => 0,
'verified' => 0,
'missing' => 0,
'unverified' => 0,
'percentage' => 100,
'verification_percentage' => 100,
'missing_items' => collect(),
'unverified_items' => collect(),
];
}
$documents = $student->documents;
$uploaded = 0;
$verified = 0;
$missingItems = collect();
$unverifiedItems = collect();
foreach ($requiredTypes as $type) {
$matchingDocuments = $documents->where('document_type_id', $type->id);
$latestDocument = $matchingDocuments->sortByDesc('created_at')->first();
if (! $latestDocument) {
$missingItems->push($type->name);
continue;
}
$uploaded++;
if ($latestDocument->verification_status === 'valid') {
$verified++;
} else {
$unverifiedItems->push($type->name);
}
}
$total = $requiredTypes->count();
return [
'total' => $total,
'uploaded' => $uploaded,
'verified' => $verified,
'missing' => max($total - $uploaded, 0),
'unverified' => max($total - $verified, 0),
'percentage' => $total > 0 ? (int) round(($uploaded / $total) * 100) : 100,
'verification_percentage' => $total > 0 ? (int) round(($verified / $total) * 100) : 100,
'missing_items' => $missingItems,
'unverified_items' => $unverifiedItems,
];
}
}