From daff18996f38a73393f689979ecc00fcf0b68d83 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Tue, 25 Aug 2026 13:30:02 +0700 Subject: [PATCH] feat: add academic advising log management with CRUD operations and UI integration --- .../Manage/AcademicAdvisingLogController.php | 57 +++ .../Manage/AcademicAdvisingLogRequest.php | 25 ++ app/Models/AcademicAdvisingLog.php | 31 ++ app/Models/Lecturer.php | 5 + app/Models/Student.php | 5 + .../Manage/AcademicAdvisingLogService.php | 53 +++ ...05_create_academic_advising_logs_table.php | 26 ++ .../seeders/AcademicAdvisingLogSeeder.php | 33 ++ database/seeders/DatabaseSeeder.php | 1 + resources/js/components/app-sidebar.tsx | 7 + .../manage/academic-advising-logs/columns.tsx | 100 +++++ .../manage/academic-advising-logs/index.tsx | 346 ++++++++++++++++++ resources/js/types/academic-advising-log.ts | 25 ++ routes/admin.php | 3 + 14 files changed, 717 insertions(+) create mode 100644 app/Http/Controllers/Admin/Manage/AcademicAdvisingLogController.php create mode 100644 app/Http/Requests/Admin/Manage/AcademicAdvisingLogRequest.php create mode 100644 app/Models/AcademicAdvisingLog.php create mode 100644 app/Services/Admin/Manage/AcademicAdvisingLogService.php create mode 100644 database/migrations/2026_08_25_000005_create_academic_advising_logs_table.php create mode 100644 database/seeders/AcademicAdvisingLogSeeder.php create mode 100644 resources/js/pages/admin/manage/academic-advising-logs/columns.tsx create mode 100644 resources/js/pages/admin/manage/academic-advising-logs/index.tsx create mode 100644 resources/js/types/academic-advising-log.ts diff --git a/app/Http/Controllers/Admin/Manage/AcademicAdvisingLogController.php b/app/Http/Controllers/Admin/Manage/AcademicAdvisingLogController.php new file mode 100644 index 0000000..134de40 --- /dev/null +++ b/app/Http/Controllers/Admin/Manage/AcademicAdvisingLogController.php @@ -0,0 +1,57 @@ + $this->service->paginated(...$request->validatedWithDefaults()), + 'students' => $this->studentService->getAllForSelect(), + 'lecturers' => $this->lecturerService->getAllForSelect(), + ]); + } + + public function store(AcademicAdvisingLogRequest $request): RedirectResponse + { + $this->service->create($request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil ditambahkan.']); + + return to_route('admin.manage.academic-advising-logs.index'); + } + + public function update(AcademicAdvisingLogRequest $request, AcademicAdvisingLog $academicAdvisingLog): RedirectResponse + { + $this->service->update($academicAdvisingLog, $request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil diperbarui.']); + + return to_route('admin.manage.academic-advising-logs.index'); + } + + public function destroy(AcademicAdvisingLog $academicAdvisingLog): RedirectResponse + { + $this->service->delete($academicAdvisingLog); + + return Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil dihapus.'])->back(); + } +} diff --git a/app/Http/Requests/Admin/Manage/AcademicAdvisingLogRequest.php b/app/Http/Requests/Admin/Manage/AcademicAdvisingLogRequest.php new file mode 100644 index 0000000..d8ef181 --- /dev/null +++ b/app/Http/Requests/Admin/Manage/AcademicAdvisingLogRequest.php @@ -0,0 +1,25 @@ + ['required', 'integer', Rule::exists('students', 'id')], + 'lecturer_id' => ['required', 'integer', Rule::exists('lecturers', 'id')], + 'topic' => ['nullable', 'string', 'max:150'], + 'notes' => ['nullable', 'string'], + 'session_date' => ['nullable', 'date'], + ]; + } +} diff --git a/app/Models/AcademicAdvisingLog.php b/app/Models/AcademicAdvisingLog.php new file mode 100644 index 0000000..875b416 --- /dev/null +++ b/app/Models/AcademicAdvisingLog.php @@ -0,0 +1,31 @@ + 'datetime', + ]; + } + + public function student(): BelongsTo + { + return $this->belongsTo(Student::class); + } + + public function lecturer(): BelongsTo + { + return $this->belongsTo(Lecturer::class); + } +} diff --git a/app/Models/Lecturer.php b/app/Models/Lecturer.php index 015c483..c16e7e9 100644 --- a/app/Models/Lecturer.php +++ b/app/Models/Lecturer.php @@ -33,4 +33,9 @@ public function leaderships(): HasMany { return $this->hasMany(DepartmentLeadership::class); } + + public function academicAdvisingLogs(): HasMany + { + return $this->hasMany(AcademicAdvisingLog::class); + } } diff --git a/app/Models/Student.php b/app/Models/Student.php index 10b6779..649c453 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -66,4 +66,9 @@ public function letterRequests(): HasMany { return $this->hasMany(LetterRequest::class); } + + public function academicAdvisingLogs(): HasMany + { + return $this->hasMany(AcademicAdvisingLog::class); + } } diff --git a/app/Services/Admin/Manage/AcademicAdvisingLogService.php b/app/Services/Admin/Manage/AcademicAdvisingLogService.php new file mode 100644 index 0000000..6950e82 --- /dev/null +++ b/app/Services/Admin/Manage/AcademicAdvisingLogService.php @@ -0,0 +1,53 @@ +select(['id', 'student_id', 'lecturer_id', 'topic', 'notes', 'session_date', 'created_at']) + ->with([ + 'student.user.profile', + 'student.department', + 'lecturer.user.profile', + ]) + ->when($search, fn ($q) => $q->where('topic', 'like', "%{$search}%") + ->orWhereHas('student', fn ($q) => $q->where('student_number', 'like', "%{$search}%") + ->orWhereHas('user.profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%")))) + ->orderBy($sort, $direction) + ->paginate($perPage); + } + + public function create(array $data): AcademicAdvisingLog + { + return AcademicAdvisingLog::create([ + 'student_id' => $data['student_id'], + 'lecturer_id' => $data['lecturer_id'], + 'topic' => $data['topic'] ?? null, + 'notes' => $data['notes'] ?? null, + 'session_date' => $data['session_date'] ?? now(), + ]); + } + + public function update(AcademicAdvisingLog $log, array $data): AcademicAdvisingLog + { + $log->student_id = $data['student_id']; + $log->lecturer_id = $data['lecturer_id']; + $log->topic = $data['topic'] ?? null; + $log->notes = $data['notes'] ?? null; + $log->session_date = $data['session_date'] ?? now(); + $log->update(); + + return $log; + } + + public function delete(AcademicAdvisingLog $log): bool + { + return $log->delete(); + } +} diff --git a/database/migrations/2026_08_25_000005_create_academic_advising_logs_table.php b/database/migrations/2026_08_25_000005_create_academic_advising_logs_table.php new file mode 100644 index 0000000..fd9f7cb --- /dev/null +++ b/database/migrations/2026_08_25_000005_create_academic_advising_logs_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('student_id')->constrained()->cascadeOnDelete(); + $table->foreignId('lecturer_id')->constrained()->cascadeOnDelete(); + $table->string('topic', 150)->nullable(); + $table->text('notes')->nullable(); + $table->timestamp('session_date')->nullable()->useCurrent(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('academic_advising_logs'); + } +}; diff --git a/database/seeders/AcademicAdvisingLogSeeder.php b/database/seeders/AcademicAdvisingLogSeeder.php new file mode 100644 index 0000000..c9b7d7f --- /dev/null +++ b/database/seeders/AcademicAdvisingLogSeeder.php @@ -0,0 +1,33 @@ + 1, + 'lecturer_id' => 1, + 'topic' => 'Konsultasi KRS Semester 5', + 'notes' => 'Mahasiswa mengambil 21 SKS, disetujui', + 'session_date' => '2026-01-20 13:00:00', + 'created_at' => '2026-01-20 13:00:00', + 'updated_at' => '2026-01-20 13:00:00', + ], + [ + 'student_id' => 2, + 'lecturer_id' => 2, + 'topic' => 'Bimbingan Tugas Akhir Bab 1', + 'notes' => 'Perlu perbaikan rumusan masalah', + 'session_date' => '2026-07-15 13:00:00', + 'created_at' => '2026-07-15 13:00:00', + 'updated_at' => '2026-07-15 13:00:00', + ], + ]); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 68da324..8edd797 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -30,6 +30,7 @@ public function run(): void TuitionPaymentSeeder::class, AnnouncementSeeder::class, LetterRequestSeeder::class, + AcademicAdvisingLogSeeder::class, NotificationSeeder::class, ]); } diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index e856406..fe61aed 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -18,6 +18,7 @@ import { GraduationCap, Mail, Megaphone, + MessageCircle, Receipt, School, User, @@ -37,6 +38,7 @@ import { SidebarMenuButton, SidebarMenuItem, } from '@/components/ui/sidebar'; +import { index as academicAdvisingLogsRoute } from '@/routes/admin/manage/academic-advising-logs'; import { index as announcementsRoute } from '@/routes/admin/manage/announcements'; import { index as assignmentsRoute } from '@/routes/admin/manage/assignments'; import { index as attendancesRoute } from '@/routes/admin/manage/attendances'; @@ -151,6 +153,11 @@ const data: { url: letterRequestsRoute.url(), icon: Mail, }, + { + name: 'Bimbingan Akademik', + url: academicAdvisingLogsRoute.url(), + icon: MessageCircle, + }, ], }, { diff --git a/resources/js/pages/admin/manage/academic-advising-logs/columns.tsx b/resources/js/pages/admin/manage/academic-advising-logs/columns.tsx new file mode 100644 index 0000000..36c0edf --- /dev/null +++ b/resources/js/pages/admin/manage/academic-advising-logs/columns.tsx @@ -0,0 +1,100 @@ +import type { ColumnDef } from '@tanstack/react-table'; +import { format } from 'date-fns'; +import { Pencil, Trash2 } from 'lucide-react'; +import { RowActions } from '@/components/row-actions'; +import type { AcademicAdvisingLog } from '@/types/academic-advising-log'; + +export type { AcademicAdvisingLog } from '@/types/academic-advising-log'; + +type CreateColumnsParams = { + handleEdit: (log: AcademicAdvisingLog) => void; + handleDeleteClick: (log: AcademicAdvisingLog) => void; +}; + +export function createAcademicAdvisingLogColumns( + params: CreateColumnsParams, +): ColumnDef[] { + const { handleEdit, handleDeleteClick } = params; + + return [ + { + accessorKey: 'student.student_number', + header: () => Mahasiswa, + cell: ({ row }) => { + const student = row.original.student; + + return ( +
+

+ {student?.user?.profile?.full_name ?? 'N/A'} +

+

+ {student?.student_number} ·{' '} + {student?.department?.name ?? '-'} +

+
+ ); + }, + }, + { + accessorKey: 'lecturer.user.profile.full_name', + header: () => Dosen Wali, + cell: ({ row }) => { + const lecturer = row.original.lecturer; + + return ( +
+

+ {lecturer?.user?.profile?.full_name ?? 'N/A'} +

+

+ {lecturer?.lecturer_number} +

+
+ ); + }, + }, + { + accessorKey: 'topic', + header: () => Topik, + cell: ({ row }) => row.original.topic ?? '-', + }, + { + accessorKey: 'session_date', + header: () => Tanggal Sesi, + cell: ({ row }) => { + const sessionDate = row.original.session_date; + + return sessionDate + ? format(new Date(sessionDate), 'd MMM yyyy, HH:mm') + : '-'; + }, + }, + { + id: 'actions', + header: () => Aksi, + meta: { + className: 'w-[100px] text-center', + headerClassName: 'w-[100px] text-center', + }, + cell: ({ row }) => ( + , + onClick: () => handleEdit(row.original), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(row.original), + }, + ]} + /> + ), + }, + ]; +} diff --git a/resources/js/pages/admin/manage/academic-advising-logs/index.tsx b/resources/js/pages/admin/manage/academic-advising-logs/index.tsx new file mode 100644 index 0000000..9b8432c --- /dev/null +++ b/resources/js/pages/admin/manage/academic-advising-logs/index.tsx @@ -0,0 +1,346 @@ +import { Head, router } from '@inertiajs/react'; +import { Plus } from 'lucide-react'; +import { useState } from 'react'; +import type { PaginationState } from '@/components/data-table'; +import { DataTable } from '@/components/data-table'; +import { DateTimeField } from '@/components/datetime-field'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { FormDialog } from '@/components/form-dialog'; +import InputError from '@/components/input-error'; +import { PageHeader } from '@/components/page-header'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { useServerTable } from '@/hooks/use-server-table'; +import { + index as academicAdvisingLogIndex, + destroy, + store, + update, +} from '@/routes/admin/manage/academic-advising-logs'; +import type { + AcademicAdvisingLog, + AcademicAdvisingLogLecturer, + AcademicAdvisingLogStudent, +} from '@/types/academic-advising-log'; +import { createAcademicAdvisingLogColumns } from './columns'; + +type Props = { + logs: { + data: AcademicAdvisingLog[]; + current_page: number; + last_page: number; + per_page: number; + total: number; + }; + students: AcademicAdvisingLogStudent[]; + lecturers: AcademicAdvisingLogLecturer[]; + highlight?: number; +}; + +function studentLabel(student: AcademicAdvisingLogStudent): string { + return `${student.user?.profile?.full_name ?? 'N/A'} - ${student.student_number}`; +} + +function lecturerLabel(lecturer: AcademicAdvisingLogLecturer): string { + return `${lecturer.user?.profile?.full_name ?? 'N/A'} - ${lecturer.lecturer_number}`; +} + +export default function AcademicAdvisingLogIndex({ + logs, + students, + lecturers, + highlight, +}: Props) { + const [createOpen, setCreateOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [deleting, setDeleting] = useState(null); + + const pagination: PaginationState = { + current_page: logs.current_page, + last_page: logs.last_page, + per_page: logs.per_page, + total: logs.total, + }; + + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => academicAdvisingLogIndex.url(), + pagination, + }); + + function handleDelete() { + if (!deleting) { + return; + } + + router.delete(destroy(deleting.id), { + onSuccess: () => setDeleting(null), + }); + } + + const columns = createAcademicAdvisingLogColumns({ + handleEdit: (log) => setEditing(log), + handleDeleteClick: (log) => setDeleting(log), + }); + + return ( + <> + + +
+ + Menampilkan log bimbingan dari notifikasi. +

+ ) + } + actions={ + + + } + /> + + + + { + if (!open) { + setEditing(null); + } + }} + editing={editing} + students={students} + lecturers={lecturers} + /> + + + + { + if (!open) { + setDeleting(null); + } + }} + title="Hapus Log Bimbingan" + description={(log) => + `Apakah Anda yakin ingin menghapus log bimbingan untuk "${log.student?.user?.profile?.full_name ?? 'mahasiswa ini'}"? Tindakan ini tidak dapat dibatalkan.` + } + onConfirm={handleDelete} + /> +
+ + ); +} + +function AdvisingLogFields({ + errors, + editing, + students, + lecturers, +}: { + errors: Record; + editing?: AcademicAdvisingLog; + students: AcademicAdvisingLogStudent[]; + lecturers: AcademicAdvisingLogLecturer[]; +}) { + return ( + <> +
+ + {!editing && } + + +
+
+ + {!editing && } + + +
+
+ + + +
+
+ +