Refactor lecturer management to support multiple departments
Some checks failed
tests / ci (pull_request) Has been cancelled
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:
parent
bf6f4b19b7
commit
07a00a92a2
@ -58,7 +58,7 @@ public function map($user): array
|
|||||||
$user->username,
|
$user->username,
|
||||||
$user->email,
|
$user->email,
|
||||||
$user->profile?->phone_number ?? '-',
|
$user->profile?->phone_number ?? '-',
|
||||||
$user->lecturer?->department?->name ?? '-',
|
$user->lecturer?->departments->pluck('name')->join(', ') ?: '-',
|
||||||
$user->profile?->gender?->label() ?? '-',
|
$user->profile?->gender?->label() ?? '-',
|
||||||
$user->profile?->birth_place ?? '-',
|
$user->profile?->birth_place ?? '-',
|
||||||
$user->profile?->birth_date?->format('d M Y') ?? '-',
|
$user->profile?->birth_date?->format('d M Y') ?? '-',
|
||||||
|
|||||||
@ -83,11 +83,11 @@ private function studentRoleData(User $user): array
|
|||||||
|
|
||||||
private function lecturerRoleData(User $user): array
|
private function lecturerRoleData(User $user): array
|
||||||
{
|
{
|
||||||
$user->load(['profile', 'lecturer.department']);
|
$user->load(['profile', 'lecturer.departments']);
|
||||||
|
|
||||||
return ['dosen', $user->lecturer ? [
|
return ['dosen', $user->lecturer ? [
|
||||||
'lecturer_number' => $user->lecturer->lecturer_number,
|
'lecturer_number' => $user->lecturer->lecturer_number,
|
||||||
'department' => $user->lecturer->department?->name,
|
'department' => $user->lecturer->departments->pluck('name')->join(', ') ?: null,
|
||||||
] : null];
|
] : null];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,7 @@ public function store(LecturerRequest $request): RedirectResponse
|
|||||||
|
|
||||||
public function edit(User $user): Response
|
public function edit(User $user): Response
|
||||||
{
|
{
|
||||||
$user->load(['profile', 'lecturer.department']);
|
$user->load(['profile', 'lecturer.departments']);
|
||||||
|
|
||||||
return Inertia::render('admin/users/lecturers/edit', [
|
return Inertia::render('admin/users/lecturers/edit', [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
|
|||||||
@ -53,11 +53,8 @@ public function rules(): array
|
|||||||
'max:20',
|
'max:20',
|
||||||
Rule::unique('lecturers')->ignore($userId, 'user_id'),
|
Rule::unique('lecturers')->ignore($userId, 'user_id'),
|
||||||
],
|
],
|
||||||
'department_id' => [
|
'department_ids' => ['required', 'array', 'min:1'],
|
||||||
'required',
|
'department_ids.*' => ['integer', Rule::exists('departments', 'id')],
|
||||||
'integer',
|
|
||||||
Rule::exists('departments', 'id'),
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
@ -14,9 +15,9 @@ class Department extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
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
|
public function students(): HasMany
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
@ -19,9 +20,9 @@ public function user(): BelongsTo
|
|||||||
return $this->belongsTo(User::class);
|
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
|
public function advisees(): HasMany
|
||||||
|
|||||||
@ -41,8 +41,8 @@ private function filteredQuery(string $search, ?string $gender, ?int $department
|
|||||||
return User::select(['id', 'username', 'email', 'is_active'])
|
return User::select(['id', 'username', 'email', 'is_active'])
|
||||||
->with([
|
->with([
|
||||||
'profile:id,user_id,full_name,phone_number,gender,birth_place,birth_date,address',
|
'profile:id,user_id,full_name,phone_number,gender,birth_place,birth_date,address',
|
||||||
'lecturer:id,user_id,lecturer_number,department_id',
|
'lecturer:id,user_id,lecturer_number',
|
||||||
'lecturer.department:id,name',
|
'lecturer.departments:id,name',
|
||||||
])
|
])
|
||||||
->whereHas('roles', fn ($q) => $q->where('name', 'dosen'))
|
->whereHas('roles', fn ($q) => $q->where('name', 'dosen'))
|
||||||
->when($search, fn ($q) => $q->where(function ($query) use ($search) {
|
->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}%"));
|
->orWhereHas('lecturer', fn ($q) => $q->where('lecturer_number', 'like', "%{$search}%"));
|
||||||
}))
|
}))
|
||||||
->when($gender, fn ($q) => $q->whereHas('profile', fn ($q) => $q->where('gender', $gender)))
|
->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
|
public function create(array $data): User
|
||||||
@ -75,11 +75,12 @@ public function create(array $data): User
|
|||||||
'birth_place' => $data['birth_place'],
|
'birth_place' => $data['birth_place'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user->lecturer()->create([
|
$lecturer = $user->lecturer()->create([
|
||||||
'lecturer_number' => $data['lecturer_number'],
|
'lecturer_number' => $data['lecturer_number'],
|
||||||
'department_id' => $data['department_id'],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$lecturer->departments()->sync($data['department_ids']);
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -101,13 +102,14 @@ public function update(User $user, array $data): User
|
|||||||
'birth_place' => $data['birth_place'],
|
'birth_place' => $data['birth_place'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user->lecturer()->updateOrCreate([], [
|
$lecturer = $user->lecturer()->updateOrCreate([], [
|
||||||
'lecturer_number' => $data['lecturer_number'],
|
'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
|
public function delete(User $user): void
|
||||||
|
|||||||
21
database/factories/LecturerFactory.php
Normal file
21
database/factories/LecturerFactory.php
Normal 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('##########'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
27
database/factories/StudentFactory.php
Normal file
27
database/factories/StudentFactory.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
30
database/factories/UserProfileFactory.php
Normal file
30
database/factories/UserProfileFactory.php
Normal 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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,7 +12,6 @@ public function up(): void
|
|||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||||
$table->string('lecturer_number', 10)->unique();
|
$table->string('lecturer_number', 10)->unique();
|
||||||
$table->foreignId('department_id')->nullable()->constrained()->nullOnDelete();
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
$table->softDeletes();
|
$table->softDeletes();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -14,8 +14,8 @@ public function run(): void
|
|||||||
$this->call([
|
$this->call([
|
||||||
RolePermissionSeeder::class,
|
RolePermissionSeeder::class,
|
||||||
DepartmentSeeder::class,
|
DepartmentSeeder::class,
|
||||||
UserSeeder::class,
|
|
||||||
AcademicTermSeeder::class,
|
AcademicTermSeeder::class,
|
||||||
|
UserSeeder::class,
|
||||||
DepartmentLeadershipSeeder::class,
|
DepartmentLeadershipSeeder::class,
|
||||||
CourseSeeder::class,
|
CourseSeeder::class,
|
||||||
CourseClassSeeder::class,
|
CourseClassSeeder::class,
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Department;
|
||||||
use App\Models\DepartmentLeadership;
|
use App\Models\DepartmentLeadership;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
@ -9,31 +10,21 @@ class DepartmentLeadershipSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
DepartmentLeadership::insert([
|
Department::all()->each(function (Department $department) {
|
||||||
[
|
$lecturer = $department->lecturers()
|
||||||
'department_id' => 1,
|
->whereHas('user.roles', fn ($q) => $q->where('name', 'kaprodi'))
|
||||||
'lecturer_id' => 1,
|
->first();
|
||||||
'started_at' => '2024-01-10',
|
|
||||||
|
if (! $lecturer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DepartmentLeadership::create([
|
||||||
|
'department_id' => $department->id,
|
||||||
|
'lecturer_id' => $lecturer->id,
|
||||||
|
'started_at' => now()->subYear(),
|
||||||
'ended_at' => null,
|
'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(),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,14 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Enums\Semester;
|
||||||
use App\Enums\StudentStatus;
|
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\User;
|
||||||
|
use App\Models\UserProfile;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
@ -12,57 +18,6 @@ class UserSeeder extends Seeder
|
|||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$users = [
|
$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',
|
'username' => 'pangestu',
|
||||||
'email' => 'pangestu@student.itmpwk.ac.id',
|
'email' => 'pangestu@student.itmpwk.ac.id',
|
||||||
@ -79,7 +34,7 @@ public function run(): void
|
|||||||
'student_number' => '23010001',
|
'student_number' => '23010001',
|
||||||
'department_id' => 1,
|
'department_id' => 1,
|
||||||
'enrollment_year' => 2023,
|
'enrollment_year' => 2023,
|
||||||
'academic_advisor_id' => 1,
|
'academic_advisor_id' => null,
|
||||||
'status' => StudentStatus::Active,
|
'status' => StudentStatus::Active,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -97,9 +52,9 @@ public function run(): void
|
|||||||
],
|
],
|
||||||
'student' => [
|
'student' => [
|
||||||
'student_number' => '23020002',
|
'student_number' => '23020002',
|
||||||
'department_id' => 2,
|
'department_id' => 1,
|
||||||
'enrollment_year' => 2023,
|
'enrollment_year' => 2023,
|
||||||
'academic_advisor_id' => 2,
|
'academic_advisor_id' => null,
|
||||||
'status' => StudentStatus::Active,
|
'status' => StudentStatus::Active,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -119,7 +74,7 @@ public function run(): void
|
|||||||
'student_number' => '24010003',
|
'student_number' => '24010003',
|
||||||
'department_id' => 1,
|
'department_id' => 1,
|
||||||
'enrollment_year' => 2024,
|
'enrollment_year' => 2024,
|
||||||
'academic_advisor_id' => 1,
|
'academic_advisor_id' => null,
|
||||||
'status' => StudentStatus::Active,
|
'status' => StudentStatus::Active,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -137,9 +92,9 @@ public function run(): void
|
|||||||
],
|
],
|
||||||
'student' => [
|
'student' => [
|
||||||
'student_number' => '24020004',
|
'student_number' => '24020004',
|
||||||
'department_id' => 2,
|
'department_id' => 1,
|
||||||
'enrollment_year' => 2024,
|
'enrollment_year' => 2024,
|
||||||
'academic_advisor_id' => 2,
|
'academic_advisor_id' => null,
|
||||||
'status' => StudentStatus::Active,
|
'status' => StudentStatus::Active,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -164,12 +119,134 @@ public function run(): void
|
|||||||
$user->assignRole($roles);
|
$user->assignRole($roles);
|
||||||
|
|
||||||
if ($lecturer) {
|
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) {
|
if ($student) {
|
||||||
$user->student()->create($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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export type Lecturer = {
|
|||||||
profile: { full_name: string; phone_number: string; gender: string } | null;
|
profile: { full_name: string; phone_number: string; gender: string } | null;
|
||||||
lecturer: {
|
lecturer: {
|
||||||
lecturer_number: string;
|
lecturer_number: string;
|
||||||
department: { name: string } | null;
|
departments: { id: number; name: string }[];
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,9 +59,17 @@ export function createLecturerColumns(
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'lecturer.department.name',
|
id: 'departments',
|
||||||
header: () => <span>Jurusan</span>,
|
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',
|
accessorKey: 'profile.phone_number',
|
||||||
|
|||||||
@ -7,11 +7,21 @@ import InputError from '@/components/input-error';
|
|||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
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 { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { PhoneInput } from '@/components/ui/phone-input';
|
import { PhoneInput } from '@/components/ui/phone-input';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
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 { Textarea } from '@/components/ui/textarea';
|
||||||
import { index, store } from '@/routes/admin/users/lecturers';
|
import { index, store } from '@/routes/admin/users/lecturers';
|
||||||
|
|
||||||
@ -24,6 +34,10 @@ type Props = {
|
|||||||
export default function LecturerCreate({ departments }: Props) {
|
export default function LecturerCreate({ departments }: Props) {
|
||||||
const [gender, setGender] = useState('');
|
const [gender, setGender] = useState('');
|
||||||
const [birthDate, setBirthDate] = useState<Date | undefined>();
|
const [birthDate, setBirthDate] = useState<Date | undefined>();
|
||||||
|
const [selectedDepartments, setSelectedDepartments] = useState<
|
||||||
|
Department[]
|
||||||
|
>([]);
|
||||||
|
const departmentAnchor = useComboboxAnchor();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -52,16 +66,31 @@ export default function LecturerCreate({ departments }: Props) {
|
|||||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="username">
|
<Label htmlFor="username">
|
||||||
Username <span className="text-red-500">*</span>
|
Username{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input id="username" name="username" placeholder="Masukkan username" />
|
<Input
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
placeholder="Masukkan username"
|
||||||
|
/>
|
||||||
<InputError message={errors.username} />
|
<InputError message={errors.username} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="email">
|
<Label htmlFor="email">
|
||||||
Email <span className="text-red-500">*</span>
|
Email{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</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} />
|
<InputError message={errors.email} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -74,57 +103,138 @@ export default function LecturerCreate({ departments }: Props) {
|
|||||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="full_name">
|
<Label htmlFor="full_name">
|
||||||
Nama Lengkap <span className="text-red-500">*</span>
|
Nama Lengkap{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input id="full_name" name="full_name" placeholder="Masukkan nama lengkap" />
|
<Input
|
||||||
<InputError message={errors.full_name} />
|
id="full_name"
|
||||||
|
name="full_name"
|
||||||
|
placeholder="Masukkan nama lengkap"
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={errors.full_name}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="phone_number">
|
<Label htmlFor="phone_number">
|
||||||
Nomor Telepon <span className="text-red-500">*</span>
|
Nomor Telepon{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<PhoneInput id="phone_number" name="phone_number" placeholder="08xx xxxx xxxx" />
|
<PhoneInput
|
||||||
<InputError message={errors.phone_number} />
|
id="phone_number"
|
||||||
|
name="phone_number"
|
||||||
|
placeholder="08xx xxxx xxxx"
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={errors.phone_number}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:col-span-2 md:grid-cols-3">
|
<div className="grid gap-4 md:col-span-2 md:grid-cols-3">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Jenis Kelamin <span className="text-red-500">*</span>
|
Jenis Kelamin{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<input type="hidden" name="gender" value={gender} />
|
<input
|
||||||
<RadioGroup value={gender} onValueChange={setGender} className="flex gap-4">
|
type="hidden"
|
||||||
|
name="gender"
|
||||||
|
value={gender}
|
||||||
|
/>
|
||||||
|
<RadioGroup
|
||||||
|
value={gender}
|
||||||
|
onValueChange={setGender}
|
||||||
|
className="flex gap-4"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RadioGroupItem value="male" id="male" />
|
<RadioGroupItem
|
||||||
<Label htmlFor="male" className="font-normal">Laki-laki</Label>
|
value="male"
|
||||||
|
id="male"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="male"
|
||||||
|
className="font-normal"
|
||||||
|
>
|
||||||
|
Laki-laki
|
||||||
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RadioGroupItem value="female" id="female" />
|
<RadioGroupItem
|
||||||
<Label htmlFor="female" className="font-normal">Perempuan</Label>
|
value="female"
|
||||||
|
id="female"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="female"
|
||||||
|
className="font-normal"
|
||||||
|
>
|
||||||
|
Perempuan
|
||||||
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
<InputError message={errors.gender} />
|
<InputError
|
||||||
|
message={errors.gender}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Tempat Lahir <span className="text-red-500">*</span>
|
Tempat Lahir{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input name="birth_place" placeholder="Masukkan tempat lahir" />
|
<Input
|
||||||
<InputError message={errors.birth_place} />
|
name="birth_place"
|
||||||
|
placeholder="Masukkan tempat lahir"
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={errors.birth_place}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Tanggal Lahir <span className="text-red-500">*</span>
|
Tanggal Lahir{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<input type="hidden" name="birth_date" value={birthDate ? format(birthDate, 'yyyy-MM-dd') : ''} />
|
<input
|
||||||
<DatePicker value={birthDate} onChange={setBirthDate} />
|
type="hidden"
|
||||||
<InputError message={errors.birth_date} />
|
name="birth_date"
|
||||||
|
value={
|
||||||
|
birthDate
|
||||||
|
? format(
|
||||||
|
birthDate,
|
||||||
|
'yyyy-MM-dd',
|
||||||
|
)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
value={birthDate}
|
||||||
|
onChange={setBirthDate}
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={errors.birth_date}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2 md:col-span-2">
|
<div className="grid gap-2 md:col-span-2">
|
||||||
<Label htmlFor="address">
|
<Label htmlFor="address">
|
||||||
Alamat <span className="text-red-500">*</span>
|
Alamat{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea id="address" name="address" placeholder="Masukkan alamat" />
|
<Textarea
|
||||||
|
id="address"
|
||||||
|
name="address"
|
||||||
|
placeholder="Masukkan alamat"
|
||||||
|
/>
|
||||||
<InputError message={errors.address} />
|
<InputError message={errors.address} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -137,29 +247,94 @@ export default function LecturerCreate({ departments }: Props) {
|
|||||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="lecturer_number">
|
<Label htmlFor="lecturer_number">
|
||||||
NIDN <span className="text-red-500">*</span>
|
NIDN{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input id="lecturer_number" name="lecturer_number" placeholder="Masukkan NIDN" />
|
<Input
|
||||||
<InputError message={errors.lecturer_number} />
|
id="lecturer_number"
|
||||||
|
name="lecturer_number"
|
||||||
|
placeholder="Masukkan NIDN"
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={errors.lecturer_number}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Jurusan <span className="text-red-500">*</span>
|
Jurusan{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<input type="hidden" name="department_id" />
|
{selectedDepartments.map((dept) => (
|
||||||
<Select name="department_id">
|
<input
|
||||||
<SelectTrigger className="w-full">
|
key={dept.id}
|
||||||
<SelectValue placeholder="Pilih jurusan" />
|
type="hidden"
|
||||||
</SelectTrigger>
|
name="department_ids[]"
|
||||||
<SelectContent>
|
value={dept.id}
|
||||||
{departments.map((dept) => (
|
/>
|
||||||
<SelectItem key={dept.id} value={String(dept.id)}>
|
))}
|
||||||
{dept.name}
|
<Combobox
|
||||||
</SelectItem>
|
items={departments}
|
||||||
))}
|
multiple
|
||||||
</SelectContent>
|
value={selectedDepartments}
|
||||||
</Select>
|
onValueChange={
|
||||||
<InputError message={errors.department_id} />
|
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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -7,11 +7,21 @@ import InputError from '@/components/input-error';
|
|||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
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 { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { PhoneInput } from '@/components/ui/phone-input';
|
import { PhoneInput } from '@/components/ui/phone-input';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
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 { Textarea } from '@/components/ui/textarea';
|
||||||
import { index, update } from '@/routes/admin/users/lecturers';
|
import { index, update } from '@/routes/admin/users/lecturers';
|
||||||
|
|
||||||
@ -21,8 +31,18 @@ type User = {
|
|||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
profile: { full_name: string; phone_number: string; address: string; gender: string; birth_date: string; birth_place: string } | null;
|
profile: {
|
||||||
lecturer: { lecturer_number: string; department_id: number } | null;
|
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 = {
|
type Props = {
|
||||||
@ -33,8 +53,14 @@ type Props = {
|
|||||||
export default function LecturerEdit({ user, departments }: Props) {
|
export default function LecturerEdit({ user, departments }: Props) {
|
||||||
const [gender, setGender] = useState(user.profile?.gender ?? '');
|
const [gender, setGender] = useState(user.profile?.gender ?? '');
|
||||||
const [birthDate, setBirthDate] = useState<Date | undefined>(
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -63,16 +89,31 @@ export default function LecturerEdit({ user, departments }: Props) {
|
|||||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="username">
|
<Label htmlFor="username">
|
||||||
Username <span className="text-red-500">*</span>
|
Username{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input id="username" name="username" defaultValue={user.username} />
|
<Input
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
defaultValue={user.username}
|
||||||
|
/>
|
||||||
<InputError message={errors.username} />
|
<InputError message={errors.username} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="email">
|
<Label htmlFor="email">
|
||||||
Email <span className="text-red-500">*</span>
|
Email{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</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} />
|
<InputError message={errors.email} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -85,57 +126,147 @@ export default function LecturerEdit({ user, departments }: Props) {
|
|||||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="full_name">
|
<Label htmlFor="full_name">
|
||||||
Nama Lengkap <span className="text-red-500">*</span>
|
Nama Lengkap{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input id="full_name" name="full_name" defaultValue={user.profile?.full_name ?? ''} />
|
<Input
|
||||||
<InputError message={errors.full_name} />
|
id="full_name"
|
||||||
|
name="full_name"
|
||||||
|
defaultValue={
|
||||||
|
user.profile?.full_name ?? ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={errors.full_name}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="phone_number">
|
<Label htmlFor="phone_number">
|
||||||
Nomor Telepon <span className="text-red-500">*</span>
|
Nomor Telepon{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<PhoneInput id="phone_number" name="phone_number" value={user.profile?.phone_number ?? ''} />
|
<PhoneInput
|
||||||
<InputError message={errors.phone_number} />
|
id="phone_number"
|
||||||
|
name="phone_number"
|
||||||
|
value={
|
||||||
|
user.profile?.phone_number ?? ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={errors.phone_number}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:col-span-2 md:grid-cols-3">
|
<div className="grid gap-4 md:col-span-2 md:grid-cols-3">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Jenis Kelamin <span className="text-red-500">*</span>
|
Jenis Kelamin{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<input type="hidden" name="gender" value={gender} />
|
<input
|
||||||
<RadioGroup value={gender} onValueChange={setGender} className="flex gap-4">
|
type="hidden"
|
||||||
|
name="gender"
|
||||||
|
value={gender}
|
||||||
|
/>
|
||||||
|
<RadioGroup
|
||||||
|
value={gender}
|
||||||
|
onValueChange={setGender}
|
||||||
|
className="flex gap-4"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RadioGroupItem value="male" id="male" />
|
<RadioGroupItem
|
||||||
<Label htmlFor="male" className="font-normal">Laki-laki</Label>
|
value="male"
|
||||||
|
id="male"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="male"
|
||||||
|
className="font-normal"
|
||||||
|
>
|
||||||
|
Laki-laki
|
||||||
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RadioGroupItem value="female" id="female" />
|
<RadioGroupItem
|
||||||
<Label htmlFor="female" className="font-normal">Perempuan</Label>
|
value="female"
|
||||||
|
id="female"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="female"
|
||||||
|
className="font-normal"
|
||||||
|
>
|
||||||
|
Perempuan
|
||||||
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
<InputError message={errors.gender} />
|
<InputError
|
||||||
|
message={errors.gender}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Tempat Lahir <span className="text-red-500">*</span>
|
Tempat Lahir{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input name="birth_place" defaultValue={user.profile?.birth_place ?? ''} />
|
<Input
|
||||||
<InputError message={errors.birth_place} />
|
name="birth_place"
|
||||||
|
defaultValue={
|
||||||
|
user.profile?.birth_place ??
|
||||||
|
''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={errors.birth_place}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Tanggal Lahir <span className="text-red-500">*</span>
|
Tanggal Lahir{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<input type="hidden" name="birth_date" value={birthDate ? format(birthDate, 'yyyy-MM-dd') : ''} />
|
<input
|
||||||
<DatePicker value={birthDate} onChange={setBirthDate} />
|
type="hidden"
|
||||||
<InputError message={errors.birth_date} />
|
name="birth_date"
|
||||||
|
value={
|
||||||
|
birthDate
|
||||||
|
? format(
|
||||||
|
birthDate,
|
||||||
|
'yyyy-MM-dd',
|
||||||
|
)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
value={birthDate}
|
||||||
|
onChange={setBirthDate}
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={errors.birth_date}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2 md:col-span-2">
|
<div className="grid gap-2 md:col-span-2">
|
||||||
<Label htmlFor="address">
|
<Label htmlFor="address">
|
||||||
Alamat <span className="text-red-500">*</span>
|
Alamat{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea id="address" name="address" defaultValue={user.profile?.address ?? ''} />
|
<Textarea
|
||||||
|
id="address"
|
||||||
|
name="address"
|
||||||
|
defaultValue={
|
||||||
|
user.profile?.address ?? ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
<InputError message={errors.address} />
|
<InputError message={errors.address} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -148,28 +279,97 @@ export default function LecturerEdit({ user, departments }: Props) {
|
|||||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="lecturer_number">
|
<Label htmlFor="lecturer_number">
|
||||||
NIDN <span className="text-red-500">*</span>
|
NIDN{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input id="lecturer_number" name="lecturer_number" defaultValue={user.lecturer?.lecturer_number ?? ''} />
|
<Input
|
||||||
<InputError message={errors.lecturer_number} />
|
id="lecturer_number"
|
||||||
|
name="lecturer_number"
|
||||||
|
defaultValue={
|
||||||
|
user.lecturer
|
||||||
|
?.lecturer_number ?? ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
message={errors.lecturer_number}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Jurusan <span className="text-red-500">*</span>
|
Jurusan{' '}
|
||||||
|
<span className="text-red-500">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select name="department_id" defaultValue={user.lecturer?.department_id ? String(user.lecturer.department_id) : undefined}>
|
{selectedDepartments.map((dept) => (
|
||||||
<SelectTrigger className="w-full">
|
<input
|
||||||
<SelectValue placeholder="Pilih jurusan" />
|
key={dept.id}
|
||||||
</SelectTrigger>
|
type="hidden"
|
||||||
<SelectContent>
|
name="department_ids[]"
|
||||||
{departments.map((dept) => (
|
value={dept.id}
|
||||||
<SelectItem key={dept.id} value={String(dept.id)}>
|
/>
|
||||||
{dept.name}
|
))}
|
||||||
</SelectItem>
|
<Combobox
|
||||||
))}
|
items={departments}
|
||||||
</SelectContent>
|
multiple
|
||||||
</Select>
|
value={selectedDepartments}
|
||||||
<InputError message={errors.department_id} />
|
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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user