feat: implement academic advising and letter request management for lecturers and students

This commit is contained in:
Yoga Pangestu 2026-08-30 22:13:26 +07:00
parent 8d56ed16ac
commit e9acf29c20
14 changed files with 757 additions and 30 deletions

View File

@ -6,10 +6,12 @@
use App\Http\Requests\Admin\Services\AcademicAdvisingLogRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\AcademicAdvisingLog;
use App\Models\Lecturer;
use App\Services\Admin\Services\AcademicAdvisingLogService;
use App\Services\Admin\Users\LecturerService;
use App\Services\Admin\Users\StudentService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
@ -34,8 +36,36 @@ public function index(PaginatedRequest $request): Response
]);
}
public function mine(Request $request): Response
{
$lecturer = $this->currentLecturer($request);
return Inertia::render('lecturer/academic-advising-logs/index', [
'logs' => $this->service->forLecturer($lecturer),
'advisees' => $lecturer->advisees()
->with('user.profile')
->get(['id', 'user_id', 'student_number']),
]);
}
public function store(AcademicAdvisingLogRequest $request): RedirectResponse
{
if ($request->routeIs('lecturer.*')) {
$lecturer = $this->currentLecturer($request);
$this->service->create([
'student_id' => $request->validated('student_id'),
'lecturer_id' => $lecturer->id,
'topic' => $request->validated('topic'),
'notes' => $request->validated('notes'),
'session_date' => $request->validated('session_date'),
]);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil dicatat.']);
return to_route('lecturer.academic-advising-logs.index');
}
$this->service->create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil ditambahkan.']);
@ -58,4 +88,13 @@ public function destroy(AcademicAdvisingLog $academicAdvisingLog): RedirectRespo
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil dihapus.'])->back();
}
private function currentLecturer(Request $request): Lecturer
{
$lecturer = $request->user()->lecturer;
abort_if(! $lecturer, 403);
return $lecturer;
}
}

View File

@ -6,9 +6,11 @@
use App\Http\Requests\Admin\Services\LetterRequestRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\LetterRequest;
use App\Models\Student;
use App\Services\Admin\Services\LetterRequestService;
use App\Services\Admin\Users\StudentService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
@ -31,8 +33,45 @@ public function index(PaginatedRequest $request): Response
]);
}
public function mine(Request $request): Response
{
return Inertia::render('student/letter-requests/index', [
'letterRequests' => $this->service->mine($this->currentStudent($request)),
]);
}
public function create(): Response
{
return Inertia::render('student/letter-requests/create');
}
public function show(Request $request, LetterRequest $letterRequest): Response
{
$student = $this->currentStudent($request);
abort_if($letterRequest->student_id !== $student->id, 403);
return Inertia::render('student/letter-requests/show', [
'letterRequest' => $this->service->withDetails($letterRequest),
]);
}
public function store(LetterRequestRequest $request): RedirectResponse
{
if ($request->routeIs('student.*')) {
$student = $this->currentStudent($request);
$letterRequest = $this->service->create([
'student_id' => $student->id,
'letter_type' => $request->validated('letter_type'),
'purpose' => $request->validated('purpose'),
], null);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil diajukan.']);
return to_route('student.letter-requests.show', $letterRequest);
}
$this->service->create($request->validated(), $request->file('result'));
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil ditambahkan.']);
@ -55,4 +94,13 @@ public function destroy(LetterRequest $letterRequest): RedirectResponse
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil dihapus.'])->back();
}
private function currentStudent(Request $request): Student
{
$student = $request->user()->student;
abort_if(! $student, 403);
return $student;
}
}

View File

@ -9,11 +9,27 @@ class AcademicAdvisingLogRequest extends FormRequest
{
public function authorize(): bool
{
if ($this->routeIs('lecturer.*')) {
return (bool) $this->user()->lecturer;
}
return true;
}
public function rules(): array
{
if ($this->routeIs('lecturer.*')) {
$lecturer = $this->user()->lecturer;
$adviseeIds = $lecturer ? $lecturer->advisees()->pluck('id') : collect();
return [
'student_id' => ['required', 'integer', Rule::in($adviseeIds)],
'topic' => ['nullable', 'string', 'max:150'],
'notes' => ['nullable', 'string'],
'session_date' => ['nullable', 'date'],
];
}
return [
'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
'lecturer_id' => ['required', 'integer', Rule::exists('lecturers', 'id')],
@ -22,4 +38,11 @@ public function rules(): array
'session_date' => ['nullable', 'date'],
];
}
public function messages(): array
{
return [
'student_id.in' => 'Mahasiswa yang dipilih bukan mahasiswa bimbingan Anda.',
];
}
}

View File

@ -10,11 +10,22 @@ class LetterRequestRequest extends FormRequest
{
public function authorize(): bool
{
if ($this->routeIs('student.*')) {
return (bool) $this->user()->student;
}
return true;
}
public function rules(): array
{
if ($this->routeIs('student.*')) {
return [
'letter_type' => ['required', 'string', 'max:100'],
'purpose' => ['nullable', 'string'],
];
}
return [
'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
'letter_type' => ['required', 'string', 'max:100'],

View File

@ -3,10 +3,24 @@
namespace App\Services\Admin\Services;
use App\Models\AcademicAdvisingLog;
use App\Models\Lecturer;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
class AcademicAdvisingLogService
{
/**
* @return Collection<int, AcademicAdvisingLog>
*/
public function forLecturer(Lecturer $lecturer): Collection
{
return AcademicAdvisingLog::query()
->where('lecturer_id', $lecturer->id)
->with('student.user.profile')
->latest()
->get();
}
public function paginated(int $perPage = 25, string $search = '', ?int $lecturerId = null): LengthAwarePaginator
{
return AcademicAdvisingLog::query()

View File

@ -4,11 +4,30 @@
use App\Enums\LetterStatus;
use App\Models\LetterRequest;
use App\Models\Student;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\UploadedFile;
class LetterRequestService
{
/**
* @return Collection<int, LetterRequest>
*/
public function mine(Student $student): Collection
{
return LetterRequest::query()
->where('student_id', $student->id)
->with('processor.profile')
->latest()
->get();
}
public function withDetails(LetterRequest $letterRequest): LetterRequest
{
return $letterRequest->load('processor.profile');
}
public function paginated(int $perPage = 25, string $search = '', ?string $status = null): LengthAwarePaginator
{
return LetterRequest::query()

View File

@ -50,12 +50,21 @@ import { index as letterRequestsRoute } from '@/routes/admin/services/letter-req
import { index as administratorsRoute } from '@/routes/admin/users/administrators';
import { index as lecturersRoute } from '@/routes/admin/users/lecturers';
import { index as studentsRoute } from '@/routes/admin/users/students';
import { index as lecturerAcademicAdvisingLogsRoute } from '@/routes/lecturer/academic-advising-logs';
import { index as studentCourseRegistrationsRoute } from '@/routes/student/course-registrations';
import { index as studentLetterRequestsRoute } from '@/routes/student/letter-requests';
import type { Auth } from '@/types/auth';
const data: {
navMain: (NavGroup | NavItem)[];
navSecondary: { title: string; url: string; icon: Icon }[];
} = {
navMain: [
const STAFF_ROLES = ['developer', 'staff-admin', 'staff-keuangan', 'kaprodi'];
function buildNavMain({
isMahasiswa,
isDosen,
}: {
isMahasiswa: boolean;
isDosen: boolean;
}): (NavGroup | NavItem)[] {
return [
{
name: 'Dasbor',
url: '#',
@ -106,7 +115,9 @@ const data: {
items: [
{
name: 'Registrasi KRS',
url: courseRegistrationsRoute.url(),
url: isMahasiswa
? studentCourseRegistrationsRoute.url()
: courseRegistrationsRoute.url(),
icon: FileCheck2,
},
{
@ -161,33 +172,43 @@ const data: {
items: [
{
name: 'Surat Permohonan',
url: letterRequestsRoute.url(),
url: isMahasiswa
? studentLetterRequestsRoute.url()
: letterRequestsRoute.url(),
icon: Mail,
},
{
name: 'Bimbingan Akademik',
url: academicAdvisingLogsRoute.url(),
url: isDosen
? lecturerAcademicAdvisingLogsRoute.url()
: academicAdvisingLogsRoute.url(),
icon: MessageCircle,
},
],
},
],
navSecondary: [
{
title: 'Kritik dan Saran',
url: feedbackRoute.url(),
icon: IconMessageDots,
},
{
title: 'Bantuan',
url: '#',
icon: IconHelp,
},
],
};
];
}
const navSecondary: { title: string; url: string; icon: Icon }[] = [
{
title: 'Kritik dan Saran',
url: feedbackRoute.url(),
icon: IconMessageDots,
},
{
title: 'Bantuan',
url: '#',
icon: IconHelp,
},
];
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const { name } = usePage().props;
const { name, auth } = usePage<{ auth: Auth }>().props;
const roleNames = auth?.user?.roles?.map((role) => role.name) ?? [];
const isStaff = roleNames.some((role) => STAFF_ROLES.includes(role));
const isMahasiswa = !isStaff && roleNames.includes('mahasiswa');
const isDosen = !isStaff && roleNames.includes('dosen');
const navMain = buildNavMain({ isMahasiswa, isDosen });
return (
<Sidebar collapsible="icon" {...props}>
@ -209,8 +230,8 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
<NavMain items={navMain} />
<NavSecondary items={navSecondary} className="mt-auto" />
</SidebarContent>
</Sidebar>
);

View File

@ -0,0 +1,49 @@
import type { ColumnDef } from '@tanstack/react-table';
import { format } from 'date-fns';
import type { AcademicAdvisingLog } from '@/types/academic-advising-log';
export const advisingLogColumns: ColumnDef<AcademicAdvisingLog>[] = [
{
accessorKey: 'student.student_number',
header: () => <span>Mahasiswa</span>,
cell: ({ row }) => {
const student = row.original.student;
return (
<div>
<p className="font-medium">
{student?.user?.profile?.full_name ?? 'N/A'}
</p>
<p className="text-xs text-muted-foreground">
{student?.student_number}
</p>
</div>
);
},
},
{
accessorKey: 'topic',
header: () => <span>Topik</span>,
cell: ({ row }) => row.original.topic || '-',
},
{
accessorKey: 'notes',
header: () => <span>Catatan</span>,
cell: ({ row }) => (
<p className="line-clamp-2 max-w-sm text-muted-foreground">
{row.original.notes || '-'}
</p>
),
},
{
accessorKey: 'session_date',
header: () => <span>Tanggal Sesi</span>,
cell: ({ row }) => {
const sessionDate = row.original.session_date;
return sessionDate
? format(new Date(sessionDate), 'd MMM yyyy, HH:mm')
: '-';
},
},
];

View File

@ -0,0 +1,156 @@
import { Head } from '@inertiajs/react';
import { Plus } from 'lucide-react';
import { useState } from 'react';
import { DataTable } from '@/components/data-table';
import { DateTimeField } from '@/components/datetime-field';
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 { store } from '@/routes/lecturer/academic-advising-logs';
import type { AcademicAdvisingLog } from '@/types/academic-advising-log';
import { advisingLogColumns } from './columns';
type Advisee = {
id: number;
student_number: string;
user: { profile: { full_name: string } | null } | null;
};
type Props = {
logs: AcademicAdvisingLog[];
advisees: Advisee[];
};
function adviseeLabel(advisee: Advisee): string {
return `${advisee.user?.profile?.full_name ?? 'N/A'} - ${advisee.student_number}`;
}
export default function LecturerAcademicAdvisingLogIndex({
logs,
advisees,
}: Props) {
const [createOpen, setCreateOpen] = useState(false);
return (
<>
<Head title="Bimbingan Akademik" />
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
<PageHeader
title="Bimbingan Akademik"
actions={
<Button asChild>
<button
type="button"
onClick={() => setCreateOpen(true)}
>
<Plus className="h-4 w-4" />
Tambah Bimbingan
</button>
</Button>
}
/>
<CreateForm
open={createOpen}
onOpenChange={setCreateOpen}
advisees={advisees}
/>
<DataTable
columns={advisingLogColumns}
data={logs}
emptyText="Belum ada log bimbingan akademik."
/>
</div>
</>
);
}
function CreateForm({
open,
onOpenChange,
advisees,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
advisees: Advisee[];
}) {
return (
<FormDialog
open={open}
onOpenChange={onOpenChange}
title="Tambah Bimbingan Akademik"
action={store()}
resetOnSuccess
onSuccess={() => onOpenChange(false)}
>
{({ errors }) => (
<div className="grid gap-4">
<div className="grid gap-2">
<Label>
Mahasiswa{' '}
<span className="text-destructive">*</span>
</Label>
<Select name="student_id">
<SelectTrigger className="w-full">
<SelectValue placeholder="Pilih mahasiswa bimbingan" />
</SelectTrigger>
<SelectContent>
{advisees.map((advisee) => (
<SelectItem
key={advisee.id}
value={String(advisee.id)}
>
{adviseeLabel(advisee)}
</SelectItem>
))}
</SelectContent>
</Select>
{advisees.length === 0 && (
<p className="text-xs text-muted-foreground">
Anda belum memiliki mahasiswa bimbingan.
</p>
)}
<InputError message={errors.student_id} />
</div>
<div className="grid gap-2">
<Label htmlFor="topic">Topik</Label>
<Input
id="topic"
name="topic"
placeholder="Masukkan topik bimbingan"
/>
<InputError message={errors.topic} />
</div>
<div className="grid gap-2">
<Label htmlFor="notes">Catatan</Label>
<Textarea
id="notes"
name="notes"
placeholder="Masukkan catatan bimbingan"
/>
<InputError message={errors.notes} />
</div>
<DateTimeField
label="Tanggal Sesi"
name="session_date"
placeholder="Pilih tanggal sesi"
error={errors.session_date}
/>
</div>
)}
</FormDialog>
);
}

View File

@ -0,0 +1,91 @@
import type { ColumnDef } from '@tanstack/react-table';
import { format } from 'date-fns';
import { Eye } from 'lucide-react';
import { RowActions } from '@/components/row-actions';
import { Badge } from '@/components/ui/badge';
import { show } from '@/routes/student/letter-requests';
import type { LetterRequest, LetterStatus } from '@/types/letter-request';
import { LetterStatusLabels } from '@/types/letter-request';
export const letterRequestColumns: ColumnDef<LetterRequest>[] = [
{
accessorKey: 'letter_type',
header: () => <span>Jenis Surat</span>,
cell: ({ row }) => {
const letterRequest = row.original;
return (
<div>
<p className="font-medium">{letterRequest.letter_type}</p>
{letterRequest.purpose && (
<p className="line-clamp-1 max-w-xs text-xs text-muted-foreground">
{letterRequest.purpose}
</p>
)}
</div>
);
},
},
{
accessorKey: 'status',
header: () => <span className="block text-center">Status</span>,
meta: {
className: 'w-[130px] text-center',
headerClassName: 'w-[130px] text-center',
},
cell: ({ row }) => {
const status = row.original.status as LetterStatus | null;
const variant =
status === 'completed'
? 'default'
: status === 'rejected'
? 'destructive'
: status === 'in_process'
? 'secondary'
: 'outline';
return (
<div className="flex justify-center">
{status ? (
<Badge variant={variant}>
{LetterStatusLabels[status]}
</Badge>
) : (
<span className="text-muted-foreground">-</span>
)}
</div>
);
},
},
{
accessorKey: 'submitted_at',
header: () => <span>Diajukan</span>,
cell: ({ row }) => {
const submittedAt = row.original.submitted_at;
return submittedAt
? format(new Date(submittedAt), 'd MMM yyyy, HH:mm')
: '-';
},
},
{
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
className: 'w-[80px] text-center',
headerClassName: 'w-[80px] text-center',
},
cell: ({ row }) => (
<RowActions
actions={[
{
label: 'Lihat Detail',
icon: <Eye className="h-4 w-4" />,
href: show.url(row.original.id),
},
]}
/>
),
},
];

View File

@ -0,0 +1,79 @@
import { Form, Head, Link } from '@inertiajs/react';
import { ArrowLeft } from 'lucide-react';
import InputError from '@/components/input-error';
import { PageHeader } from '@/components/page-header';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { index, store } from '@/routes/student/letter-requests';
export default function LetterRequestCreate() {
return (
<>
<Head title="Ajukan Surat Permohonan" />
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
<PageHeader
title="Ajukan Surat Permohonan"
actions={
<Button variant="outline" asChild>
<Link href={index.url()}>
<ArrowLeft className="h-4 w-4" />
Kembali
</Link>
</Button>
}
/>
<Form action={store()}>
{({ errors, processing }) => (
<Card>
<CardHeader>
<CardTitle>Detail Permohonan</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="letter_type">
Jenis Surat{' '}
<span className="text-destructive">
*
</span>
</Label>
<Input
id="letter_type"
name="letter_type"
placeholder="Contoh: Surat Aktif Kuliah"
aria-invalid={!!errors.letter_type}
/>
<InputError message={errors.letter_type} />
</div>
<div className="grid gap-2">
<Label htmlFor="purpose">Keperluan</Label>
<Textarea
id="purpose"
name="purpose"
placeholder="Jelaskan keperluan surat ini"
/>
<InputError message={errors.purpose} />
</div>
</CardContent>
<CardFooter className="justify-end">
<Button type="submit" disabled={processing}>
Ajukan
</Button>
</CardFooter>
</Card>
)}
</Form>
</div>
</>
);
}

View File

@ -0,0 +1,40 @@
import { Head, Link } from '@inertiajs/react';
import { Plus } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import { PageHeader } from '@/components/page-header';
import { Button } from '@/components/ui/button';
import { create } from '@/routes/student/letter-requests';
import type { LetterRequest } from '@/types/letter-request';
import { letterRequestColumns } from './columns';
type Props = {
letterRequests: LetterRequest[];
};
export default function LetterRequestIndex({ letterRequests }: Props) {
return (
<>
<Head title="Surat Permohonan" />
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
<PageHeader
title="Surat Permohonan"
actions={
<Button asChild>
<Link href={create.url()}>
<Plus className="h-4 w-4" />
Ajukan Surat
</Link>
</Button>
}
/>
<DataTable
columns={letterRequestColumns}
data={letterRequests}
emptyText="Anda belum pernah mengajukan surat permohonan."
/>
</div>
</>
);
}

View File

@ -0,0 +1,123 @@
import { Head, Link } from '@inertiajs/react';
import { format } from 'date-fns';
import { ArrowLeft, Paperclip } from 'lucide-react';
import { PageHeader } from '@/components/page-header';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { index } from '@/routes/student/letter-requests';
import type { LetterRequest, LetterStatus } from '@/types/letter-request';
import { LetterStatusLabels } from '@/types/letter-request';
type Props = {
letterRequest: LetterRequest;
};
export default function LetterRequestShow({ letterRequest }: Props) {
const status = letterRequest.status as LetterStatus | null;
const variant =
status === 'completed'
? 'default'
: status === 'rejected'
? 'destructive'
: status === 'in_process'
? 'secondary'
: 'outline';
return (
<>
<Head title="Detail Surat Permohonan" />
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
<PageHeader
title="Detail Surat Permohonan"
actions={
<Button variant="outline" asChild>
<Link href={index.url()}>
<ArrowLeft className="h-4 w-4" />
Kembali
</Link>
</Button>
}
/>
<Card>
<CardHeader className="flex flex-row items-start justify-between gap-4">
<div>
<CardTitle>{letterRequest.letter_type}</CardTitle>
{letterRequest.submitted_at && (
<p className="mt-1 text-sm text-muted-foreground">
Diajukan pada{' '}
{format(
new Date(letterRequest.submitted_at),
'd MMM yyyy, HH:mm',
)}
</p>
)}
</div>
{status && (
<Badge variant={variant}>
{LetterStatusLabels[status]}
</Badge>
)}
</CardHeader>
<CardContent className="grid gap-4 border-t pt-4 text-sm">
<div>
<p className="font-medium text-muted-foreground">
Keperluan
</p>
<p className="mt-1">
{letterRequest.purpose || '-'}
</p>
</div>
{letterRequest.processor && (
<div>
<p className="font-medium text-muted-foreground">
Diproses Oleh
</p>
<p className="mt-1">
{letterRequest.processor.profile
?.full_name ?? '-'}
</p>
</div>
)}
{letterRequest.completed_at && (
<div>
<p className="font-medium text-muted-foreground">
Selesai Pada
</p>
<p className="mt-1">
{format(
new Date(letterRequest.completed_at),
'd MMM yyyy, HH:mm',
)}
</p>
</div>
)}
<div>
<p className="font-medium text-muted-foreground">
Dokumen
</p>
{letterRequest.result_url ? (
<a
href={letterRequest.result_url}
target="_blank"
rel="noreferrer"
className="mt-1 inline-flex items-center gap-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
<Paperclip className="h-3.5 w-3.5" />
{letterRequest.result_name}
</a>
) : (
<p className="mt-1 text-muted-foreground">
Belum tersedia.
</p>
)}
</div>
</CardContent>
</Card>
</div>
</>
);
}

View File

@ -85,11 +85,25 @@
});
});
Route::middleware('role:mahasiswa')->prefix('course-registrations')->name('student.course-registrations.')->group(function () {
Route::get('/', [CourseRegistrationController::class, 'mine'])->name('index');
Route::get('create', [CourseRegistrationController::class, 'create'])->name('create');
Route::post('/', [CourseRegistrationController::class, 'store'])->name('store');
Route::get('{submission}', [CourseRegistrationController::class, 'show'])->name('show');
Route::middleware('role:mahasiswa')->group(function () {
Route::prefix('course-registrations')->name('student.course-registrations.')->group(function () {
Route::get('/', [CourseRegistrationController::class, 'mine'])->name('index');
Route::get('create', [CourseRegistrationController::class, 'create'])->name('create');
Route::post('/', [CourseRegistrationController::class, 'store'])->name('store');
Route::get('{submission}', [CourseRegistrationController::class, 'show'])->name('show');
});
Route::prefix('letter-requests')->name('student.letter-requests.')->group(function () {
Route::get('/', [LetterRequestController::class, 'mine'])->name('index');
Route::get('create', [LetterRequestController::class, 'create'])->name('create');
Route::post('/', [LetterRequestController::class, 'store'])->name('store');
Route::get('{letter_request}', [LetterRequestController::class, 'show'])->name('show');
});
});
Route::middleware('role:dosen')->prefix('academic-advising-logs')->name('lecturer.academic-advising-logs.')->group(function () {
Route::get('/', [AcademicAdvisingLogController::class, 'mine'])->name('index');
Route::post('/', [AcademicAdvisingLogController::class, 'store'])->name('store');
});
Route::prefix('admin/finances')->name('admin.finances.')->group(function () {