feat: add graduation archive and public document services
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Integration;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AcademicYear;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AcademicYearSyncController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
/** @var \App\Models\SyncToken $token */
|
||||
$token = $request->attributes->get('syncToken');
|
||||
|
||||
abort_unless(collect($token->abilities ?? [])->contains('academic_years'), 403, 'Token tidak memiliki akses ke data tahun ajaran.');
|
||||
|
||||
$items = AcademicYear::query()
|
||||
->orderByDesc('is_active')
|
||||
->orderByDesc('start_year')
|
||||
->get(['id', 'name', 'start_year', 'end_year', 'is_active', 'updated_at']);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'source' => 'database-kesiswaan',
|
||||
'resource' => 'academic_years',
|
||||
'count' => $items->count(),
|
||||
'data' => $items,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Integration;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Department;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DepartmentSyncController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
/** @var \App\Models\SyncToken $token */
|
||||
$token = $request->attributes->get('syncToken');
|
||||
|
||||
abort_unless(collect($token->abilities ?? [])->contains('departments'), 403, 'Token tidak memiliki akses ke data jurusan.');
|
||||
|
||||
$items = Department::query()
|
||||
->orderBy('name')
|
||||
->get(['id', 'code', 'name', 'description', 'is_active', 'updated_at']);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'source' => 'database-kesiswaan',
|
||||
'resource' => 'departments',
|
||||
'count' => $items->count(),
|
||||
'data' => $items,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Integration;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SchoolClass;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SchoolClassSyncController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
/** @var \App\Models\SyncToken $token */
|
||||
$token = $request->attributes->get('syncToken');
|
||||
|
||||
abort_unless(collect($token->abilities ?? [])->contains('classes'), 403, 'Token tidak memiliki akses ke data kelas.');
|
||||
|
||||
$items = SchoolClass::query()
|
||||
->orderByDesc('academic_year_id')
|
||||
->orderBy('name')
|
||||
->get([
|
||||
'id',
|
||||
'academic_year_id',
|
||||
'department_id',
|
||||
'name',
|
||||
'level',
|
||||
'homeroom_teacher',
|
||||
'capacity',
|
||||
'is_active',
|
||||
'updated_at',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'source' => 'database-kesiswaan',
|
||||
'resource' => 'classes',
|
||||
'count' => $items->count(),
|
||||
'data' => $items,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Integration;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Student;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class StudentSyncController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
/** @var \App\Models\SyncToken $token */
|
||||
$token = $request->attributes->get('syncToken');
|
||||
|
||||
abort_unless(collect($token->abilities ?? [])->contains('students'), 403, 'Token tidak memiliki akses ke data siswa.');
|
||||
|
||||
$items = Student::query()
|
||||
->orderBy('full_name')
|
||||
->get([
|
||||
'id',
|
||||
'student_number',
|
||||
'nisn',
|
||||
'full_name',
|
||||
'gender',
|
||||
'academic_year_id',
|
||||
'department_id',
|
||||
'class_id',
|
||||
'status',
|
||||
'phone_number',
|
||||
'email',
|
||||
'photo_path',
|
||||
'notes',
|
||||
'updated_at',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'source' => 'database-kesiswaan',
|
||||
'resource' => 'students',
|
||||
'count' => $items->count(),
|
||||
'data' => $items,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\AcademicYear;
|
||||
use App\Models\Department;
|
||||
use App\Models\SchoolClass;
|
||||
use App\Models\StudentGraduation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
use RealRashid\SweetAlert\Facades\Alert;
|
||||
|
||||
class GraduatedStudentController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$search = trim((string) $request->get('search'));
|
||||
$academicYearId = $request->get('academic_year_id');
|
||||
$departmentId = $request->get('department_id');
|
||||
$classId = $request->get('class_id');
|
||||
$sort = (string) $request->get('sort', 'full_name');
|
||||
$direction = strtolower((string) $request->get('direction', 'asc')) === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
$allowedSorts = [
|
||||
'full_name' => 'full_name',
|
||||
'student_number' => 'student_number',
|
||||
'academic_year' => 'academic_year_id',
|
||||
'department' => 'department_id',
|
||||
'class' => 'class_id',
|
||||
'graduation_date' => 'graduation_date',
|
||||
'created_at' => 'created_at',
|
||||
];
|
||||
|
||||
if (! array_key_exists($sort, $allowedSorts)) {
|
||||
$sort = 'full_name';
|
||||
}
|
||||
|
||||
$graduations = StudentGraduation::query()
|
||||
->with(['academicYear', 'department', 'classRoom'])
|
||||
->when($search !== '', function ($query) use ($search) {
|
||||
$query->where(function ($subQuery) use ($search) {
|
||||
$subQuery->where('full_name', 'like', "%{$search}%")
|
||||
->orWhere('student_number', 'like', "%{$search}%")
|
||||
->orWhere('nisn', 'like', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->when($academicYearId, fn ($query) => $query->where('academic_year_id', $academicYearId))
|
||||
->when($departmentId, fn ($query) => $query->where('department_id', $departmentId))
|
||||
->when($classId, fn ($query) => $query->where('class_id', $classId))
|
||||
->orderBy($allowedSorts[$sort], $direction)
|
||||
->orderBy('id', $direction)
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
$summary = [
|
||||
'total' => $graduations->total(),
|
||||
'with_skl' => StudentGraduation::query()->whereNotNull('skl_file_path')->count(),
|
||||
'with_ijazah' => StudentGraduation::query()->whereNotNull('ijazah_file_path')->count(),
|
||||
];
|
||||
|
||||
return view('graduated-students.index', [
|
||||
'graduations' => $graduations,
|
||||
'search' => $search,
|
||||
'filters' => [
|
||||
'academic_year_id' => $academicYearId,
|
||||
'department_id' => $departmentId,
|
||||
'class_id' => $classId,
|
||||
],
|
||||
'academicYears' => AcademicYear::orderBy('name')->pluck('name', 'id'),
|
||||
'departments' => Department::orderBy('name')->pluck('name', 'id'),
|
||||
'classes' => SchoolClass::orderBy('name')->pluck('name', 'id'),
|
||||
'sort' => $sort,
|
||||
'direction' => $direction,
|
||||
'summary' => $summary,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(StudentGraduation $graduatedStudent): View
|
||||
{
|
||||
$graduatedStudent->load(['academicYear', 'department', 'classRoom']);
|
||||
|
||||
return view('graduated-students.show', [
|
||||
'graduation' => $graduatedStudent,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, StudentGraduation $graduatedStudent): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'graduation_date' => ['nullable', 'date'],
|
||||
'graduation_note' => ['nullable', 'string'],
|
||||
'skl_file' => ['nullable', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:4096'],
|
||||
'ijazah_file' => ['nullable', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:4096'],
|
||||
], [
|
||||
'skl_file.mimes' => 'File SKL harus berupa PDF, JPG, JPEG, atau PNG.',
|
||||
'skl_file.max' => 'Ukuran file SKL maksimal 4 MB.',
|
||||
'ijazah_file.mimes' => 'File ijazah harus berupa PDF, JPG, JPEG, atau PNG.',
|
||||
'ijazah_file.max' => 'Ukuran file ijazah maksimal 4 MB.',
|
||||
]);
|
||||
|
||||
$graduatedStudent->graduation_date = $validated['graduation_date'] ?? $graduatedStudent->graduation_date;
|
||||
$graduatedStudent->graduation_note = $validated['graduation_note'] ?? $graduatedStudent->graduation_note;
|
||||
|
||||
if ($request->hasFile('skl_file')) {
|
||||
if ($graduatedStudent->skl_file_path && Storage::disk('public')->exists($graduatedStudent->skl_file_path)) {
|
||||
Storage::disk('public')->delete($graduatedStudent->skl_file_path);
|
||||
}
|
||||
|
||||
$file = $request->file('skl_file');
|
||||
$path = $file->store("student-graduation-documents/{$graduatedStudent->student_id}/skl", 'public');
|
||||
|
||||
$graduatedStudent->skl_file_name = $file->getClientOriginalName();
|
||||
$graduatedStudent->skl_file_path = $path;
|
||||
$graduatedStudent->skl_mime_type = $file->getMimeType();
|
||||
$graduatedStudent->skl_file_size = $file->getSize();
|
||||
}
|
||||
|
||||
if ($request->hasFile('ijazah_file')) {
|
||||
if ($graduatedStudent->ijazah_file_path && Storage::disk('public')->exists($graduatedStudent->ijazah_file_path)) {
|
||||
Storage::disk('public')->delete($graduatedStudent->ijazah_file_path);
|
||||
}
|
||||
|
||||
$file = $request->file('ijazah_file');
|
||||
$path = $file->store("student-graduation-documents/{$graduatedStudent->student_id}/ijazah", 'public');
|
||||
|
||||
$graduatedStudent->ijazah_file_name = $file->getClientOriginalName();
|
||||
$graduatedStudent->ijazah_file_path = $path;
|
||||
$graduatedStudent->ijazah_mime_type = $file->getMimeType();
|
||||
$graduatedStudent->ijazah_file_size = $file->getSize();
|
||||
}
|
||||
|
||||
$graduatedStudent->save();
|
||||
|
||||
Alert::success('Berhasil', 'Arsip siswa lulus berhasil diperbarui.');
|
||||
|
||||
return redirect()->route('graduated-students.show', $graduatedStudent)->with('success', 'Arsip siswa lulus berhasil diperbarui.');
|
||||
}
|
||||
|
||||
public function previewDocument(StudentGraduation $graduatedStudent, string $document): View
|
||||
{
|
||||
abort_unless(in_array($document, ['skl', 'ijazah'], true), 404);
|
||||
|
||||
$filePathField = $document === 'skl' ? 'skl_file_path' : 'ijazah_file_path';
|
||||
$fileNameField = $document === 'skl' ? 'skl_file_name' : 'ijazah_file_name';
|
||||
$mimeTypeField = $document === 'skl' ? 'skl_mime_type' : 'ijazah_mime_type';
|
||||
|
||||
abort_unless($graduatedStudent->{$filePathField} && Storage::disk('public')->exists($graduatedStudent->{$filePathField}), 404);
|
||||
|
||||
$mimeType = $graduatedStudent->{$mimeTypeField} ?: Storage::disk('public')->mimeType($graduatedStudent->{$filePathField});
|
||||
$fileUrl = asset('storage/' . $graduatedStudent->{$filePathField});
|
||||
$isImage = Str::startsWith((string) $mimeType, 'image/');
|
||||
$isPdf = $mimeType === 'application/pdf';
|
||||
|
||||
return view('graduated-students.preview-document', [
|
||||
'graduation' => $graduatedStudent,
|
||||
'documentType' => $document,
|
||||
'documentLabel' => $document === 'skl' ? 'SKL' : 'Ijazah',
|
||||
'fileName' => $graduatedStudent->{$fileNameField},
|
||||
'fileUrl' => $fileUrl,
|
||||
'mimeType' => $mimeType,
|
||||
'isImage' => $isImage,
|
||||
'isPdf' => $isPdf,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\StudentGraduation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
use RealRashid\SweetAlert\Facades\Alert;
|
||||
|
||||
class GraduationBulkUploadController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
return view('graduated-students.bulk-upload', [
|
||||
'preview' => $request->session()->get('graduation_bulk_upload_preview'),
|
||||
'documentTypes' => [
|
||||
'skl' => 'SKL',
|
||||
'ijazah' => 'Ijazah',
|
||||
],
|
||||
'matchByOptions' => [
|
||||
'student_number' => 'NIS',
|
||||
'nisn' => 'NISN',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function preview(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'document_type' => ['required', 'in:skl,ijazah'],
|
||||
'match_by' => ['required', 'in:student_number,nisn'],
|
||||
'documents' => ['required', 'array', 'min:1', 'max:10'],
|
||||
'documents.*' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:2048'],
|
||||
], [
|
||||
'documents.required' => 'Pilih minimal satu file dokumen untuk dipreview.',
|
||||
'documents.max' => 'Maksimal 10 file per batch upload.',
|
||||
'documents.*.mimes' => 'Dokumen hanya boleh berupa PDF, JPG, JPEG, atau PNG.',
|
||||
'documents.*.max' => 'Ukuran tiap dokumen maksimal 2 MB.',
|
||||
]);
|
||||
|
||||
$graduations = StudentGraduation::query()->get()->keyBy($validated['match_by']);
|
||||
$rows = [];
|
||||
$summary = [
|
||||
'total_files' => 0,
|
||||
'matched' => 0,
|
||||
'unmatched' => 0,
|
||||
'will_replace' => 0,
|
||||
'ready_to_upload' => 0,
|
||||
];
|
||||
|
||||
foreach ($request->file('documents', []) as $file) {
|
||||
$summary['total_files']++;
|
||||
|
||||
$originalName = $file->getClientOriginalName();
|
||||
$baseName = pathinfo($originalName, PATHINFO_FILENAME);
|
||||
$normalizedKey = trim($baseName);
|
||||
$graduation = $graduations->get($normalizedKey);
|
||||
$existingPathField = $validated['document_type'] === 'skl' ? 'skl_file_path' : 'ijazah_file_path';
|
||||
|
||||
$storedTempPath = $file->store('temp/graduation-bulk-upload', 'public');
|
||||
|
||||
$row = [
|
||||
'original_name' => $originalName,
|
||||
'stored_temp_path' => $storedTempPath,
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'file_size' => $file->getSize(),
|
||||
'match_key' => $normalizedKey,
|
||||
'graduation_id' => $graduation?->id,
|
||||
'student_name' => $graduation?->full_name,
|
||||
'identifier_value' => $graduation?->{$validated['match_by']},
|
||||
'status' => $graduation ? 'matched' : 'unmatched',
|
||||
'will_replace' => $graduation ? filled($graduation->{$existingPathField}) : false,
|
||||
];
|
||||
|
||||
if ($graduation) {
|
||||
$summary['matched']++;
|
||||
$summary['ready_to_upload']++;
|
||||
|
||||
if ($row['will_replace']) {
|
||||
$summary['will_replace']++;
|
||||
}
|
||||
} else {
|
||||
$summary['unmatched']++;
|
||||
}
|
||||
|
||||
$rows[] = $row;
|
||||
}
|
||||
|
||||
$preview = [
|
||||
'token' => (string) Str::uuid(),
|
||||
'document_type' => $validated['document_type'],
|
||||
'document_label' => $validated['document_type'] === 'skl' ? 'SKL' : 'Ijazah',
|
||||
'match_by' => $validated['match_by'],
|
||||
'match_by_label' => $validated['match_by'] === 'student_number' ? 'NIS' : 'NISN',
|
||||
'rows' => $rows,
|
||||
'summary' => $summary,
|
||||
];
|
||||
|
||||
$request->session()->put('graduation_bulk_upload_preview', $preview);
|
||||
|
||||
return redirect()->route('graduated-students.bulk-upload.index');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'preview_token' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$preview = $request->session()->get('graduation_bulk_upload_preview');
|
||||
|
||||
if (! $preview || ($preview['token'] ?? null) !== $request->string('preview_token')->value()) {
|
||||
return back()->withErrors([
|
||||
'preview_token' => 'Preview bulk upload sudah tidak valid. Silakan upload ulang dokumen.',
|
||||
]);
|
||||
}
|
||||
|
||||
$documentType = $preview['document_type'];
|
||||
$fileNameField = $documentType === 'skl' ? 'skl_file_name' : 'ijazah_file_name';
|
||||
$filePathField = $documentType === 'skl' ? 'skl_file_path' : 'ijazah_file_path';
|
||||
$mimeTypeField = $documentType === 'skl' ? 'skl_mime_type' : 'ijazah_mime_type';
|
||||
$fileSizeField = $documentType === 'skl' ? 'skl_file_size' : 'ijazah_file_size';
|
||||
|
||||
$processed = 0;
|
||||
$skipped = 0;
|
||||
|
||||
DB::transaction(function () use ($preview, $documentType, $fileNameField, $filePathField, $mimeTypeField, $fileSizeField, &$processed, &$skipped) {
|
||||
foreach ($preview['rows'] as $row) {
|
||||
if (($row['status'] ?? null) !== 'matched' || empty($row['graduation_id'])) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$graduation = StudentGraduation::query()->find($row['graduation_id']);
|
||||
|
||||
if (! $graduation) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! Storage::disk('public')->exists($row['stored_temp_path'])) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($graduation->{$filePathField} && Storage::disk('public')->exists($graduation->{$filePathField})) {
|
||||
Storage::disk('public')->delete($graduation->{$filePathField});
|
||||
}
|
||||
|
||||
$extension = pathinfo($row['original_name'], PATHINFO_EXTENSION);
|
||||
$finalPath = "student-graduation-documents/{$graduation->student_id}/{$documentType}/" . Str::uuid() . ($extension ? '.' . $extension : '');
|
||||
Storage::disk('public')->move($row['stored_temp_path'], $finalPath);
|
||||
|
||||
$graduation->update([
|
||||
$fileNameField => $row['original_name'],
|
||||
$filePathField => $finalPath,
|
||||
$mimeTypeField => $row['mime_type'],
|
||||
$fileSizeField => $row['file_size'],
|
||||
]);
|
||||
|
||||
$processed++;
|
||||
}
|
||||
});
|
||||
|
||||
foreach ($preview['rows'] as $row) {
|
||||
if (! empty($row['stored_temp_path']) && Storage::disk('public')->exists($row['stored_temp_path'])) {
|
||||
Storage::disk('public')->delete($row['stored_temp_path']);
|
||||
}
|
||||
}
|
||||
|
||||
$request->session()->forget('graduation_bulk_upload_preview');
|
||||
|
||||
Alert::success('Berhasil', "Bulk upload {$preview['document_label']} selesai. {$processed} dokumen diproses, {$skipped} dilewati.");
|
||||
|
||||
return redirect()
|
||||
->route('graduated-students.bulk-upload.index')
|
||||
->with('success', "Bulk upload {$preview['document_label']} selesai. {$processed} dokumen diproses, {$skipped} dilewati.");
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,11 @@ 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
|
||||
{
|
||||
@@ -44,6 +46,9 @@ class PublicStudentLookupController extends Controller
|
||||
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) {
|
||||
@@ -136,6 +141,21 @@ class PublicStudentLookupController extends Controller
|
||||
&& (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'], true), 404);
|
||||
|
||||
$graduation = StudentGraduation::query()->where('student_id', $student->id)->firstOrFail();
|
||||
|
||||
$filePathField = $document === 'skl' ? 'skl_file_path' : 'ijazah_file_path';
|
||||
$fileNameField = $document === 'skl' ? 'skl_file_name' : 'ijazah_file_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()
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Models\Village;
|
||||
use App\Models\StudentAcademicHistory;
|
||||
use App\Models\StudentExit;
|
||||
use App\Models\StudentTransfer;
|
||||
use App\Models\StudentGraduation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
@@ -80,7 +81,7 @@ class StudentController extends Controller
|
||||
->when($academicYearId, fn ($query) => $query->where('students.academic_year_id', $academicYearId))
|
||||
->when($departmentId, fn ($query) => $query->where('students.department_id', $departmentId))
|
||||
->when($classId, fn ($query) => $query->where('students.class_id', $classId))
|
||||
->when($viewConfig['view'] === 'all' && blank($statusFilter), fn ($query) => $query->whereNotIn('students.status', ['dropped', 'transferred']))
|
||||
->when($viewConfig['view'] === 'all' && blank($statusFilter), fn ($query) => $query->where('students.status', 'active'))
|
||||
->when($viewConfig['view'] === 'all' && $statusFilter === 'active', fn ($query) => $query->where('students.status', 'active'))
|
||||
->when($viewConfig['view'] === 'all' && $statusFilter === 'non_active', fn ($query) => $query->whereIn('students.status', ['inactive', 'graduated', 'dropped', 'transferred']))
|
||||
->when($viewConfig['view'] !== 'all' && $status, fn ($query) => $query->where('students.status', $status))
|
||||
@@ -150,6 +151,29 @@ class StudentController extends Controller
|
||||
return $student;
|
||||
});
|
||||
|
||||
$summary = [
|
||||
'total' => $students->total(),
|
||||
'byAcademicYear' => Student::query()
|
||||
->select('academic_years.name', 'academic_years.start_year', DB::raw('count(*) as total'))
|
||||
->join('academic_years', 'academic_years.id', '=', 'students.academic_year_id')
|
||||
->when($search !== '', function ($query) use ($search) {
|
||||
$query->where(function ($subQuery) use ($search) {
|
||||
$subQuery->where('students.full_name', 'like', "%{$search}%")
|
||||
->orWhere('students.student_number', 'like', "%{$search}%")
|
||||
->orWhere('students.nisn', 'like', "%{$search}%")
|
||||
->orWhere('students.phone_number', 'like', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->when($academicYearId, fn ($query) => $query->where('students.academic_year_id', $academicYearId))
|
||||
->when($departmentId, fn ($query) => $query->where('students.department_id', $departmentId))
|
||||
->when($classId, fn ($query) => $query->where('students.class_id', $classId))
|
||||
->where('students.status', $status ?: 'graduated')
|
||||
->groupBy('academic_years.name', 'academic_years.start_year')
|
||||
->orderByDesc('academic_years.start_year')
|
||||
->limit(3)
|
||||
->get(),
|
||||
];
|
||||
|
||||
return view('students.index', [
|
||||
'students' => $students,
|
||||
'search' => $search,
|
||||
@@ -162,16 +186,19 @@ class StudentController extends Controller
|
||||
'academicYears' => AcademicYear::orderBy('name')->pluck('name', 'id'),
|
||||
'departments' => Department::where('is_active', true)->orderBy('name')->pluck('name', 'id'),
|
||||
'classes' => SchoolClass::orderBy('name')->pluck('name', 'id'),
|
||||
'statuses' => [
|
||||
'active' => 'Aktif',
|
||||
'non_active' => 'Non Aktif',
|
||||
],
|
||||
'statuses' => $viewConfig['view'] === 'all'
|
||||
? [
|
||||
'active' => 'Aktif',
|
||||
'non_active' => 'Non Aktif',
|
||||
]
|
||||
: [],
|
||||
'studentStatusLabels' => $this->statusOptions(),
|
||||
'sort' => $sort,
|
||||
'direction' => $direction,
|
||||
'pageTitle' => $viewConfig['title'],
|
||||
'pageDescription' => $viewConfig['description'],
|
||||
'viewMode' => $viewConfig['view'],
|
||||
'summary' => $summary,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -188,6 +215,7 @@ class StudentController extends Controller
|
||||
$this->syncAcademicHistory($student, $data);
|
||||
$this->syncExitRecord($student, $data);
|
||||
$this->syncTransferRecord($student, $data);
|
||||
$this->syncGraduationRecord($student, $data);
|
||||
|
||||
Alert::success('Berhasil', 'Data siswa berhasil ditambahkan.');
|
||||
|
||||
@@ -261,6 +289,7 @@ class StudentController extends Controller
|
||||
$this->syncAcademicHistory($student, $data);
|
||||
$this->syncExitRecord($student, $data);
|
||||
$this->syncTransferRecord($student, $data);
|
||||
$this->syncGraduationRecord($student, $data);
|
||||
|
||||
Alert::success('Berhasil', 'Data siswa berhasil diperbarui.');
|
||||
|
||||
@@ -750,6 +779,60 @@ class StudentController extends Controller
|
||||
StudentTransfer::query()->where('student_id', $student->id)->delete();
|
||||
}
|
||||
|
||||
protected function syncGraduationRecord(Student $student, array $data): void
|
||||
{
|
||||
if (($data['status'] ?? null) === 'graduated') {
|
||||
$graduationRecord = StudentGraduation::firstOrNew(['student_id' => $student->id]);
|
||||
|
||||
$graduationRecord->fill([
|
||||
'student_number' => $student->student_number,
|
||||
'nisn' => $student->nisn,
|
||||
'full_name' => $student->full_name,
|
||||
'academic_year_id' => $student->academic_year_id,
|
||||
'department_id' => $student->department_id,
|
||||
'class_id' => $student->class_id,
|
||||
'entry_date' => $student->entry_date,
|
||||
'graduation_date' => $graduationRecord->graduation_date ?? now()->toDateString(),
|
||||
'graduation_note' => $student->notes,
|
||||
'status' => 'graduated',
|
||||
]);
|
||||
|
||||
if (request()->hasFile('skl_file')) {
|
||||
if ($graduationRecord->skl_file_path && Storage::disk('public')->exists($graduationRecord->skl_file_path)) {
|
||||
Storage::disk('public')->delete($graduationRecord->skl_file_path);
|
||||
}
|
||||
|
||||
$file = request()->file('skl_file');
|
||||
$path = $file->store("student-graduation-documents/{$student->id}/skl", 'public');
|
||||
|
||||
$graduationRecord->skl_file_name = $file->getClientOriginalName();
|
||||
$graduationRecord->skl_file_path = $path;
|
||||
$graduationRecord->skl_mime_type = $file->getMimeType();
|
||||
$graduationRecord->skl_file_size = $file->getSize();
|
||||
}
|
||||
|
||||
if (request()->hasFile('ijazah_file')) {
|
||||
if ($graduationRecord->ijazah_file_path && Storage::disk('public')->exists($graduationRecord->ijazah_file_path)) {
|
||||
Storage::disk('public')->delete($graduationRecord->ijazah_file_path);
|
||||
}
|
||||
|
||||
$file = request()->file('ijazah_file');
|
||||
$path = $file->store("student-graduation-documents/{$student->id}/ijazah", 'public');
|
||||
|
||||
$graduationRecord->ijazah_file_name = $file->getClientOriginalName();
|
||||
$graduationRecord->ijazah_file_path = $path;
|
||||
$graduationRecord->ijazah_mime_type = $file->getMimeType();
|
||||
$graduationRecord->ijazah_file_size = $file->getSize();
|
||||
}
|
||||
|
||||
$graduationRecord->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
StudentGraduation::query()->where('student_id', $student->id)->delete();
|
||||
}
|
||||
|
||||
protected function importRules(): array
|
||||
{
|
||||
return [
|
||||
@@ -1114,12 +1197,6 @@ class StudentController extends Controller
|
||||
protected function resolveStudentViewConfig(string $viewMode, mixed $requestedStatus): array
|
||||
{
|
||||
return match ($viewMode) {
|
||||
'graduated' => [
|
||||
'view' => 'graduated',
|
||||
'status' => 'graduated',
|
||||
'title' => 'Data Siswa Lulus',
|
||||
'description' => 'Daftar siswa yang sudah berstatus lulus.',
|
||||
],
|
||||
'transferred' => [
|
||||
'view' => 'transferred',
|
||||
'status' => 'transferred',
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\AcademicYear;
|
||||
use App\Models\SchoolClass;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentAcademicHistory;
|
||||
use App\Models\StudentGraduation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -19,31 +20,86 @@ class StudentPromotionController extends Controller
|
||||
{
|
||||
$fromAcademicYearId = $request->integer('from_academic_year_id') ?: AcademicYear::where('is_active', true)->value('id');
|
||||
$toAcademicYearId = $request->integer('to_academic_year_id');
|
||||
$selectedClassId = $request->integer('class_id');
|
||||
|
||||
$academicYears = AcademicYear::orderByDesc('start_year')->orderByDesc('end_year')->get();
|
||||
|
||||
$availableClasses = collect();
|
||||
$classSummaries = collect();
|
||||
$selectedClass = null;
|
||||
$students = collect();
|
||||
|
||||
if ($fromAcademicYearId) {
|
||||
$students = Student::query()
|
||||
->with(['academicYear', 'department', 'classRoom'])
|
||||
$availableClasses = SchoolClass::query()
|
||||
->with(['department'])
|
||||
->where('academic_year_id', $fromAcademicYearId)
|
||||
->where('status', 'active')
|
||||
->orderBy('full_name')
|
||||
->where('is_active', true)
|
||||
->withCount([
|
||||
'students as active_students_count' => fn ($query) => $query->where('status', 'active'),
|
||||
])
|
||||
->orderBy('level')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(function (Student $student) use ($toAcademicYearId) {
|
||||
$student->promotion_recommendation = $this->buildPromotionRecommendation($student, $toAcademicYearId);
|
||||
->filter(fn (SchoolClass $classRoom) => $classRoom->active_students_count > 0)
|
||||
->values();
|
||||
|
||||
return $student;
|
||||
});
|
||||
$classSummaries = $availableClasses->map(function (SchoolClass $classRoom) use ($toAcademicYearId) {
|
||||
$sampleStudent = Student::query()
|
||||
->with(['classRoom'])
|
||||
->where('academic_year_id', $classRoom->academic_year_id)
|
||||
->where('class_id', $classRoom->id)
|
||||
->where('status', 'active')
|
||||
->orderBy('full_name')
|
||||
->first();
|
||||
|
||||
$recommendation = $sampleStudent
|
||||
? $this->buildPromotionRecommendation($sampleStudent, $toAcademicYearId)
|
||||
: [
|
||||
'label' => 'Belum ada siswa aktif',
|
||||
'target_class_name' => null,
|
||||
];
|
||||
|
||||
return (object) [
|
||||
'id' => $classRoom->id,
|
||||
'name' => $classRoom->name,
|
||||
'level' => $classRoom->level,
|
||||
'department_name' => $classRoom->department?->name,
|
||||
'active_students_count' => $classRoom->active_students_count,
|
||||
'recommendation_label' => $recommendation['label'] ?? '-',
|
||||
'recommendation_target_name' => $recommendation['target_class_name'] ?? null,
|
||||
];
|
||||
})->values();
|
||||
|
||||
if ($selectedClassId) {
|
||||
$selectedClass = $availableClasses->firstWhere('id', $selectedClassId);
|
||||
|
||||
if ($selectedClass) {
|
||||
$students = Student::query()
|
||||
->with(['academicYear', 'department', 'classRoom'])
|
||||
->where('academic_year_id', $fromAcademicYearId)
|
||||
->where('class_id', $selectedClassId)
|
||||
->where('status', 'active')
|
||||
->orderBy('full_name')
|
||||
->get()
|
||||
->map(function (Student $student) use ($toAcademicYearId) {
|
||||
$student->promotion_recommendation = $this->buildPromotionRecommendation($student, $toAcademicYearId);
|
||||
|
||||
return $student;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view('student-promotions.index', [
|
||||
'academicYears' => $academicYears,
|
||||
'availableClasses' => $availableClasses,
|
||||
'classSummaries' => $classSummaries,
|
||||
'selectedClass' => $selectedClass,
|
||||
'students' => $students,
|
||||
'filters' => [
|
||||
'from_academic_year_id' => $fromAcademicYearId,
|
||||
'to_academic_year_id' => $toAcademicYearId,
|
||||
'class_id' => $selectedClassId,
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -53,6 +109,7 @@ class StudentPromotionController extends Controller
|
||||
$data = $request->validate([
|
||||
'from_academic_year_id' => ['required', 'exists:academic_years,id'],
|
||||
'to_academic_year_id' => ['required', 'exists:academic_years,id', 'different:from_academic_year_id'],
|
||||
'class_id' => ['nullable', 'exists:classes,id'],
|
||||
'promotions' => ['required', 'array', 'min:1'],
|
||||
'promotions.*.student_id' => ['required', 'exists:students,id'],
|
||||
'promotions.*.target_class_id' => ['nullable', 'exists:classes,id'],
|
||||
@@ -102,6 +159,12 @@ class StudentPromotionController extends Controller
|
||||
->withInput();
|
||||
}
|
||||
|
||||
if (! empty($data['class_id']) && (int) $student->class_id !== (int) $data['class_id']) {
|
||||
return back()
|
||||
->withErrors(["promotions.$index.student_id" => "Siswa {$student->full_name} tidak berasal dari kelas asal yang dipilih."])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
if ($student->status !== 'active') {
|
||||
return back()
|
||||
->withErrors(["promotions.$index.student_id" => "Siswa {$student->full_name} tidak berstatus aktif sehingga tidak bisa diproses promosi."])
|
||||
@@ -200,6 +263,22 @@ class StudentPromotionController extends Controller
|
||||
'notes' => $promotion['notes'] ?? 'Diproses melalui modul promosi siswa.',
|
||||
]);
|
||||
|
||||
StudentGraduation::updateOrCreate(
|
||||
['student_id' => $student->id],
|
||||
[
|
||||
'student_number' => $student->student_number,
|
||||
'nisn' => $student->nisn,
|
||||
'full_name' => $student->full_name,
|
||||
'academic_year_id' => $student->academic_year_id,
|
||||
'department_id' => $student->department_id,
|
||||
'class_id' => $student->class_id,
|
||||
'entry_date' => $student->entry_date,
|
||||
'graduation_date' => now()->toDateString(),
|
||||
'graduation_note' => $promotion['notes'] ?? 'Diproses melalui modul promosi siswa.',
|
||||
'status' => 'graduated',
|
||||
]
|
||||
);
|
||||
|
||||
$processed++;
|
||||
continue;
|
||||
}
|
||||
@@ -245,6 +324,7 @@ class StudentPromotionController extends Controller
|
||||
->route('student-promotions.index', [
|
||||
'from_academic_year_id' => $data['from_academic_year_id'],
|
||||
'to_academic_year_id' => $data['to_academic_year_id'],
|
||||
'class_id' => $data['class_id'] ?? null,
|
||||
])
|
||||
->with('success', $processed . ' siswa berhasil diproses melalui modul promosi.');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\SyncToken;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SyncTokenController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
return view('settings.sync-tokens.index', [
|
||||
'tokens' => SyncToken::query()->with('creator')->latest()->get(),
|
||||
'plainToken' => $request->session()->get('sync_token_plain'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'abilities' => ['nullable', 'array'],
|
||||
'abilities.*' => ['string', 'max:100'],
|
||||
]);
|
||||
|
||||
$plainToken = 'dksync_' . Str::random(48);
|
||||
|
||||
SyncToken::query()->create([
|
||||
'name' => $data['name'],
|
||||
'token_hash' => Hash::make($plainToken),
|
||||
'abilities' => $data['abilities'] ?? ['academic_years', 'departments', 'classes', 'students'],
|
||||
'created_by' => $request->user()->id,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('settings.sync-tokens.index')
|
||||
->with('status', 'Token sinkronisasi berhasil dibuat.')
|
||||
->with('sync_token_plain', $plainToken);
|
||||
}
|
||||
|
||||
public function revoke(SyncToken $syncToken): RedirectResponse
|
||||
{
|
||||
if (! $syncToken->revoked_at) {
|
||||
$syncToken->update([
|
||||
'revoked_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('settings.sync-tokens.index')
|
||||
->with('status', 'Token sinkronisasi berhasil dinonaktifkan.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user