feat: enhance student and lecturer data retrieval with improved joins and sorting

This commit is contained in:
Yoga Pangestu 2026-09-02 19:00:39 +07:00
parent 8528a3258d
commit 53afdb1fec
7 changed files with 99 additions and 30 deletions

View File

@ -41,15 +41,19 @@ public function departmentSummary(): Collection
public function paginated(int $perPage = 25, string $search = '', ?int $departmentId = null, ?int $advisorLecturerId = null, ?array $ledDepartmentIds = null): LengthAwarePaginator
{
$paginator = Student::query()
->select(['id', 'user_id', 'student_number', 'department_id'])
->where('status', StudentStatus::Active)
->when($ledDepartmentIds !== null, fn ($q) => $q->whereIn('department_id', $ledDepartmentIds))
->when($departmentId, fn ($q) => $q->where('department_id', $departmentId))
->when($advisorLecturerId, fn ($q) => $q->where('academic_advisor_id', $advisorLecturerId))
->when($search, fn ($q) => $q->where('student_number', 'like', "%{$search}%")
->orWhereHas('user.profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%")))
->select(['students.id', 'students.user_id', 'students.student_number', 'students.department_id'])
->join('departments', 'departments.id', '=', 'students.department_id')
->join('users', 'users.id', '=', 'students.user_id')
->join('user_profiles', 'user_profiles.user_id', '=', 'users.id')
->where('students.status', StudentStatus::Active)
->when($ledDepartmentIds !== null, fn ($q) => $q->whereIn('students.department_id', $ledDepartmentIds))
->when($departmentId, fn ($q) => $q->where('students.department_id', $departmentId))
->when($advisorLecturerId, fn ($q) => $q->where('students.academic_advisor_id', $advisorLecturerId))
->when($search, fn ($q) => $q->where('students.student_number', 'like', "%{$search}%")
->orWhere('user_profiles.full_name', 'like', "%{$search}%"))
->with(['user.profile', 'department:id,name'])
->orderBy('student_number')
->orderBy('departments.name')
->orderBy('user_profiles.full_name')
->paginate($perPage);
return $paginator->through(fn (Student $student) => [

View File

@ -14,12 +14,15 @@ class LecturerService
{
public function getAllForSelect(): Collection
{
return Lecturer::select(['id', 'user_id', 'lecturer_number'])
return Lecturer::select(['lecturers.id', 'lecturers.user_id', 'lecturers.lecturer_number'])
->join('users', 'users.id', '=', 'lecturers.user_id')
->join('user_profiles', 'user_profiles.user_id', '=', 'users.id')
->with([
'user:id,username',
'user.profile:id,user_id,full_name',
'departments:id,name',
])
->orderBy('user_profiles.full_name')
->get();
}

View File

@ -14,13 +14,19 @@ class StudentService
{
public function getAllForSelect(?string $status = null): Collection
{
return Student::select(['id', 'user_id', 'student_number', 'department_id', 'current_semester'])
return Student::select(['students.id', 'students.user_id', 'students.student_number', 'students.department_id', 'students.current_semester'])
->join('departments', 'departments.id', '=', 'students.department_id')
->join('users', 'users.id', '=', 'students.user_id')
->join('user_profiles', 'user_profiles.user_id', '=', 'users.id')
->with([
'user:id,username',
'user.profile:id,user_id,full_name',
'department:id,name',
])
->when($status, fn ($q) => $q->where('status', $status))
->when($status, fn ($q) => $q->where('students.status', $status))
->orderBy('departments.name')
->orderBy('students.current_semester')
->orderBy('user_profiles.full_name')
->get();
}

View File

@ -32,10 +32,13 @@ import {
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
useComboboxAnchor,
} from '@/components/ui/combobox';
@ -108,6 +111,28 @@ function studentLabel(student: TuitionInvoiceStudent): string {
return `${student.user?.profile?.full_name ?? 'N/A'} - ${student.student_number}`;
}
type StudentGroup = { value: string; items: TuitionInvoiceStudent[] };
function groupStudentsByDepartmentAndSemester(
students: TuitionInvoiceStudent[],
): StudentGroup[] {
const groups: StudentGroup[] = [];
let currentKey: string | null = null;
for (const student of students) {
const key = `${student.department?.name ?? 'Tanpa Jurusan'} — Semester ${student.current_semester ?? 'Tidak ditentukan'}`;
if (key !== currentKey) {
currentKey = key;
groups.push({ value: key, items: [] });
}
groups[groups.length - 1].items.push(student);
}
return groups;
}
export default function TuitionInvoiceIndex({
invoices,
summary,
@ -379,6 +404,7 @@ function CreateForm({
!student.invoiced_term_ids?.includes(Number(academicTermId)),
)
: [];
const studentGroups = groupStudentsByDepartmentAndSemester(availableStudents);
function reset() {
setAcademicTermId('');
@ -468,7 +494,7 @@ function CreateForm({
/>
))}
<Combobox
items={availableStudents}
items={studentGroups}
multiple
disabled={!academicTermId}
value={selectedStudents}
@ -501,13 +527,27 @@ function CreateForm({
Mahasiswa tidak ditemukan.
</ComboboxEmpty>
<ComboboxList>
{(student: TuitionInvoiceStudent) => (
<ComboboxItem
key={student.id}
value={student}
{(group: StudentGroup) => (
<ComboboxGroup
key={group.value}
items={group.items}
>
{studentLabel(student)}
</ComboboxItem>
<ComboboxLabel>
{group.value}
</ComboboxLabel>
<ComboboxCollection>
{(
student: TuitionInvoiceStudent,
) => (
<ComboboxItem
key={student.id}
value={student}
>
{studentLabel(student)}
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>
@ -569,6 +609,7 @@ function EditForm({
const [student, setStudent] = useState<TuitionInvoiceStudent | null>(
students.find((s) => s.id === editing?.student_id) ?? null,
);
const studentGroups = groupStudentsByDepartmentAndSemester(students);
return (
<FormDialog
@ -618,7 +659,7 @@ function EditForm({
value={student?.id ?? ''}
/>
<Combobox
items={students}
items={studentGroups}
value={student}
onValueChange={setStudent}
itemToStringLabel={studentLabel}
@ -633,13 +674,29 @@ function EditForm({
Mahasiswa tidak ditemukan.
</ComboboxEmpty>
<ComboboxList>
{(option: TuitionInvoiceStudent) => (
<ComboboxItem
key={option.id}
value={option}
{(group: StudentGroup) => (
<ComboboxGroup
key={group.value}
items={group.items}
>
{studentLabel(option)}
</ComboboxItem>
<ComboboxLabel>
{group.value}
</ComboboxLabel>
<ComboboxCollection>
{(
option: TuitionInvoiceStudent,
) => (
<ComboboxItem
key={option.id}
value={option}
>
{studentLabel(
option,
)}
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>

View File

@ -26,11 +26,6 @@ export function createCourseRegistrationColumns(): ColumnDef<CourseRegistrationR
);
},
},
{
id: 'department',
header: () => <span>Jurusan</span>,
cell: ({ row }) => row.original.student?.department?.name ?? '-',
},
{
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,

View File

@ -79,6 +79,9 @@ export default function CourseRegistrationIndex({
onSearchChange={handleSearchChange}
searchValue={search}
searchKey="student"
groupBy={(row) =>
row.student?.department?.name ?? 'Tanpa Jurusan'
}
toolbar={
<FilterDialog
fields={filterFields}

View File

@ -2,6 +2,7 @@ export type TuitionInvoiceStudent = {
id: number;
student_number: string;
department: { id: number; name: string } | null;
current_semester: number;
user: { profile: { full_name: string } | null } | null;
invoiced_term_ids?: number[];
};