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

View File

@ -14,12 +14,15 @@ class LecturerService
{ {
public function getAllForSelect(): Collection 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([ ->with([
'user:id,username', 'user:id,username',
'user.profile:id,user_id,full_name', 'user.profile:id,user_id,full_name',
'departments:id,name', 'departments:id,name',
]) ])
->orderBy('user_profiles.full_name')
->get(); ->get();
} }

View File

@ -14,13 +14,19 @@ class StudentService
{ {
public function getAllForSelect(?string $status = null): Collection 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([ ->with([
'user:id,username', 'user:id,username',
'user.profile:id,user_id,full_name', 'user.profile:id,user_id,full_name',
'department:id,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(); ->get();
} }

View File

@ -32,10 +32,13 @@ import {
ComboboxChip, ComboboxChip,
ComboboxChips, ComboboxChips,
ComboboxChipsInput, ComboboxChipsInput,
ComboboxCollection,
ComboboxContent, ComboboxContent,
ComboboxEmpty, ComboboxEmpty,
ComboboxGroup,
ComboboxInput, ComboboxInput,
ComboboxItem, ComboboxItem,
ComboboxLabel,
ComboboxList, ComboboxList,
useComboboxAnchor, useComboboxAnchor,
} from '@/components/ui/combobox'; } from '@/components/ui/combobox';
@ -108,6 +111,28 @@ function studentLabel(student: TuitionInvoiceStudent): string {
return `${student.user?.profile?.full_name ?? 'N/A'} - ${student.student_number}`; 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({ export default function TuitionInvoiceIndex({
invoices, invoices,
summary, summary,
@ -379,6 +404,7 @@ function CreateForm({
!student.invoiced_term_ids?.includes(Number(academicTermId)), !student.invoiced_term_ids?.includes(Number(academicTermId)),
) )
: []; : [];
const studentGroups = groupStudentsByDepartmentAndSemester(availableStudents);
function reset() { function reset() {
setAcademicTermId(''); setAcademicTermId('');
@ -468,7 +494,7 @@ function CreateForm({
/> />
))} ))}
<Combobox <Combobox
items={availableStudents} items={studentGroups}
multiple multiple
disabled={!academicTermId} disabled={!academicTermId}
value={selectedStudents} value={selectedStudents}
@ -501,13 +527,27 @@ function CreateForm({
Mahasiswa tidak ditemukan. Mahasiswa tidak ditemukan.
</ComboboxEmpty> </ComboboxEmpty>
<ComboboxList> <ComboboxList>
{(student: TuitionInvoiceStudent) => ( {(group: StudentGroup) => (
<ComboboxItem <ComboboxGroup
key={student.id} key={group.value}
value={student} items={group.items}
> >
{studentLabel(student)} <ComboboxLabel>
</ComboboxItem> {group.value}
</ComboboxLabel>
<ComboboxCollection>
{(
student: TuitionInvoiceStudent,
) => (
<ComboboxItem
key={student.id}
value={student}
>
{studentLabel(student)}
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
)} )}
</ComboboxList> </ComboboxList>
</ComboboxContent> </ComboboxContent>
@ -569,6 +609,7 @@ function EditForm({
const [student, setStudent] = useState<TuitionInvoiceStudent | null>( const [student, setStudent] = useState<TuitionInvoiceStudent | null>(
students.find((s) => s.id === editing?.student_id) ?? null, students.find((s) => s.id === editing?.student_id) ?? null,
); );
const studentGroups = groupStudentsByDepartmentAndSemester(students);
return ( return (
<FormDialog <FormDialog
@ -618,7 +659,7 @@ function EditForm({
value={student?.id ?? ''} value={student?.id ?? ''}
/> />
<Combobox <Combobox
items={students} items={studentGroups}
value={student} value={student}
onValueChange={setStudent} onValueChange={setStudent}
itemToStringLabel={studentLabel} itemToStringLabel={studentLabel}
@ -633,13 +674,29 @@ function EditForm({
Mahasiswa tidak ditemukan. Mahasiswa tidak ditemukan.
</ComboboxEmpty> </ComboboxEmpty>
<ComboboxList> <ComboboxList>
{(option: TuitionInvoiceStudent) => ( {(group: StudentGroup) => (
<ComboboxItem <ComboboxGroup
key={option.id} key={group.value}
value={option} items={group.items}
> >
{studentLabel(option)} <ComboboxLabel>
</ComboboxItem> {group.value}
</ComboboxLabel>
<ComboboxCollection>
{(
option: TuitionInvoiceStudent,
) => (
<ComboboxItem
key={option.id}
value={option}
>
{studentLabel(
option,
)}
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
)} )}
</ComboboxList> </ComboboxList>
</ComboboxContent> </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', id: 'actions',
header: () => <span className="block text-center">Aksi</span>, header: () => <span className="block text-center">Aksi</span>,

View File

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

View File

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