Refactor lecturer management to support multiple departments
Some checks failed
tests / ci (pull_request) Has been cancelled

- Updated LecturerController to load multiple departments for lecturers.
- Modified LecturerRequest to accept an array of department IDs instead of a single department ID.
- Changed Department model to establish a many-to-many relationship with Lecturer.
- Adjusted Lecturer model to reflect the new many-to-many relationship with Department.
- Updated LecturerService to handle multiple departments during creation and updates.
- Created LecturerFactory and StudentFactory for generating test data.
- Added a migration for the new lecturer_department pivot table.
- Refactored seeder classes to accommodate the new structure for lecturers and departments.
- Enhanced frontend components for creating and editing lecturers to support multiple department selection.
This commit is contained in:
Yoga Pangestu 2026-08-30 12:18:13 +07:00
parent bf6f4b19b7
commit 07a00a92a2
18 changed files with 757 additions and 203 deletions

View File

@ -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') ?? '-',

View File

@ -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];
}

View File

@ -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,

View File

@ -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')],
];
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,21 @@
<?php
namespace Database\Factories;
use App\Models\Lecturer;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Lecturer>
*/
class LecturerFactory extends Factory
{
protected $model = Lecturer::class;
public function definition(): array
{
return [
'lecturer_number' => fake()->unique()->numerify('##########'),
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Database\Factories;
use App\Enums\StudentStatus;
use App\Models\Department;
use App\Models\Student;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Student>
*/
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,
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Database\Factories;
use App\Enums\Gender;
use App\Models\UserProfile;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<UserProfile>
*/
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(),
];
}
}

View File

@ -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();
});

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('lecturer_department', function (Blueprint $table) {
$table->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');
}
};

View File

@ -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,

View File

@ -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(),
],
]);
]);
});
}
}

View File

@ -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);
}
}
}
}

View File

@ -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: () => <span>Jurusan</span>,
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',

View File

@ -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<Date | undefined>();
const [selectedDepartments, setSelectedDepartments] = useState<
Department[]
>([]);
const departmentAnchor = useComboboxAnchor();
return (
<>
@ -52,16 +66,31 @@ export default function LecturerCreate({ departments }: Props) {
<CardContent className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="username">
Username <span className="text-red-500">*</span>
Username{' '}
<span className="text-red-500">
*
</span>
</Label>
<Input id="username" name="username" placeholder="Masukkan username" />
<Input
id="username"
name="username"
placeholder="Masukkan username"
/>
<InputError message={errors.username} />
</div>
<div className="grid gap-2">
<Label htmlFor="email">
Email <span className="text-red-500">*</span>
Email{' '}
<span className="text-red-500">
*
</span>
</Label>
<Input id="email" name="email" type="email" placeholder="Masukkan email" />
<Input
id="email"
name="email"
type="email"
placeholder="Masukkan email"
/>
<InputError message={errors.email} />
</div>
</CardContent>
@ -74,57 +103,138 @@ export default function LecturerCreate({ departments }: Props) {
<CardContent className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="full_name">
Nama Lengkap <span className="text-red-500">*</span>
Nama Lengkap{' '}
<span className="text-red-500">
*
</span>
</Label>
<Input id="full_name" name="full_name" placeholder="Masukkan nama lengkap" />
<InputError message={errors.full_name} />
<Input
id="full_name"
name="full_name"
placeholder="Masukkan nama lengkap"
/>
<InputError
message={errors.full_name}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="phone_number">
Nomor Telepon <span className="text-red-500">*</span>
Nomor Telepon{' '}
<span className="text-red-500">
*
</span>
</Label>
<PhoneInput id="phone_number" name="phone_number" placeholder="08xx xxxx xxxx" />
<InputError message={errors.phone_number} />
<PhoneInput
id="phone_number"
name="phone_number"
placeholder="08xx xxxx xxxx"
/>
<InputError
message={errors.phone_number}
/>
</div>
<div className="grid gap-4 md:col-span-2 md:grid-cols-3">
<div className="grid gap-2">
<Label>
Jenis Kelamin <span className="text-red-500">*</span>
Jenis Kelamin{' '}
<span className="text-red-500">
*
</span>
</Label>
<input type="hidden" name="gender" value={gender} />
<RadioGroup value={gender} onValueChange={setGender} className="flex gap-4">
<input
type="hidden"
name="gender"
value={gender}
/>
<RadioGroup
value={gender}
onValueChange={setGender}
className="flex gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="male" id="male" />
<Label htmlFor="male" className="font-normal">Laki-laki</Label>
<RadioGroupItem
value="male"
id="male"
/>
<Label
htmlFor="male"
className="font-normal"
>
Laki-laki
</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="female" id="female" />
<Label htmlFor="female" className="font-normal">Perempuan</Label>
<RadioGroupItem
value="female"
id="female"
/>
<Label
htmlFor="female"
className="font-normal"
>
Perempuan
</Label>
</div>
</RadioGroup>
<InputError message={errors.gender} />
<InputError
message={errors.gender}
/>
</div>
<div className="grid gap-2">
<Label>
Tempat Lahir <span className="text-red-500">*</span>
Tempat Lahir{' '}
<span className="text-red-500">
*
</span>
</Label>
<Input name="birth_place" placeholder="Masukkan tempat lahir" />
<InputError message={errors.birth_place} />
<Input
name="birth_place"
placeholder="Masukkan tempat lahir"
/>
<InputError
message={errors.birth_place}
/>
</div>
<div className="grid gap-2">
<Label>
Tanggal Lahir <span className="text-red-500">*</span>
Tanggal Lahir{' '}
<span className="text-red-500">
*
</span>
</Label>
<input type="hidden" name="birth_date" value={birthDate ? format(birthDate, 'yyyy-MM-dd') : ''} />
<DatePicker value={birthDate} onChange={setBirthDate} />
<InputError message={errors.birth_date} />
<input
type="hidden"
name="birth_date"
value={
birthDate
? format(
birthDate,
'yyyy-MM-dd',
)
: ''
}
/>
<DatePicker
value={birthDate}
onChange={setBirthDate}
/>
<InputError
message={errors.birth_date}
/>
</div>
</div>
<div className="grid gap-2 md:col-span-2">
<Label htmlFor="address">
Alamat <span className="text-red-500">*</span>
Alamat{' '}
<span className="text-red-500">
*
</span>
</Label>
<Textarea id="address" name="address" placeholder="Masukkan alamat" />
<Textarea
id="address"
name="address"
placeholder="Masukkan alamat"
/>
<InputError message={errors.address} />
</div>
</CardContent>
@ -137,29 +247,94 @@ export default function LecturerCreate({ departments }: Props) {
<CardContent className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="lecturer_number">
NIDN <span className="text-red-500">*</span>
NIDN{' '}
<span className="text-red-500">
*
</span>
</Label>
<Input id="lecturer_number" name="lecturer_number" placeholder="Masukkan NIDN" />
<InputError message={errors.lecturer_number} />
<Input
id="lecturer_number"
name="lecturer_number"
placeholder="Masukkan NIDN"
/>
<InputError
message={errors.lecturer_number}
/>
</div>
<div className="grid gap-2">
<Label>
Jurusan <span className="text-red-500">*</span>
Jurusan{' '}
<span className="text-red-500">
*
</span>
</Label>
<input type="hidden" name="department_id" />
<Select name="department_id">
<SelectTrigger className="w-full">
<SelectValue placeholder="Pilih jurusan" />
</SelectTrigger>
<SelectContent>
{departments.map((dept) => (
<SelectItem key={dept.id} value={String(dept.id)}>
{dept.name}
</SelectItem>
))}
</SelectContent>
</Select>
<InputError message={errors.department_id} />
{selectedDepartments.map((dept) => (
<input
key={dept.id}
type="hidden"
name="department_ids[]"
value={dept.id}
/>
))}
<Combobox
items={departments}
multiple
value={selectedDepartments}
onValueChange={
setSelectedDepartments
}
itemToStringLabel={(dept) =>
dept.name
}
isItemEqualToValue={(a, b) =>
a.id === b.id
}
>
<ComboboxChips
ref={departmentAnchor}
>
{selectedDepartments.map(
(dept) => (
<ComboboxChip
key={dept.id}
aria-label={
dept.name
}
>
{dept.name}
</ComboboxChip>
),
)}
<ComboboxChipsInput
placeholder={
selectedDepartments.length ===
0
? 'Pilih jurusan'
: ''
}
/>
</ComboboxChips>
<ComboboxContent
anchor={departmentAnchor}
>
<ComboboxEmpty>
Jurusan tidak ditemukan.
</ComboboxEmpty>
<ComboboxList>
{departments.map((dept) => (
<ComboboxItem
key={dept.id}
value={dept}
>
{dept.name}
</ComboboxItem>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
<InputError
message={errors.department_ids}
/>
</div>
</CardContent>
</Card>

View File

@ -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, update } from '@/routes/admin/users/lecturers';
@ -21,8 +31,18 @@ type User = {
id: number;
username: string;
email: string;
profile: { full_name: string; phone_number: string; address: string; gender: string; birth_date: string; birth_place: string } | null;
lecturer: { lecturer_number: string; department_id: number } | null;
profile: {
full_name: string;
phone_number: string;
address: string;
gender: string;
birth_date: string;
birth_place: string;
} | null;
lecturer: {
lecturer_number: string;
departments: { id: number; name: string }[];
} | null;
};
type Props = {
@ -33,8 +53,14 @@ type Props = {
export default function LecturerEdit({ user, departments }: Props) {
const [gender, setGender] = useState(user.profile?.gender ?? '');
const [birthDate, setBirthDate] = useState<Date | undefined>(
user.profile?.birth_date ? new Date(user.profile.birth_date) : undefined
user.profile?.birth_date
? new Date(user.profile.birth_date)
: undefined,
);
const [selectedDepartments, setSelectedDepartments] = useState<
Department[]
>(user.lecturer?.departments ?? []);
const departmentAnchor = useComboboxAnchor();
return (
<>
@ -63,16 +89,31 @@ export default function LecturerEdit({ user, departments }: Props) {
<CardContent className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="username">
Username <span className="text-red-500">*</span>
Username{' '}
<span className="text-red-500">
*
</span>
</Label>
<Input id="username" name="username" defaultValue={user.username} />
<Input
id="username"
name="username"
defaultValue={user.username}
/>
<InputError message={errors.username} />
</div>
<div className="grid gap-2">
<Label htmlFor="email">
Email <span className="text-red-500">*</span>
Email{' '}
<span className="text-red-500">
*
</span>
</Label>
<Input id="email" name="email" type="email" defaultValue={user.email} />
<Input
id="email"
name="email"
type="email"
defaultValue={user.email}
/>
<InputError message={errors.email} />
</div>
</CardContent>
@ -85,57 +126,147 @@ export default function LecturerEdit({ user, departments }: Props) {
<CardContent className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="full_name">
Nama Lengkap <span className="text-red-500">*</span>
Nama Lengkap{' '}
<span className="text-red-500">
*
</span>
</Label>
<Input id="full_name" name="full_name" defaultValue={user.profile?.full_name ?? ''} />
<InputError message={errors.full_name} />
<Input
id="full_name"
name="full_name"
defaultValue={
user.profile?.full_name ?? ''
}
/>
<InputError
message={errors.full_name}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="phone_number">
Nomor Telepon <span className="text-red-500">*</span>
Nomor Telepon{' '}
<span className="text-red-500">
*
</span>
</Label>
<PhoneInput id="phone_number" name="phone_number" value={user.profile?.phone_number ?? ''} />
<InputError message={errors.phone_number} />
<PhoneInput
id="phone_number"
name="phone_number"
value={
user.profile?.phone_number ?? ''
}
/>
<InputError
message={errors.phone_number}
/>
</div>
<div className="grid gap-4 md:col-span-2 md:grid-cols-3">
<div className="grid gap-2">
<Label>
Jenis Kelamin <span className="text-red-500">*</span>
Jenis Kelamin{' '}
<span className="text-red-500">
*
</span>
</Label>
<input type="hidden" name="gender" value={gender} />
<RadioGroup value={gender} onValueChange={setGender} className="flex gap-4">
<input
type="hidden"
name="gender"
value={gender}
/>
<RadioGroup
value={gender}
onValueChange={setGender}
className="flex gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="male" id="male" />
<Label htmlFor="male" className="font-normal">Laki-laki</Label>
<RadioGroupItem
value="male"
id="male"
/>
<Label
htmlFor="male"
className="font-normal"
>
Laki-laki
</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="female" id="female" />
<Label htmlFor="female" className="font-normal">Perempuan</Label>
<RadioGroupItem
value="female"
id="female"
/>
<Label
htmlFor="female"
className="font-normal"
>
Perempuan
</Label>
</div>
</RadioGroup>
<InputError message={errors.gender} />
<InputError
message={errors.gender}
/>
</div>
<div className="grid gap-2">
<Label>
Tempat Lahir <span className="text-red-500">*</span>
Tempat Lahir{' '}
<span className="text-red-500">
*
</span>
</Label>
<Input name="birth_place" defaultValue={user.profile?.birth_place ?? ''} />
<InputError message={errors.birth_place} />
<Input
name="birth_place"
defaultValue={
user.profile?.birth_place ??
''
}
/>
<InputError
message={errors.birth_place}
/>
</div>
<div className="grid gap-2">
<Label>
Tanggal Lahir <span className="text-red-500">*</span>
Tanggal Lahir{' '}
<span className="text-red-500">
*
</span>
</Label>
<input type="hidden" name="birth_date" value={birthDate ? format(birthDate, 'yyyy-MM-dd') : ''} />
<DatePicker value={birthDate} onChange={setBirthDate} />
<InputError message={errors.birth_date} />
<input
type="hidden"
name="birth_date"
value={
birthDate
? format(
birthDate,
'yyyy-MM-dd',
)
: ''
}
/>
<DatePicker
value={birthDate}
onChange={setBirthDate}
/>
<InputError
message={errors.birth_date}
/>
</div>
</div>
<div className="grid gap-2 md:col-span-2">
<Label htmlFor="address">
Alamat <span className="text-red-500">*</span>
Alamat{' '}
<span className="text-red-500">
*
</span>
</Label>
<Textarea id="address" name="address" defaultValue={user.profile?.address ?? ''} />
<Textarea
id="address"
name="address"
defaultValue={
user.profile?.address ?? ''
}
/>
<InputError message={errors.address} />
</div>
</CardContent>
@ -148,28 +279,97 @@ export default function LecturerEdit({ user, departments }: Props) {
<CardContent className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="lecturer_number">
NIDN <span className="text-red-500">*</span>
NIDN{' '}
<span className="text-red-500">
*
</span>
</Label>
<Input id="lecturer_number" name="lecturer_number" defaultValue={user.lecturer?.lecturer_number ?? ''} />
<InputError message={errors.lecturer_number} />
<Input
id="lecturer_number"
name="lecturer_number"
defaultValue={
user.lecturer
?.lecturer_number ?? ''
}
/>
<InputError
message={errors.lecturer_number}
/>
</div>
<div className="grid gap-2">
<Label>
Jurusan <span className="text-red-500">*</span>
Jurusan{' '}
<span className="text-red-500">
*
</span>
</Label>
<Select name="department_id" defaultValue={user.lecturer?.department_id ? String(user.lecturer.department_id) : undefined}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Pilih jurusan" />
</SelectTrigger>
<SelectContent>
{departments.map((dept) => (
<SelectItem key={dept.id} value={String(dept.id)}>
{dept.name}
</SelectItem>
))}
</SelectContent>
</Select>
<InputError message={errors.department_id} />
{selectedDepartments.map((dept) => (
<input
key={dept.id}
type="hidden"
name="department_ids[]"
value={dept.id}
/>
))}
<Combobox
items={departments}
multiple
value={selectedDepartments}
onValueChange={
setSelectedDepartments
}
itemToStringLabel={(dept) =>
dept.name
}
isItemEqualToValue={(a, b) =>
a.id === b.id
}
>
<ComboboxChips
ref={departmentAnchor}
>
{selectedDepartments.map(
(dept) => (
<ComboboxChip
key={dept.id}
aria-label={
dept.name
}
>
{dept.name}
</ComboboxChip>
),
)}
<ComboboxChipsInput
placeholder={
selectedDepartments.length ===
0
? 'Pilih jurusan'
: ''
}
/>
</ComboboxChips>
<ComboboxContent
anchor={departmentAnchor}
>
<ComboboxEmpty>
Jurusan tidak ditemukan.
</ComboboxEmpty>
<ComboboxList>
{departments.map((dept) => (
<ComboboxItem
key={dept.id}
value={dept}
>
{dept.name}
</ComboboxItem>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
<InputError
message={errors.department_ids}
/>
</div>
</CardContent>
</Card>