refactor: migrate hardcoded UI labels to localization keys and improve submission and attendance data mapping logic

This commit is contained in:
Yoga Pangestu 2026-04-06 15:52:00 +07:00
parent 6755e740a2
commit 7c252bd41b
6 changed files with 170 additions and 61 deletions

View File

@ -10,16 +10,16 @@ class HomeController extends Controller
public function index(): View
{
$headings = [
'hero_title' => SystemNotification::getMessage('Kuliah Jadi Makin Simpel! 🚀📚', 'Sistem Manajemen Kelas'),
'hero_subtitle' => SystemNotification::getMessage('Gak Ribet Lagi.', 'Sederhana & Terintegrasi.'),
'feature_session' => SystemNotification::getMessage('Info Sesi Kuliah 🏢📅', 'Manajemen Sesi'),
'feature_session_desc' => SystemNotification::getMessage('Pantau jadwal kelasmu biar nggak ketinggalan info penting ya! 🕒✨', 'Pemantauan kelas dan penjadwalan terstruktur dan mudah dilihat.'),
'feature_task' => SystemNotification::getMessage('Tugas & Materi 📝📚', 'Tugas & Materi'),
'feature_task_desc' => SystemNotification::getMessage('Ambil materi dan kumpulin tugas dalam satu tempat, praktis banget! 🚀📂', 'Pengelolaan repositori perkuliahan dan pengumpulan tugas.'),
'feature_attendance' => SystemNotification::getMessage('Presensi Digital 🙋‍♂️✅', 'Presensi Digital'),
'feature_attendance_desc' => SystemNotification::getMessage('Absen makin gampang, tinggal klik langsung tercatat hadir. Gapake lama! 🔥⚡', 'Sistem pendataan kehadiran yang praktis, cepat, dan real-time.'),
'feature_group' => SystemNotification::getMessage('Grup Belajar 👯‍♀️💬', 'Grup Belajar'),
'feature_group_desc' => SystemNotification::getMessage('Bikin kelompok biar belajar bareng temen makin asik dan seru! 🌈✨', 'Ruang kolaborasi, diskusi, dan obrolan untuk saling terhubung.'),
'hero_title' => SystemNotification::getByKey('labels.home.hero_title'),
'hero_subtitle' => SystemNotification::getByKey('labels.home.hero_subtitle'),
'feature_session' => SystemNotification::getByKey('labels.home.feature_session'),
'feature_session_desc' => SystemNotification::getByKey('labels.home.feature_session_desc'),
'feature_task' => SystemNotification::getByKey('labels.home.feature_task'),
'feature_task_desc' => SystemNotification::getByKey('labels.home.feature_task_desc'),
'feature_attendance' => SystemNotification::getByKey('labels.home.feature_attendance'),
'feature_attendance_desc' => SystemNotification::getByKey('labels.home.feature_attendance_desc'),
'feature_group' => SystemNotification::getByKey('labels.home.feature_group'),
'feature_group_desc' => SystemNotification::getByKey('labels.home.feature_group_desc'),
];
return view('welcome', compact('headings'));

View File

@ -42,21 +42,40 @@ public function show(Request $request, Course $course): View
if ($assignment) {
$isGroup = $assignment->type === AssignmentType::Group;
$submissions = AssignmentSubmission::with(['student', 'media', 'studyGroup.students'])
$allSubmissions = AssignmentSubmission::with(['student', 'media', 'studyGroup.students'])
->where('assignment_id', $assignment->id)
->get();
if ($isGroup) {
$totalTargets = $assignment->studyGroups()->count();
$targets = $assignment->studyGroups()->with('students')->get();
$submissionMap = $allSubmissions->keyBy('study_group_id');
$submissions = $targets->map(fn ($group) => (object) [
'studyGroup' => $group,
'student' => $submissionMap->get($group->id)?->student,
'submitted_at' => $submissionMap->get($group->id)?->submitted_at,
'has_submitted' => $submissionMap->has($group->id),
'submission' => $submissionMap->get($group->id),
]);
$totalTargets = $targets->count();
} else {
$totalTargets = $assignment->students()->count();
if ($totalTargets === 0) {
$totalTargets = Student::whereHas('user', fn ($q) => $q->active())->count();
$targets = $assignment->students()->orderBy('full_name')->get();
if ($targets->isEmpty()) {
$targets = Student::whereHas('user', fn ($q) => $q->active())->orderBy('full_name')->get();
}
$submissionMap = $allSubmissions->keyBy('student_id');
$submissions = $targets->map(fn ($student) => (object) [
'student' => $student,
'submitted_at' => $submissionMap->get($student->id)?->submitted_at,
'has_submitted' => $submissionMap->has($student->id),
'submission' => $submissionMap->get($student->id),
]);
$totalTargets = $targets->count();
}
}
$submittedCount = $submissions->count();
$submittedCount = $allSubmissions ? $allSubmissions->unique($isGroup ? 'study_group_id' : 'student_id')->count() : 0;
$submissionPercentage = $totalTargets > 0 ? round(($submittedCount / $totalTargets) * 100) : 0;
$formattedDeadline = $assignment && $assignment->due_date
@ -64,8 +83,8 @@ public function show(Request $request, Course $course): View
: '-';
$headings = [
'list' => SystemNotification::getMessage('Daftar Pengumpulan 🚀✨', 'Daftar Pengumpulan'),
'info' => SystemNotification::getMessage('Informasi Tugas 📚✨', 'Informasi Tugas'),
'list' => SystemNotification::getByKey('labels.submission_list.title'),
'info' => SystemNotification::getByKey('labels.assignment_info_heading.title'),
];
return view('pages.share-assignment', compact(

View File

@ -26,16 +26,24 @@ public function show(Request $request, Course $course): View
$defaultDate = $latestAttendance ? $latestAttendance->date?->toDateString() : now()->toDateString();
$date = $request->query('date', $defaultDate);
$attendances = Attendance::with('student')
->whereHas('courseSchedule', function ($query) use ($course) {
$query->where('course_id', $course->id);
})
->whereDate('date', $date)
$activeStudents = Student::whereHas('user', fn ($q) => $q->active())
->orderBy('full_name')
->get();
$availableDates = Attendance::whereHas('courseSchedule', function ($query) use ($course) {
$attendanceRecords = Attendance::whereHas('courseSchedule', function ($query) use ($course) {
$query->where('course_id', $course->id);
})
->whereDate('date', $date)
->get()
->keyBy('student_id');
$attendances = $activeStudents->map(fn ($student) => (object) [
'student' => $student,
'attended_at' => $attendanceRecords->get($student->id)?->attended_at,
'has_attended' => $attendanceRecords->has($student->id),
]);
$availableDates = ClassSession::where('course_id', $course->id)
->select('date')
->distinct()
->orderByDesc('date')
@ -48,17 +56,17 @@ public function show(Request $request, Course $course): View
->get();
$sessionInfo = ClassSession::where('course_id', $course->id)->whereDate('date', $date)->first();
$totalStudents = Student::whereHas('user', fn ($q) => $q->active())->count();
$presentCount = $attendances->count();
$totalStudents = $activeStudents->count();
$presentCount = $attendanceRecords->count();
$attendancePercentage = $totalStudents > 0 ? round(($presentCount / $totalStudents) * 100) : 0;
$formattedTime = $sessionInfo
? Carbon::parse($sessionInfo->start_time)->format('H:i').' - '.Carbon::parse($sessionInfo->end_time)->format('H:i')
? Carbon::parse($sessionInfo->start_time)->translatedFormat('H:i').' - '.Carbon::parse($sessionInfo->end_time)->format('H:i')
: '-';
$headings = [
'list' => SystemNotification::getMessage('Daftar Kehadiran 📝✨', 'Daftar Kehadiran'),
'info' => SystemNotification::getMessage('Informasi Sesi 🏢✨', 'Informasi Sesi'),
'list' => SystemNotification::getByKey('labels.attendance_list.title'),
'info' => SystemNotification::getByKey('labels.session_info.title'),
];
return view('pages.share-attendance', compact(

View File

@ -247,6 +247,30 @@
'title' => 'Tugas Kelompok',
'description' => 'Hanya <strong>Ketua Kelompok</strong> yang dapat mengumpulkan atau memperbarui tugas ini. Silakan hubungi ketua kelompok Anda.',
],
'attendance_list' => [
'title' => 'Daftar Kehadiran 📝✨',
],
'session_info' => [
'title' => 'Informasi Sesi 🏢✨',
],
'submission_list' => [
'title' => 'Daftar Pengumpulan 🚀✨',
],
'assignment_info_heading' => [
'title' => 'Informasi Tugas 📚✨',
],
'home' => [
'hero_title' => 'Kuliah Jadi Makin Simpel! 🚀📚',
'hero_subtitle' => 'Gak Ribet Lagi.',
'feature_session' => 'Info Sesi Kuliah 🏢📅',
'feature_session_desc' => 'Pantau jadwal kelasmu biar nggak ketinggalan info penting ya! 🕒✨',
'feature_task' => 'Tugas & Materi 📝📚',
'feature_task_desc' => 'Ambil materi dan kumpulin tugas dalam satu tempat, praktis banget! 🚀📂',
'feature_attendance' => 'Presensi Digital 🙋‍♂️✅',
'feature_attendance_desc' => 'Absen makin gampang, tinggal klik langsung tercatat hadir. Gapake lama! 🔥⚡',
'feature_group' => 'Grup Belajar 👯‍♀️💬',
'feature_group_desc' => 'Bikin kelompok biar belajar bareng temen makin asik dan seru! 🌈✨',
],
],
// Icons
@ -511,6 +535,30 @@
'title' => 'Tugas Kelompok',
'description' => 'Hanya <strong>Ketua Kelompok</strong> yang memiliki otoritas untuk memperbarui atau mengumpulkan tugas kelompok.',
],
'attendance_list' => [
'title' => 'Daftar Kehadiran',
],
'session_info' => [
'title' => 'Informasi Sesi',
],
'submission_list' => [
'title' => 'Daftar Pengumpulan',
],
'assignment_info_heading' => [
'title' => 'Informasi Tugas',
],
'home' => [
'hero_title' => 'Sistem Manajemen Kelas',
'hero_subtitle' => 'Sederhana & Terintegrasi.',
'feature_session' => 'Manajemen Sesi',
'feature_session_desc' => 'Pemantauan kelas dan penjadwalan terstruktur dan mudah dilihat.',
'feature_task' => 'Tugas & Materi',
'feature_task_desc' => 'Pengelolaan repositori perkuliahan dan pengumpulan tugas.',
'feature_attendance' => 'Presensi Digital',
'feature_attendance_desc' => 'Sistem pendataan kehadiran yang praktis, cepat, dan real-time.',
'feature_group' => 'Grup Belajar',
'feature_group_desc' => 'Ruang kolaborasi, diskusi, dan obrolan untuk saling terhubung.',
],
],
// Icons

View File

@ -105,8 +105,8 @@ class="fi-card rounded-xl border border-gray-200 dark:border-gray-700 bg-white d
class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-5 py-4 font-semibold text-gray-950 dark:text-white">{{ $isGroup ? 'Kelompok' : 'Mahasiswa' }}</th>
<th class="px-5 py-4 font-semibold text-gray-950 dark:text-white">{{ $isGroup ? 'Anggota' : 'Nomor Induk' }}
</th>
<th class="px-5 py-4 font-semibold text-gray-950 dark:text-white">{{ $isGroup ? 'Anggota' : 'NIM' }}</th>
<th class="px-5 py-4 font-semibold text-gray-950 dark:text-white text-center">Status</th>
<th class="px-5 py-4 font-semibold text-gray-950 dark:text-white">Hasil Tugas
</th>
<th class="px-5 py-4 font-semibold text-gray-950 dark:text-white text-right">
@ -114,24 +114,26 @@ class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
@forelse($submissions as $submission)
@forelse($submissions as $item)
<tr class="hover:bg-gray-50/50 dark:hover:bg-gray-800/50 transition-colors">
<td class="px-5 py-4">
<div class="flex items-center gap-3">
<div class="flex flex-col gap-0.5">
@if($isGroup && $submission->studyGroup)
<span class="font-bold text-gray-950 dark:text-white text-sm">{{ $submission->studyGroup->name }}</span>
<span class="text-[10px] text-gray-500 dark:text-gray-400">Pengumpul: {{ $submission->student->full_name }}</span>
@if($isGroup && $item->studyGroup)
<span class="font-bold text-gray-950 dark:text-white text-sm">{{ $item->studyGroup->name }}</span>
@if($item->has_submitted)
<span class="text-[10px] text-gray-500 dark:text-gray-400">Pengumpul: {{ $item->student->full_name }}</span>
@endif
@else
<span class="font-bold text-gray-950 dark:text-white text-sm">{{ $submission->student->full_name }}</span>
<span class="font-bold text-gray-950 dark:text-white text-sm">{{ $item->student->full_name }}</span>
@endif
</div>
</div>
</td>
<td class="px-5 py-4">
@if($isGroup && $submission->studyGroup)
@if($isGroup && $item->studyGroup)
<div class="flex flex-wrap gap-1 max-w-[200px]">
@foreach($submission->studyGroup->students as $member)
@foreach($item->studyGroup->students as $member)
<span class="inline-flex items-center rounded-md bg-gray-50 dark:bg-gray-800/50 px-1.5 py-0.5 text-[10px] font-medium text-gray-600 dark:text-gray-400 ring-1 ring-inset ring-gray-500/10 whitespace-nowrap">
{{ $member->full_name }}
</span>
@ -139,17 +141,30 @@ class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800
</div>
@else
<span class="text-gray-500 dark:text-gray-400 font-medium text-sm">
{{ $submission->student->student_number }}
{{ $item->student->student_number }}
</span>
@endif
</td>
<td class="px-5 py-4 text-center">
@if ($item->has_submitted)
<span
class="inline-flex items-center rounded-md bg-emerald-50 dark:bg-emerald-500/10 px-2 py-1 text-[10px] font-bold text-emerald-700 dark:text-emerald-400 ring-1 ring-inset ring-emerald-600/20 uppercase tracking-widest">
Sudah Mengumpulkan
</span>
@else
<span
class="inline-flex items-center rounded-md bg-rose-50 dark:bg-rose-500/10 px-2 py-1 text-[10px] font-bold text-rose-700 dark:text-rose-400 ring-1 ring-inset ring-rose-600/20 uppercase tracking-widest">
Belum Mengumpulkan
</span>
@endif
</td>
<td class="px-5 py-4">
<div class="flex flex-col gap-2 max-w-sm">
@if ($submission->getMedia('submission')->count() > 0)
@if ($item->has_submitted && $item->submission && $item->submission->getMedia('submission')->count() > 0)
<div class="flex flex-wrap gap-1.5 mt-1">
@foreach ($submission->getMedia('submission') as $media)
<button type="button" onclick="openPdfModal('{{ $media->getUrl() }}', '{{ addslashes($submission->student->full_name) }}')"
@foreach ($item->submission->getMedia('submission') as $media)
<button type="button" onclick="openPdfModal('{{ $media->getUrl() }}', '{{ addslashes($item->student->full_name ?? ($isGroup ? $item->studyGroup->name : 'Mahasiswa')) }}')"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-primary-50 dark:bg-primary-500/10 hover:bg-primary-100 dark:hover:bg-primary-500/20 text-primary-600 dark:text-primary-400 border border-primary-200 dark:border-primary-500/20 text-[10px] font-bold uppercase tracking-widest transition-colors decoration-transparent shrink-0 focus:outline-none">
<x-heroicon-m-document-arrow-down
class="w-4 h-4 shrink-0" />
@ -160,8 +175,8 @@ class="truncate max-w-[150px]">{{ $media->file_name }}</span>
</div>
@else
<span
class="text-[10px] text-danger-500 uppercase tracking-widest font-bold italic mt-1">File
tidak ditemukan</span>
class="text-[10px] text-gray-400 uppercase tracking-widest font-bold italic mt-1">Tidak
ada file</span>
@endif
</div>
</td>
@ -170,14 +185,18 @@ class="text-[10px] text-danger-500 uppercase tracking-widest font-bold italic mt
$isLate =
$assignment &&
$assignment->due_date &&
$submission->submitted_at > $assignment->due_date;
$item->submitted_at > $assignment->due_date;
@endphp
<div
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg {{ $isLate ? 'bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400' : 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400' }} font-bold text-[10px] uppercase tracking-widest">
<span
class="w-1.5 h-1.5 rounded-full {{ $isLate ? 'bg-amber-500' : 'bg-emerald-500' }}"></span>
{{ $submission->submitted_at ? $submission->submitted_at->format('d M Y H:i') : '-' }}
</div>
@if($item->has_submitted)
<div
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg {{ $isLate ? 'bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400' : 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400' }} font-bold text-[10px] uppercase tracking-widest">
<span
class="w-1.5 h-1.5 rounded-full {{ $isLate ? 'bg-amber-500' : 'bg-emerald-500' }}"></span>
{{ $item->submitted_at ? $item->submitted_at->format('d M Y H:i') : '-' }}
</div>
@else
<span class="text-gray-400 dark:text-gray-600 font-bold text-[10px] uppercase tracking-widest">-</span>
@endif
</td>
</tr>
@empty
@ -278,7 +297,7 @@ class="text-[10px] font-bold text-gray-400 dark:text-gray-500 uppercase tracking
<div class="fixed inset-0 z-10 w-screen overflow-y-auto pointer-events-none">
<div class="flex min-h-full items-center justify-center p-4 sm:p-6">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 shadow-2xl w-full max-w-5xl h-[85vh] flex flex-col pointer-events-auto">
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-900/50">
<h3 class="text-sm font-bold text-gray-900 dark:text-white truncate pr-4" id="modal-title">Title</h3>
<button type="button" onclick="closePdfModal()" class="rounded-full bg-white dark:bg-gray-800 p-2 text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 ring-1 ring-inset ring-gray-200 dark:ring-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition focus:outline-none">
@ -286,7 +305,7 @@ class="text-[10px] font-bold text-gray-400 dark:text-gray-500 uppercase tracking
<x-heroicon-m-x-mark class="w-4 h-4" />
</button>
</div>
<div class="flex-1 bg-gray-100 dark:bg-gray-950 p-2 sm:p-4">
<iframe id="pdf-iframe" src="" class="w-full h-full rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-inner" frameborder="0" allowfullscreen></iframe>
</div>

View File

@ -96,9 +96,6 @@ class="inline-flex items-center rounded-md bg-zinc-100 dark:bg-zinc-800 px-2 py-
<h2 class="text-base font-bold text-gray-950 dark:text-white tracking-tight uppercase">
{{ $headings['list'] }}
</h2>
<span class="text-[11px] font-black text-gray-400 dark:text-gray-500 uppercase tracking-widest">
{{ $attendances->count() }} Terdata
</span>
</div>
<div
@ -111,6 +108,7 @@ class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800
<th class="px-5 py-4 font-semibold text-gray-950 dark:text-white">Mahasiswa</th>
<th class="px-5 py-4 font-semibold text-gray-950 dark:text-white">NIM
</th>
<th class="px-5 py-4 font-semibold text-gray-950 dark:text-white text-center">Status</th>
<th class="px-5 py-4 font-semibold text-gray-950 dark:text-white text-right">
Waktu Presensi</th>
</tr>
@ -131,12 +129,29 @@ class="font-bold text-gray-950 dark:text-white text-sm">{{ $attendance->student-
{{ $attendance->student->student_number }}
</span>
</td>
<td class="px-5 py-4 text-center">
@if ($attendance->has_attended)
<span
class="inline-flex items-center rounded-md bg-emerald-50 dark:bg-emerald-500/10 px-2 py-1 text-[10px] font-bold text-emerald-700 dark:text-emerald-400 ring-1 ring-inset ring-emerald-600/20 uppercase tracking-widest">
Hadir
</span>
@else
<span
class="inline-flex items-center rounded-md bg-rose-50 dark:bg-rose-500/10 px-2 py-1 text-[10px] font-bold text-rose-700 dark:text-rose-400 ring-1 ring-inset ring-rose-600/20 uppercase tracking-widest">
Alpa
</span>
@endif
</td>
<td class="px-5 py-4 text-right">
<div
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 font-bold text-[10px] uppercase tracking-widest">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
{{ $attendance->attended_at ? $attendance->attended_at->format('H:i') : '-' }}
</div>
@if ($attendance->has_attended)
<div
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 font-bold text-[10px] uppercase tracking-widest">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
{{ $attendance->attended_at ? $attendance->attended_at->translatedFormat('d F Y H:i') : '-' }}
</div>
@else
<span class="text-gray-400 dark:text-gray-600 font-bold text-[10px] uppercase tracking-widest">-</span>
@endif
</td>
</tr>
@empty