diff --git a/app/Exports/LecturersExport.php b/app/Exports/LecturersExport.php index 1281cd8..620452f 100644 --- a/app/Exports/LecturersExport.php +++ b/app/Exports/LecturersExport.php @@ -58,7 +58,7 @@ public function map($user): array $user->username, $user->email, $user->profile?->phone_number ?? '-', - $user->lecturer?->department?->name ?? '-', + $user->lecturer?->departments->pluck('name')->join(', ') ?: '-', $user->profile?->gender?->label() ?? '-', $user->profile?->birth_place ?? '-', $user->profile?->birth_date?->format('d M Y') ?? '-', diff --git a/app/Http/Controllers/Admin/Settings/ProfileController.php b/app/Http/Controllers/Admin/Settings/ProfileController.php index 33c6b20..ac0ba8d 100644 --- a/app/Http/Controllers/Admin/Settings/ProfileController.php +++ b/app/Http/Controllers/Admin/Settings/ProfileController.php @@ -83,11 +83,11 @@ private function studentRoleData(User $user): array private function lecturerRoleData(User $user): array { - $user->load(['profile', 'lecturer.department']); + $user->load(['profile', 'lecturer.departments']); return ['dosen', $user->lecturer ? [ 'lecturer_number' => $user->lecturer->lecturer_number, - 'department' => $user->lecturer->department?->name, + 'department' => $user->lecturer->departments->pluck('name')->join(', ') ?: null, ] : null]; } diff --git a/app/Http/Controllers/Admin/Users/LecturerController.php b/app/Http/Controllers/Admin/Users/LecturerController.php index 1e77aa9..2f51a3f 100644 --- a/app/Http/Controllers/Admin/Users/LecturerController.php +++ b/app/Http/Controllers/Admin/Users/LecturerController.php @@ -54,7 +54,7 @@ public function store(LecturerRequest $request): RedirectResponse public function edit(User $user): Response { - $user->load(['profile', 'lecturer.department']); + $user->load(['profile', 'lecturer.departments']); return Inertia::render('admin/users/lecturers/edit', [ 'user' => $user, diff --git a/app/Http/Requests/Admin/Users/LecturerRequest.php b/app/Http/Requests/Admin/Users/LecturerRequest.php index 4ae2bb2..b6b40af 100644 --- a/app/Http/Requests/Admin/Users/LecturerRequest.php +++ b/app/Http/Requests/Admin/Users/LecturerRequest.php @@ -53,11 +53,8 @@ public function rules(): array 'max:20', Rule::unique('lecturers')->ignore($userId, 'user_id'), ], - 'department_id' => [ - 'required', - 'integer', - Rule::exists('departments', 'id'), - ], + 'department_ids' => ['required', 'array', 'min:1'], + 'department_ids.*' => ['integer', Rule::exists('departments', 'id')], ]; } } diff --git a/app/Models/Department.php b/app/Models/Department.php index ed7cca5..597597c 100644 --- a/app/Models/Department.php +++ b/app/Models/Department.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; @@ -14,9 +15,9 @@ class Department extends Model { use HasFactory, SoftDeletes; - public function lecturers(): HasMany + public function lecturers(): BelongsToMany { - return $this->hasMany(Lecturer::class); + return $this->belongsToMany(Lecturer::class, 'lecturer_department'); } public function students(): HasMany diff --git a/app/Models/Lecturer.php b/app/Models/Lecturer.php index c16e7e9..0d7a20a 100644 --- a/app/Models/Lecturer.php +++ b/app/Models/Lecturer.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -19,9 +20,9 @@ public function user(): BelongsTo return $this->belongsTo(User::class); } - public function department(): BelongsTo + public function departments(): BelongsToMany { - return $this->belongsTo(Department::class); + return $this->belongsToMany(Department::class, 'lecturer_department'); } public function advisees(): HasMany diff --git a/app/Services/Admin/Users/LecturerService.php b/app/Services/Admin/Users/LecturerService.php index d97f9d9..4d58ad0 100644 --- a/app/Services/Admin/Users/LecturerService.php +++ b/app/Services/Admin/Users/LecturerService.php @@ -41,8 +41,8 @@ private function filteredQuery(string $search, ?string $gender, ?int $department return User::select(['id', 'username', 'email', 'is_active']) ->with([ 'profile:id,user_id,full_name,phone_number,gender,birth_place,birth_date,address', - 'lecturer:id,user_id,lecturer_number,department_id', - 'lecturer.department:id,name', + 'lecturer:id,user_id,lecturer_number', + 'lecturer.departments:id,name', ]) ->whereHas('roles', fn ($q) => $q->where('name', 'dosen')) ->when($search, fn ($q) => $q->where(function ($query) use ($search) { @@ -52,7 +52,7 @@ private function filteredQuery(string $search, ?string $gender, ?int $department ->orWhereHas('lecturer', fn ($q) => $q->where('lecturer_number', 'like', "%{$search}%")); })) ->when($gender, fn ($q) => $q->whereHas('profile', fn ($q) => $q->where('gender', $gender))) - ->when($departmentId, fn ($q) => $q->whereHas('lecturer', fn ($q) => $q->where('department_id', $departmentId))); + ->when($departmentId, fn ($q) => $q->whereHas('lecturer.departments', fn ($q) => $q->where('departments.id', $departmentId))); } public function create(array $data): User @@ -75,11 +75,12 @@ public function create(array $data): User 'birth_place' => $data['birth_place'], ]); - $user->lecturer()->create([ + $lecturer = $user->lecturer()->create([ 'lecturer_number' => $data['lecturer_number'], - 'department_id' => $data['department_id'], ]); + $lecturer->departments()->sync($data['department_ids']); + return $user; }); } @@ -101,13 +102,14 @@ public function update(User $user, array $data): User 'birth_place' => $data['birth_place'], ]); - $user->lecturer()->updateOrCreate([], [ + $lecturer = $user->lecturer()->updateOrCreate([], [ 'lecturer_number' => $data['lecturer_number'], - 'department_id' => $data['department_id'], ]); + + $lecturer->departments()->sync($data['department_ids']); }); - return $user->fresh(['profile', 'lecturer.department', 'roles']); + return $user->fresh(['profile', 'lecturer.departments', 'roles']); } public function delete(User $user): void diff --git a/database/factories/LecturerFactory.php b/database/factories/LecturerFactory.php new file mode 100644 index 0000000..b0fe1d9 --- /dev/null +++ b/database/factories/LecturerFactory.php @@ -0,0 +1,21 @@ + + */ +class LecturerFactory extends Factory +{ + protected $model = Lecturer::class; + + public function definition(): array + { + return [ + 'lecturer_number' => fake()->unique()->numerify('##########'), + ]; + } +} diff --git a/database/factories/StudentFactory.php b/database/factories/StudentFactory.php new file mode 100644 index 0000000..3e2e6bd --- /dev/null +++ b/database/factories/StudentFactory.php @@ -0,0 +1,27 @@ + + */ +class StudentFactory extends Factory +{ + protected $model = Student::class; + + public function definition(): array + { + return [ + 'student_number' => fake()->unique()->numerify('########'), + 'department_id' => fn () => Department::query()->inRandomOrder()->value('id'), + 'enrollment_year' => fake()->numberBetween(now()->year - 4, now()->year), + 'academic_advisor_id' => null, + 'status' => StudentStatus::Active, + ]; + } +} diff --git a/database/factories/UserProfileFactory.php b/database/factories/UserProfileFactory.php new file mode 100644 index 0000000..aafc2ff --- /dev/null +++ b/database/factories/UserProfileFactory.php @@ -0,0 +1,30 @@ + + */ +class UserProfileFactory extends Factory +{ + protected $model = UserProfile::class; + + public function definition(): array + { + $gender = fake()->randomElement(Gender::cases()); + $faker = fake('id_ID'); + + return [ + 'full_name' => $faker->name($gender === Gender::Male ? 'male' : 'female'), + 'phone_number' => '08'.fake()->numerify('##########'), + 'address' => $faker->address(), + 'gender' => $gender, + 'birth_date' => fake()->dateTimeBetween('-55 years', '-18 years')->format('Y-m-d'), + 'birth_place' => $faker->city(), + ]; + } +} diff --git a/database/migrations/2026_08_03_000005_create_lecturers_table.php b/database/migrations/2026_08_03_000005_create_lecturers_table.php index 9b06c3b..9c463d0 100644 --- a/database/migrations/2026_08_03_000005_create_lecturers_table.php +++ b/database/migrations/2026_08_03_000005_create_lecturers_table.php @@ -12,7 +12,6 @@ public function up(): void $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->string('lecturer_number', 10)->unique(); - $table->foreignId('department_id')->nullable()->constrained()->nullOnDelete(); $table->timestamps(); $table->softDeletes(); }); diff --git a/database/migrations/2026_08_26_000001_create_lecturer_department_table.php b/database/migrations/2026_08_26_000001_create_lecturer_department_table.php new file mode 100644 index 0000000..c7d7f6e --- /dev/null +++ b/database/migrations/2026_08_26_000001_create_lecturer_department_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignId('lecturer_id')->constrained()->cascadeOnDelete(); + $table->foreignId('department_id')->constrained()->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['lecturer_id', 'department_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('lecturer_department'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 8edd797..9f77eb1 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -14,8 +14,8 @@ public function run(): void $this->call([ RolePermissionSeeder::class, DepartmentSeeder::class, - UserSeeder::class, AcademicTermSeeder::class, + UserSeeder::class, DepartmentLeadershipSeeder::class, CourseSeeder::class, CourseClassSeeder::class, diff --git a/database/seeders/DepartmentLeadershipSeeder.php b/database/seeders/DepartmentLeadershipSeeder.php index 9d234a1..845e349 100644 --- a/database/seeders/DepartmentLeadershipSeeder.php +++ b/database/seeders/DepartmentLeadershipSeeder.php @@ -2,6 +2,7 @@ namespace Database\Seeders; +use App\Models\Department; use App\Models\DepartmentLeadership; use Illuminate\Database\Seeder; @@ -9,31 +10,21 @@ class DepartmentLeadershipSeeder extends Seeder { public function run(): void { - DepartmentLeadership::insert([ - [ - 'department_id' => 1, - 'lecturer_id' => 1, - 'started_at' => '2024-01-10', + Department::all()->each(function (Department $department) { + $lecturer = $department->lecturers() + ->whereHas('user.roles', fn ($q) => $q->where('name', 'kaprodi')) + ->first(); + + if (! $lecturer) { + return; + } + + DepartmentLeadership::create([ + 'department_id' => $department->id, + 'lecturer_id' => $lecturer->id, + 'started_at' => now()->subYear(), 'ended_at' => null, - 'created_at' => now(), - 'updated_at' => now(), - ], - [ - 'department_id' => 2, - 'lecturer_id' => 2, - 'started_at' => '2024-01-10', - 'ended_at' => null, - 'created_at' => now(), - 'updated_at' => now(), - ], - [ - 'department_id' => 3, - 'lecturer_id' => 3, - 'started_at' => '2023-06-01', - 'ended_at' => null, - 'created_at' => now(), - 'updated_at' => now(), - ], - ]); + ]); + }); } } diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index 6c87856..f995403 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -2,8 +2,14 @@ namespace Database\Seeders; +use App\Enums\Semester; use App\Enums\StudentStatus; +use App\Models\AcademicTerm; +use App\Models\Department; +use App\Models\Lecturer; +use App\Models\Student; use App\Models\User; +use App\Models\UserProfile; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Hash; @@ -12,57 +18,6 @@ class UserSeeder extends Seeder public function run(): void { $users = [ - [ - 'username' => 'dwi', - 'email' => 'dwi@lecturer.itmpwk.ac.id', - 'roles' => ['dosen'], - 'profile' => [ - 'full_name' => 'Dwi Saputra', - 'phone_number' => '081234567801', - 'address' => 'Jl. Kampus No. 1, Purwakarta', - 'gender' => 'male', - 'birth_place' => 'Bandung', - 'birth_date' => '1985-03-15', - ], - 'lecturer' => [ - 'lecturer_number' => '0012345601', - 'department_id' => 1, - ], - ], - [ - 'username' => 'rina', - 'email' => 'rina@lecturer.itmpwk.ac.id', - 'roles' => ['dosen'], - 'profile' => [ - 'full_name' => 'Rina Marlina', - 'phone_number' => '081234567802', - 'address' => 'Jl. Kampus No. 1, Purwakarta', - 'gender' => 'female', - 'birth_place' => 'Jakarta', - 'birth_date' => '1987-07-20', - ], - 'lecturer' => [ - 'lecturer_number' => '0012345602', - 'department_id' => 2, - ], - ], - [ - 'username' => 'budi', - 'email' => 'budi@lecturer.itmpwk.ac.id', - 'roles' => ['dosen'], - 'profile' => [ - 'full_name' => 'Budi Hartono', - 'phone_number' => '081234567803', - 'address' => 'Jl. Kampus No. 1, Purwakarta', - 'gender' => 'male', - 'birth_place' => 'Surabaya', - 'birth_date' => '1983-11-10', - ], - 'lecturer' => [ - 'lecturer_number' => '0012345603', - 'department_id' => 3, - ], - ], [ 'username' => 'pangestu', 'email' => 'pangestu@student.itmpwk.ac.id', @@ -79,7 +34,7 @@ public function run(): void 'student_number' => '23010001', 'department_id' => 1, 'enrollment_year' => 2023, - 'academic_advisor_id' => 1, + 'academic_advisor_id' => null, 'status' => StudentStatus::Active, ], ], @@ -97,9 +52,9 @@ public function run(): void ], 'student' => [ 'student_number' => '23020002', - 'department_id' => 2, + 'department_id' => 1, 'enrollment_year' => 2023, - 'academic_advisor_id' => 2, + 'academic_advisor_id' => null, 'status' => StudentStatus::Active, ], ], @@ -119,7 +74,7 @@ public function run(): void 'student_number' => '24010003', 'department_id' => 1, 'enrollment_year' => 2024, - 'academic_advisor_id' => 1, + 'academic_advisor_id' => null, 'status' => StudentStatus::Active, ], ], @@ -137,9 +92,9 @@ public function run(): void ], 'student' => [ 'student_number' => '24020004', - 'department_id' => 2, + 'department_id' => 1, 'enrollment_year' => 2024, - 'academic_advisor_id' => 2, + 'academic_advisor_id' => null, 'status' => StudentStatus::Active, ], ], @@ -164,12 +119,134 @@ public function run(): void $user->assignRole($roles); if ($lecturer) { - $user->lecturer()->create($lecturer); + $departmentId = $lecturer['department_id']; + unset($lecturer['department_id']); + + $createdLecturer = $user->lecturer()->create($lecturer); + $createdLecturer->departments()->attach($departmentId); } if ($student) { $user->student()->create($student); } } + + $this->seedRandomLecturers(); + $this->seedRandomStudents(); + $this->seedOtherStaff(); + } + + /** + * 5-8 dosen acak per jurusan. Sebagian dosen diafiliasikan ke lebih dari + * satu jurusan lewat pivot lecturer_department, sebagian cukup 1 jurusan. + */ + private function seedRandomLecturers(): void + { + $departments = Department::all(); + + foreach ($departments as $department) { + $count = fake()->numberBetween(5, 8); + $lecturers = collect(); + + for ($i = 0; $i < $count; $i++) { + $lecturer = $this->createLecturer($department); + $lecturers->push($lecturer); + + if (fake()->boolean(35)) { + $otherDepartmentIds = Department::query() + ->where('id', '!=', $department->id) + ->inRandomOrder() + ->limit(fake()->numberBetween(1, 2)) + ->pluck('id'); + + $lecturer->departments()->attach($otherDepartmentIds); + } + } + + $lecturers->random()->user->assignRole('kaprodi'); + } + } + + private function createLecturer(Department $department): Lecturer + { + $user = User::factory()->create(); + $user->profile()->save(UserProfile::factory()->make()); + $user->assignRole('dosen'); + + $lecturer = $user->lecturer()->save(Lecturer::factory()->makeOne()); + + $lecturer->departments()->attach($department->id); + + return $lecturer; + } + + /** + * 8-10 mahasiswa acak per jurusan untuk setiap periode akademik, supaya + * enrollment_year-nya mengikuti tahun ajaran yang benar-benar ada. + */ + private function seedRandomStudents(): void + { + $departments = Department::all(); + + foreach (AcademicTerm::all() as $term) { + [$startYear, $endYear] = explode('/', $term->name); + $enrollmentYear = $term->semester === Semester::even ? (int) $endYear : (int) $startYear; + + foreach ($departments as $department) { + $count = fake()->numberBetween(8, 10); + + for ($i = 0; $i < $count; $i++) { + $this->createStudent($department, $enrollmentYear); + } + } + } + } + + private function createStudent(Department $department, int $enrollmentYear): Student + { + $user = User::factory()->create(); + $user->profile()->save(UserProfile::factory()->make()); + $user->assignRole('mahasiswa'); + + $advisorId = Lecturer::query() + ->whereRelation('departments', 'departments.id', $department->id) + ->inRandomOrder() + ->value('id'); + + $status = fake()->randomElement([ + StudentStatus::Active, + StudentStatus::Active, + StudentStatus::Active, + StudentStatus::OnLeave, + StudentStatus::Graduated, + StudentStatus::DroppedOut, + ]); + + return $user->student()->save(Student::factory()->makeOne([ + 'department_id' => $department->id, + 'enrollment_year' => $enrollmentYear, + 'academic_advisor_id' => $advisorId, + 'status' => $status, + ])); + } + + /** + * Sejumlah kecil akun staf lain supaya tiap role punya data acak juga. + */ + private function seedOtherStaff(): void + { + $roleCounts = [ + 'staff-admin' => 2, + 'staff-keuangan' => 2, + 'developer' => 2, + ]; + + foreach ($roleCounts as $role => $count) { + for ($i = 0; $i < $count; $i++) { + $user = User::factory()->create(); + $user->profile()->save(UserProfile::factory()->make()); + $user->assignRole($role); + } + } } } diff --git a/resources/js/pages/admin/users/lecturers/columns.tsx b/resources/js/pages/admin/users/lecturers/columns.tsx index 84fcecc..b82f6b2 100644 --- a/resources/js/pages/admin/users/lecturers/columns.tsx +++ b/resources/js/pages/admin/users/lecturers/columns.tsx @@ -12,7 +12,7 @@ export type Lecturer = { profile: { full_name: string; phone_number: string; gender: string } | null; lecturer: { lecturer_number: string; - department: { name: string } | null; + departments: { id: number; name: string }[]; } | null; }; @@ -59,9 +59,17 @@ export function createLecturerColumns( ), }, { - accessorKey: 'lecturer.department.name', + id: 'departments', header: () => Jurusan, - cell: ({ row }) => row.original.lecturer?.department?.name ?? '-', + cell: ({ row }) => { + const departments = row.original.lecturer?.departments ?? []; + + return departments.length > 0 + ? departments + .map((department) => department.name) + .join(', ') + : '-'; + }, }, { accessorKey: 'profile.phone_number', diff --git a/resources/js/pages/admin/users/lecturers/create.tsx b/resources/js/pages/admin/users/lecturers/create.tsx index 07393fe..d620a0e 100644 --- a/resources/js/pages/admin/users/lecturers/create.tsx +++ b/resources/js/pages/admin/users/lecturers/create.tsx @@ -7,11 +7,21 @@ import InputError from '@/components/input-error'; import { PageHeader } from '@/components/page-header'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Combobox, + ComboboxChip, + ComboboxChips, + ComboboxChipsInput, + ComboboxContent, + ComboboxEmpty, + ComboboxItem, + ComboboxList, + useComboboxAnchor, +} from '@/components/ui/combobox'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { PhoneInput } from '@/components/ui/phone-input'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { index, store } from '@/routes/admin/users/lecturers'; @@ -24,6 +34,10 @@ type Props = { export default function LecturerCreate({ departments }: Props) { const [gender, setGender] = useState(''); const [birthDate, setBirthDate] = useState(); + const [selectedDepartments, setSelectedDepartments] = useState< + Department[] + >([]); + const departmentAnchor = useComboboxAnchor(); return ( <> @@ -52,16 +66,31 @@ export default function LecturerCreate({ departments }: Props) {
- +
- +
@@ -74,57 +103,138 @@ export default function LecturerCreate({ departments }: Props) {
- - + +
- - + +
- - + +
- - + +
- - + +
- +
- - + +
- - - + + +
-