feat: add lecturer and student management pages with CRUD functionality
- Implemented LecturerEdit and LecturerIndex components for managing lecturers. - Created StudentCreate and StudentEdit components for adding and editing students. - Developed StudentIndex component for listing students with search and pagination. - Added department and user types for better type safety. - Updated routes for lecturers and students, including reset password functionality. - Removed AppSidebar from dashboard for a cleaner layout.
This commit is contained in:
parent
32f700ea84
commit
2885de2c20
21
app/Enums/StudentStatus.php
Normal file
21
app/Enums/StudentStatus.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum StudentStatus: string
|
||||
{
|
||||
case Active = 'active';
|
||||
case OnLeave = 'on_leave';
|
||||
case Graduated = 'graduated';
|
||||
case DroppedOut = 'dropped_out';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Active => 'Aktif',
|
||||
self::OnLeave => 'Cuti',
|
||||
self::Graduated => 'Lulus',
|
||||
self::DroppedOut => 'Drop Out',
|
||||
};
|
||||
}
|
||||
}
|
||||
53
app/Http/Controllers/Admin/Master/DepartmentController.php
Normal file
53
app/Http/Controllers/Admin/Master/DepartmentController.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Master;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Master\DepartmentRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Department;
|
||||
use App\Services\Admin\Master\DepartmentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DepartmentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DepartmentService $service
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/master/departments/index', [
|
||||
'departments' => $this->service->paginated(...$request->validatedWithDefaults()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(DepartmentRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jurusan berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.master.departments.index');
|
||||
}
|
||||
|
||||
public function update(DepartmentRequest $request, Department $department): RedirectResponse
|
||||
{
|
||||
$this->service->update($department, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jurusan berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.master.departments.index');
|
||||
}
|
||||
|
||||
public function destroy(Department $department): RedirectResponse
|
||||
{
|
||||
$this->service->delete($department);
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jurusan berhasil dihapus.']);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
77
app/Http/Controllers/Admin/Users/AdministratorController.php
Normal file
77
app/Http/Controllers/Admin/Users/AdministratorController.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Users;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Users\AdministratorRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\Users\AdministratorService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class AdministratorController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected AdministratorService $service,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return Inertia::render('admin/users/administrators/index', [
|
||||
'administrators' => $this->service->getPaginated($request->only(['search', 'per_page'])),
|
||||
'filters' => $request->only(['search', 'per_page']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/users/administrators/create', [
|
||||
'roles' => Role::whereIn('name', ['staff-admin', 'staff-keuangan'])->get(['id', 'name']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(AdministratorRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Administrator berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.users.administrators.index');
|
||||
}
|
||||
|
||||
public function edit(User $user): Response
|
||||
{
|
||||
$user->load(['profile', 'roles']);
|
||||
|
||||
return Inertia::render('admin/users/administrators/edit', [
|
||||
'user' => $user,
|
||||
'roles' => Role::whereIn('name', ['staff-admin', 'staff-keuangan'])->get(['id', 'name']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(AdministratorRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->service->update($user, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Administrator berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.users.administrators.index');
|
||||
}
|
||||
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->delete($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Administrator berhasil dihapus.'])->back();
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->resetPassword($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil direset.'])->back();
|
||||
}
|
||||
}
|
||||
78
app/Http/Controllers/Admin/Users/LecturerController.php
Normal file
78
app/Http/Controllers/Admin/Users/LecturerController.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Users;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Users\LecturerRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\Users\DepartmentService;
|
||||
use App\Services\Admin\Users\LecturerService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class LecturerController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected LecturerService $service,
|
||||
protected DepartmentService $departmentService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return Inertia::render('admin/users/lecturers/index', [
|
||||
'lecturers' => $this->service->getPaginated($request->only(['search', 'per_page'])),
|
||||
'filters' => $request->only(['search', 'per_page']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/users/lecturers/create', [
|
||||
'departments' => $this->departmentService->getAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(LecturerRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Dosen berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.users.lecturers.index');
|
||||
}
|
||||
|
||||
public function edit(User $user): Response
|
||||
{
|
||||
$user->load(['profile', 'lecturer.department']);
|
||||
|
||||
return Inertia::render('admin/users/lecturers/edit', [
|
||||
'user' => $user,
|
||||
'departments' => $this->departmentService->getAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(LecturerRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->service->update($user, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Dosen berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.users.lecturers.index');
|
||||
}
|
||||
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->delete($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Dosen berhasil dihapus.'])->back();
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->resetPassword($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil direset.'])->back();
|
||||
}
|
||||
}
|
||||
82
app/Http/Controllers/Admin/Users/StudentController.php
Normal file
82
app/Http/Controllers/Admin/Users/StudentController.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Users;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Users\StudentRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\Users\DepartmentService;
|
||||
use App\Services\Admin\Users\LecturerService;
|
||||
use App\Services\Admin\Users\StudentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class StudentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected StudentService $service,
|
||||
protected DepartmentService $departmentService,
|
||||
protected LecturerService $lecturerService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return Inertia::render('admin/users/students/index', [
|
||||
'students' => $this->service->getPaginated($request->only(['search', 'per_page'])),
|
||||
'filters' => $request->only(['search', 'per_page']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/users/students/create', [
|
||||
'departments' => $this->departmentService->getAll(),
|
||||
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StudentRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Mahasiswa berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.users.students.index');
|
||||
}
|
||||
|
||||
public function edit(User $user): Response
|
||||
{
|
||||
$user->load(['profile', 'student.department', 'student.advisor.user.profile']);
|
||||
|
||||
return Inertia::render('admin/users/students/edit', [
|
||||
'user' => $user,
|
||||
'departments' => $this->departmentService->getAll(),
|
||||
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(StudentRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->service->update($user, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Mahasiswa berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.users.students.index');
|
||||
}
|
||||
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->delete($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Mahasiswa berhasil dihapus.'])->back();
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->resetPassword($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Password berhasil direset.'])->back();
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/Admin/Master/DepartmentRequest.php
Normal file
28
app/Http/Requests/Admin/Master/DepartmentRequest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Master;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class DepartmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'code' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:10',
|
||||
Rule::unique('departments')->ignore($this->route('department')),
|
||||
],
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'degree_level' => ['nullable', 'string', Rule::in(['D3', 'D4', 'S1', 'S2', 'S3'])],
|
||||
];
|
||||
}
|
||||
}
|
||||
51
app/Http/Requests/Admin/Users/AdministratorRequest.php
Normal file
51
app/Http/Requests/Admin/Users/AdministratorRequest.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Users;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AdministratorRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$userId = $this->route('user')?->id;
|
||||
|
||||
return [
|
||||
'username' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
'min:5',
|
||||
'alpha_dash',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
'role' => ['required', 'string', Rule::in(['staff-admin', 'staff-keuangan'])],
|
||||
|
||||
// Profile
|
||||
'full_name' => ['required', 'string', 'max:150'],
|
||||
'phone_number' => ['required', 'string', 'max:20'],
|
||||
'address' => ['required', 'string'],
|
||||
'gender' => ['required', 'string', Rule::in(array_values(Gender::cases()))],
|
||||
'birth_date' => ['required', 'date'],
|
||||
'birth_place' => ['required', 'string', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
63
app/Http/Requests/Admin/Users/LecturerRequest.php
Normal file
63
app/Http/Requests/Admin/Users/LecturerRequest.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Users;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class LecturerRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$userId = $this->route('user')?->id;
|
||||
|
||||
return [
|
||||
'username' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
'min:5',
|
||||
'alpha_dash',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
|
||||
// Profile
|
||||
'full_name' => ['required', 'string', 'max:150'],
|
||||
'phone_number' => ['required', 'string', 'max:20'],
|
||||
'address' => ['required', 'string'],
|
||||
'gender' => ['required', 'string', Rule::in(array_values(Gender::cases()))],
|
||||
'birth_date' => ['required', 'date'],
|
||||
'birth_place' => ['required', 'string', 'max:100'],
|
||||
|
||||
// Lecturer fields
|
||||
'lecturer_number' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
Rule::unique('lecturers')->ignore($userId, 'user_id'),
|
||||
],
|
||||
'department_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('departments', 'id'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
79
app/Http/Requests/Admin/Users/StudentRequest.php
Normal file
79
app/Http/Requests/Admin/Users/StudentRequest.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Users;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Enums\StudentStatus;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StudentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$userId = $this->route('user')?->id;
|
||||
|
||||
return [
|
||||
'username' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
'min:5',
|
||||
'alpha_dash',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
|
||||
// Profile
|
||||
'full_name' => ['required', 'string', 'max:150'],
|
||||
'phone_number' => ['required', 'string', 'max:20'],
|
||||
'address' => ['required', 'string'],
|
||||
'gender' => ['required', 'string', Rule::in(array_values(Gender::cases()))],
|
||||
'birth_date' => ['required', 'date'],
|
||||
'birth_place' => ['required', 'string', 'max:100'],
|
||||
|
||||
// Student fields
|
||||
'student_number' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:10',
|
||||
Rule::unique('students')->ignore($userId, 'user_id'),
|
||||
],
|
||||
'department_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('departments', 'id'),
|
||||
],
|
||||
'enrollment_year' => [
|
||||
'required',
|
||||
'integer',
|
||||
'digits:4',
|
||||
],
|
||||
'academic_advisor_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('lecturers', 'id'),
|
||||
],
|
||||
'status' => [
|
||||
'nullable',
|
||||
'string',
|
||||
Rule::in(array_values(StudentStatus::cases())),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
36
app/Models/Department.php
Normal file
36
app/Models/Department.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class Department extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public function lecturers(): HasMany
|
||||
{
|
||||
return $this->hasMany(Lecturer::class);
|
||||
}
|
||||
|
||||
public function students(): HasMany
|
||||
{
|
||||
return $this->hasMany(Student::class);
|
||||
}
|
||||
|
||||
public function leaderships(): HasMany
|
||||
{
|
||||
return $this->hasMany(DepartmentLeadership::class);
|
||||
}
|
||||
|
||||
public function currentLeader(): HasOne
|
||||
{
|
||||
return $this->hasOne(DepartmentLeadership::class)->whereNull('ended_at');
|
||||
}
|
||||
}
|
||||
32
app/Models/DepartmentLeadership.php
Normal file
32
app/Models/DepartmentLeadership.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class DepartmentLeadership extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'started_at' => 'date',
|
||||
'ended_at' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class);
|
||||
}
|
||||
|
||||
public function lecturer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Lecturer::class);
|
||||
}
|
||||
}
|
||||
36
app/Models/Lecturer.php
Normal file
36
app/Models/Lecturer.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class Lecturer extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class);
|
||||
}
|
||||
|
||||
public function advisees(): HasMany
|
||||
{
|
||||
return $this->hasMany(Student::class, 'academic_advisor_id');
|
||||
}
|
||||
|
||||
public function leaderships(): HasMany
|
||||
{
|
||||
return $this->hasMany(DepartmentLeadership::class);
|
||||
}
|
||||
}
|
||||
38
app/Models/Student.php
Normal file
38
app/Models/Student.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\StudentStatus;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class Student extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => StudentStatus::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class);
|
||||
}
|
||||
|
||||
public function academicAdvisor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Lecturer::class, 'academic_advisor_id');
|
||||
}
|
||||
}
|
||||
@ -32,13 +32,21 @@ protected function casts(): array
|
||||
|
||||
protected function fullName(): Attribute
|
||||
{
|
||||
return Attribute::get(function () {
|
||||
return $this->profile?->full_name ?? $this->username;
|
||||
});
|
||||
return Attribute::get(fn () => $this->profile?->full_name ?? $this->username);
|
||||
}
|
||||
|
||||
public function profile(): HasOne
|
||||
{
|
||||
return $this->hasOne(UserProfile::class);
|
||||
}
|
||||
|
||||
public function student(): HasOne
|
||||
{
|
||||
return $this->hasOne(Student::class);
|
||||
}
|
||||
|
||||
public function lecturer(): HasOne
|
||||
{
|
||||
return $this->hasOne(Lecturer::class);
|
||||
}
|
||||
}
|
||||
|
||||
38
app/Services/Admin/Master/DepartmentService.php
Normal file
38
app/Services/Admin/Master/DepartmentService.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Master;
|
||||
|
||||
use App\Models\Department;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class DepartmentService
|
||||
{
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return Department::query()
|
||||
->select(['id', 'code', 'name', 'degree_level'])
|
||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('code', 'like', "%{$search}%"))
|
||||
->orderBy($sort, $direction)
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
public function create(array $data): Department
|
||||
{
|
||||
return Department::create($data);
|
||||
}
|
||||
|
||||
public function update(Department $department, array $data): Department
|
||||
{
|
||||
$department->code = $data['code'];
|
||||
$department->name = $data['name'];
|
||||
$department->degree_level = $data['degree_level'] ?? null;
|
||||
$department->update();
|
||||
|
||||
return $department;
|
||||
}
|
||||
|
||||
public function delete(Department $department): bool
|
||||
{
|
||||
return $department->delete();
|
||||
}
|
||||
}
|
||||
86
app/Services/Admin/Users/AdministratorService.php
Normal file
86
app/Services/Admin/Users/AdministratorService.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Users;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AdministratorService
|
||||
{
|
||||
public function getPaginated(array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return User::with(['profile', 'roles'])
|
||||
->whereHas('roles', fn($q) => $q->whereIn('name', ['staff-admin', 'staff-keuangan']))
|
||||
->when($filters['search'] ?? null, fn($q, $search) => $q->where(function ($query) use ($search) {
|
||||
$query->where('username', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
->orWhereHas('profile', fn($q) => $q->where('full_name', 'like', "%{$search}%"));
|
||||
}))
|
||||
->latest()
|
||||
->paginate($filters['per_page'] ?? 25);
|
||||
}
|
||||
|
||||
public function create(array $data): User
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$user = User::create([
|
||||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make(config('app.default_password')),
|
||||
]);
|
||||
|
||||
$user->assignRole($data['role']);
|
||||
|
||||
$user->profile()->create([
|
||||
'full_name' => $data['full_name'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
'address' => $data['address'],
|
||||
'gender' => $data['gender'],
|
||||
'birth_date' => $data['birth_date'],
|
||||
'birth_place' => $data['birth_place'],
|
||||
]);
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
public function update(User $user, array $data): User
|
||||
{
|
||||
DB::transaction(function () use ($user, $data) {
|
||||
$user->update([
|
||||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
]);
|
||||
|
||||
$user->syncRoles($data['role']);
|
||||
|
||||
$user->profile()->updateOrCreate([], [
|
||||
'full_name' => $data['full_name'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
'address' => $data['address'],
|
||||
'gender' => $data['gender'],
|
||||
'birth_date' => $data['birth_date'],
|
||||
'birth_place' => $data['birth_place'],
|
||||
]);
|
||||
});
|
||||
|
||||
return $user->fresh(['profile', 'roles']);
|
||||
}
|
||||
|
||||
public function delete(User $user): void
|
||||
{
|
||||
DB::transaction(function () use ($user) {
|
||||
$user->profile()->delete();
|
||||
$user->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): void
|
||||
{
|
||||
$user->update([
|
||||
'password' => Hash::make(config('app.default_password')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
14
app/Services/Admin/Users/DepartmentService.php
Normal file
14
app/Services/Admin/Users/DepartmentService.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Users;
|
||||
|
||||
use App\Models\Department;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class DepartmentService
|
||||
{
|
||||
public function getAll(): Collection
|
||||
{
|
||||
return Department::select(['id', 'name'])->get();
|
||||
}
|
||||
}
|
||||
103
app/Services/Admin/Users/LecturerService.php
Normal file
103
app/Services/Admin/Users/LecturerService.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Users;
|
||||
|
||||
use App\Models\Lecturer;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class LecturerService
|
||||
{
|
||||
public function getAllForSelect(): Collection
|
||||
{
|
||||
return Lecturer::with('user.profile')->get();
|
||||
}
|
||||
|
||||
public function getPaginated(array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return User::with(['profile', 'lecturer.department'])
|
||||
->whereHas('roles', fn($q) => $q->where('name', 'dosen'))
|
||||
->when($filters['search'] ?? null, fn($q, $search) => $q->where(function ($query) use ($search) {
|
||||
$query->where('username', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
->orWhereHas('profile', fn($q) => $q->where('full_name', 'like', "%{$search}%"))
|
||||
->orWhereHas('lecturer', fn($q) => $q->where('lecturer_number', 'like', "%{$search}%"));
|
||||
}))
|
||||
->latest()
|
||||
->paginate($filters['per_page'] ?? 25);
|
||||
}
|
||||
|
||||
public function create(array $data): User
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$user = User::create([
|
||||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make(config('app.default_password')),
|
||||
]);
|
||||
|
||||
$user->assignRole('dosen');
|
||||
|
||||
$user->profile()->create([
|
||||
'full_name' => $data['full_name'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
'address' => $data['address'],
|
||||
'gender' => $data['gender'],
|
||||
'birth_date' => $data['birth_date'],
|
||||
'birth_place' => $data['birth_place'],
|
||||
]);
|
||||
|
||||
$user->lecturer()->create([
|
||||
'lecturer_number' => $data['lecturer_number'],
|
||||
'department_id' => $data['department_id'],
|
||||
]);
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
public function update(User $user, array $data): User
|
||||
{
|
||||
DB::transaction(function () use ($user, $data) {
|
||||
$user->update([
|
||||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
]);
|
||||
|
||||
$user->profile()->updateOrCreate([], [
|
||||
'full_name' => $data['full_name'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
'address' => $data['address'],
|
||||
'gender' => $data['gender'],
|
||||
'birth_date' => $data['birth_date'],
|
||||
'birth_place' => $data['birth_place'],
|
||||
]);
|
||||
|
||||
$user->lecturer()->updateOrCreate([], [
|
||||
'lecturer_number' => $data['lecturer_number'],
|
||||
'department_id' => $data['department_id'],
|
||||
]);
|
||||
});
|
||||
|
||||
return $user->fresh(['profile', 'lecturer.department', 'roles']);
|
||||
}
|
||||
|
||||
public function delete(User $user): void
|
||||
{
|
||||
DB::transaction(function () use ($user) {
|
||||
$user->lecturer()->delete();
|
||||
$user->profile()->delete();
|
||||
$user->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): void
|
||||
{
|
||||
$user->update([
|
||||
'password' => Hash::make(config('app.default_password')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
102
app/Services/Admin/Users/StudentService.php
Normal file
102
app/Services/Admin/Users/StudentService.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Users;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class StudentService
|
||||
{
|
||||
public function getPaginated(array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return User::with(['profile', 'student.department'])
|
||||
->whereHas('roles', fn($q) => $q->where('name', 'mahasiswa'))
|
||||
->when($filters['search'] ?? null, fn($q, $search) => $q->where(function ($query) use ($search) {
|
||||
$query->where('username', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
->orWhereHas('profile', fn($q) => $q->where('full_name', 'like', "%{$search}%"))
|
||||
->orWhereHas('student', fn($q) => $q->where('student_number', 'like', "%{$search}%"));
|
||||
}))
|
||||
->latest()
|
||||
->paginate($filters['per_page'] ?? 25);
|
||||
}
|
||||
|
||||
public function create(array $data): User
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$user = User::create([
|
||||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make(config('app.default_password')),
|
||||
]);
|
||||
|
||||
$user->assignRole('mahasiswa');
|
||||
|
||||
$user->profile()->create([
|
||||
'full_name' => $data['full_name'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
'address' => $data['address'],
|
||||
'gender' => $data['gender'],
|
||||
'birth_date' => $data['birth_date'],
|
||||
'birth_place' => $data['birth_place'],
|
||||
]);
|
||||
|
||||
$user->student()->create([
|
||||
'student_number' => $data['student_number'],
|
||||
'department_id' => $data['department_id'],
|
||||
'enrollment_year' => $data['enrollment_year'],
|
||||
'academic_advisor_id' => $data['academic_advisor_id'],
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
public function update(User $user, array $data): User
|
||||
{
|
||||
DB::transaction(function () use ($user, $data) {
|
||||
$user->update([
|
||||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
]);
|
||||
|
||||
$user->profile()->updateOrCreate([], [
|
||||
'full_name' => $data['full_name'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
'address' => $data['address'],
|
||||
'gender' => $data['gender'],
|
||||
'birth_date' => $data['birth_date'],
|
||||
'birth_place' => $data['birth_place'],
|
||||
]);
|
||||
|
||||
$user->student()->updateOrCreate([], [
|
||||
'student_number' => $data['student_number'],
|
||||
'department_id' => $data['department_id'],
|
||||
'enrollment_year' => $data['enrollment_year'],
|
||||
'academic_advisor_id' => $data['academic_advisor_id'],
|
||||
'status' => $data['status'] ?? null,
|
||||
]);
|
||||
});
|
||||
|
||||
return $user->fresh(['profile', 'student.department', 'roles']);
|
||||
}
|
||||
|
||||
public function delete(User $user): void
|
||||
{
|
||||
DB::transaction(function () use ($user) {
|
||||
$user->student()->delete();
|
||||
$user->profile()->delete();
|
||||
$user->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): void
|
||||
{
|
||||
$user->update([
|
||||
'password' => Hash::make(config('app.default_password')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -123,4 +123,5 @@
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
'default_password' => env('APP_DEFAULT_PASSWORD', 'Minimal8@'),
|
||||
];
|
||||
|
||||
@ -13,11 +13,11 @@ public function up(): void
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('full_name', 150);
|
||||
$table->string('phone_number', 20)->nullable();
|
||||
$table->text('address')->nullable();
|
||||
$table->enum('gender', array_values(Gender::cases()))->nullable();
|
||||
$table->string('birth_place', 100)->nullable();
|
||||
$table->date('birth_date')->nullable();
|
||||
$table->string('phone_number', 20);
|
||||
$table->text('address');
|
||||
$table->enum('gender', array_values(Gender::cases()));
|
||||
$table->date('birth_date');
|
||||
$table->string('birth_place', 100);
|
||||
$table->timestamps();
|
||||
$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('departments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code', 10)->unique();
|
||||
$table->string('name', 100);
|
||||
$table->string('degree_level', 10)->nullable()->default('S1');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('departments');
|
||||
}
|
||||
};
|
||||
@ -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('lecturers', function (Blueprint $table) {
|
||||
$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();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('lecturers');
|
||||
}
|
||||
};
|
||||
@ -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('department_leaderships', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('department_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('lecturer_id')->constrained()->cascadeOnDelete();
|
||||
$table->date('started_at');
|
||||
$table->date('ended_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('department_leaderships');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\StudentStatus;
|
||||
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('students', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('student_number', 10)->unique();
|
||||
$table->foreignId('department_id')->constrained()->cascadeOnDelete();
|
||||
$table->integer('enrollment_year');
|
||||
$table->foreignId('academic_advisor_id')->nullable()->constrained('lecturers')->nullOnDelete();
|
||||
$table->enum('status', array_values(StudentStatus::cases()))->nullable()->default(StudentStatus::Active);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('students');
|
||||
}
|
||||
};
|
||||
@ -13,8 +13,10 @@ public function run(): void
|
||||
{
|
||||
$this->call([
|
||||
RolePermissionSeeder::class,
|
||||
DepartmentSeeder::class,
|
||||
UserSeeder::class,
|
||||
AcademicTermSeeder::class,
|
||||
DepartmentLeadershipSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
39
database/seeders/DepartmentLeadershipSeeder.php
Normal file
39
database/seeders/DepartmentLeadershipSeeder.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\DepartmentLeadership;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DepartmentLeadershipSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
DepartmentLeadership::insert([
|
||||
[
|
||||
'department_id' => 1,
|
||||
'lecturer_id' => 1,
|
||||
'started_at' => '2024-01-10',
|
||||
'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(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
database/seeders/DepartmentSeeder.php
Normal file
36
database/seeders/DepartmentSeeder.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Department;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DepartmentSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
Department::insert([
|
||||
[
|
||||
'code' => 'SI',
|
||||
'name' => 'Sistem Informasi',
|
||||
'degree_level' => 'S1',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'code' => 'TI',
|
||||
'name' => 'Teknik Industri',
|
||||
'degree_level' => 'S1',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'code' => 'BD',
|
||||
'name' => 'Bisnis Digital',
|
||||
'degree_level' => 'S1',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -2,19 +2,67 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\StudentStatus;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UserSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
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',
|
||||
@ -27,6 +75,13 @@ public function run(): void
|
||||
'birth_place' => 'Subang',
|
||||
'birth_date' => '2005-03-13',
|
||||
],
|
||||
'student' => [
|
||||
'student_number' => '23010001',
|
||||
'department_id' => 1,
|
||||
'enrollment_year' => 2023,
|
||||
'academic_advisor_id' => 1,
|
||||
'status' => StudentStatus::Active,
|
||||
],
|
||||
],
|
||||
[
|
||||
'username' => 'doni',
|
||||
@ -40,6 +95,13 @@ public function run(): void
|
||||
'birth_place' => 'Purwakarta',
|
||||
'birth_date' => '2004-10-30',
|
||||
],
|
||||
'student' => [
|
||||
'student_number' => '23020002',
|
||||
'department_id' => 2,
|
||||
'enrollment_year' => 2023,
|
||||
'academic_advisor_id' => 2,
|
||||
'status' => StudentStatus::Active,
|
||||
],
|
||||
],
|
||||
[
|
||||
'username' => 'asep',
|
||||
@ -53,6 +115,13 @@ public function run(): void
|
||||
'birth_place' => 'Purwakarta',
|
||||
'birth_date' => '2005-09-24',
|
||||
],
|
||||
'student' => [
|
||||
'student_number' => '24010003',
|
||||
'department_id' => 1,
|
||||
'enrollment_year' => 2024,
|
||||
'academic_advisor_id' => 1,
|
||||
'status' => StudentStatus::Active,
|
||||
],
|
||||
],
|
||||
[
|
||||
'username' => 'komoala',
|
||||
@ -66,19 +135,41 @@ public function run(): void
|
||||
'birth_place' => 'Purwakarta',
|
||||
'birth_date' => '2005-05-04',
|
||||
],
|
||||
'student' => [
|
||||
'student_number' => '24020004',
|
||||
'department_id' => 2,
|
||||
'enrollment_year' => 2024,
|
||||
'academic_advisor_id' => 2,
|
||||
'status' => StudentStatus::Active,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($users as $userData) {
|
||||
$roles = $userData['roles'];
|
||||
$profile = $userData['profile'];
|
||||
|
||||
unset($userData['profile'], $userData['roles']);
|
||||
|
||||
$userData['password'] = Hash::make('Minimal8@');
|
||||
$student = $userData['student'] ?? null;
|
||||
$lecturer = $userData['lecturer'] ?? null;
|
||||
|
||||
unset($userData['profile'], $userData['student'], $userData['lecturer']);
|
||||
|
||||
$userData['password'] = Hash::make(config('app.default_password'));
|
||||
|
||||
$user = User::create($userData);
|
||||
$user->profile()->create($profile);
|
||||
|
||||
$user->assignRole($roles);
|
||||
|
||||
if ($lecturer) {
|
||||
$user->lecturer()->create($lecturer);
|
||||
}
|
||||
|
||||
if ($student) {
|
||||
$user->student()->create($student);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import { usePage } from "@inertiajs/react";
|
||||
import type { Icon } from "@tabler/icons-react";
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import type { Icon } from '@tabler/icons-react';
|
||||
import {
|
||||
IconGridDots,
|
||||
IconHelp,
|
||||
IconMessageDots
|
||||
} from "@tabler/icons-react";
|
||||
import * as React from "react";
|
||||
IconMessageDots,
|
||||
IconUsers,
|
||||
} from '@tabler/icons-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import AppLogoIcon from "@/components/app-logo-icon";
|
||||
import { NavMain, type NavGroup, type NavItem } from "@/components/nav-main";
|
||||
import { NavSecondary } from "@/components/nav-secondary";
|
||||
import AppLogoIcon from '@/components/app-logo-icon';
|
||||
import { NavMain, type NavGroup, type NavItem } from '@/components/nav-main';
|
||||
import { NavSecondary } from '@/components/nav-secondary';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@ -17,41 +18,73 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { index as academicTerm } from "@/routes/admin/master/academic-terms";
|
||||
import { Calendar } from "lucide-react";
|
||||
} from '@/components/ui/sidebar';
|
||||
import { index as academicTerm } from '@/routes/admin/master/academic-terms';
|
||||
import { index as departmentsRoute } from '@/routes/admin/master/departments';
|
||||
import { index as lecturersRoute } from '@/routes/admin/users/lecturers';
|
||||
import { index as studentsRoute } from '@/routes/admin/users/students';
|
||||
import { index as administratorsRoute } from '@/routes/admin/users/administrators';
|
||||
import { Building2, Calendar, GraduationCap, User, Users } from 'lucide-react';
|
||||
|
||||
const data: { navMain: (NavGroup | NavItem)[]; navSecondary: { title: string; url: string; icon: Icon }[] } = {
|
||||
const data: {
|
||||
navMain: (NavGroup | NavItem)[];
|
||||
navSecondary: { title: string; url: string; icon: Icon }[];
|
||||
} = {
|
||||
navMain: [
|
||||
{
|
||||
name: "Dasbor",
|
||||
url: "#",
|
||||
name: 'Dasbor',
|
||||
url: '#',
|
||||
icon: IconGridDots,
|
||||
},
|
||||
{
|
||||
label: "Master",
|
||||
label: 'Master',
|
||||
items: [
|
||||
{
|
||||
name: "Periode Akademik",
|
||||
name: 'Periode Akademik',
|
||||
url: academicTerm.url(),
|
||||
icon: Calendar,
|
||||
},
|
||||
{
|
||||
name: 'Jurusan',
|
||||
url: departmentsRoute.url(),
|
||||
icon: Building2,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Pengguna',
|
||||
items: [
|
||||
{
|
||||
name: 'Administrator',
|
||||
url: administratorsRoute.url(),
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
name: 'Dosen',
|
||||
url: lecturersRoute.url(),
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
name: 'Mahasiswa',
|
||||
url: studentsRoute.url(),
|
||||
icon: GraduationCap,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Kritik dan Saran",
|
||||
url: "#",
|
||||
title: 'Kritik dan Saran',
|
||||
url: '#',
|
||||
icon: IconMessageDots,
|
||||
},
|
||||
{
|
||||
title: "Bantuan",
|
||||
url: "#",
|
||||
title: 'Bantuan',
|
||||
url: '#',
|
||||
icon: IconHelp,
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const { name } = usePage().props;
|
||||
@ -67,7 +100,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
>
|
||||
<a href="#">
|
||||
<AppLogoIcon className="size-5! rounded-sm object-cover" />
|
||||
<span className="text-base font-semibold">{name}</span>
|
||||
<span className="text-base font-semibold">
|
||||
{name}
|
||||
</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
@ -78,5 +113,5 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
87
resources/js/pages/admin/master/departments/columns.tsx
Normal file
87
resources/js/pages/admin/master/departments/columns.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { Pencil, Trash2 } from 'lucide-react';
|
||||
import { RowActions } from '@/components/row-actions';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { Department } from '@/types/department';
|
||||
|
||||
export type { Department } from '@/types/department';
|
||||
|
||||
type CreateColumnsParams = {
|
||||
handleEdit: (department: Department) => void;
|
||||
handleDeleteClick: (department: Department) => void;
|
||||
};
|
||||
|
||||
export function createDepartmentColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<Department>[] {
|
||||
const { handleEdit, handleDeleteClick } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: () => <span>Kode</span>,
|
||||
cell: ({ row }) => (
|
||||
<span className="font-medium">
|
||||
{row.getValue('code') as string}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: () => <span>Nama</span>,
|
||||
cell: ({ row }) => <span>{row.getValue('name') as string}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: 'degree_level',
|
||||
header: () => <span className="block text-center">Jenjang</span>,
|
||||
meta: {
|
||||
className: 'w-[100px] text-center',
|
||||
headerClassName: 'w-[100px] text-center',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const degreeLevel = row.getValue('degree_level') as
|
||||
string | null;
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
{degreeLevel ? (
|
||||
<Badge variant="secondary">{degreeLevel}</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => <span className="block text-center">Aksi</span>,
|
||||
meta: {
|
||||
className: 'w-[100px] text-center',
|
||||
headerClassName: 'w-[100px] text-center',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const department = row.original;
|
||||
|
||||
return (
|
||||
<RowActions
|
||||
actions={[
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
onClick: () => handleEdit(department),
|
||||
},
|
||||
{
|
||||
label: 'Hapus',
|
||||
icon: (
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
),
|
||||
onClick: () => handleDeleteClick(department),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
298
resources/js/pages/admin/master/departments/index.tsx
Normal file
298
resources/js/pages/admin/master/departments/index.tsx
Normal file
@ -0,0 +1,298 @@
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
import { FormDialog } from '@/components/form-dialog';
|
||||
import InputError from '@/components/input-error';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
import {
|
||||
index as departmentIndex,
|
||||
destroy,
|
||||
store,
|
||||
update,
|
||||
} from '@/routes/admin/master/departments';
|
||||
import { DegreeLevels } from '@/types/department';
|
||||
import type { Department } from '@/types/department';
|
||||
import { createDepartmentColumns } from './columns';
|
||||
|
||||
type Props = {
|
||||
departments: {
|
||||
data: Department[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
export default function DepartmentIndex({ departments, highlight }: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Department | null>(null);
|
||||
const [deleting, setDeleting] = useState<Department | null>(null);
|
||||
|
||||
const pagination: PaginationState = {
|
||||
current_page: departments.current_page,
|
||||
last_page: departments.last_page,
|
||||
per_page: departments.per_page,
|
||||
total: departments.total,
|
||||
};
|
||||
|
||||
const {
|
||||
search,
|
||||
handlePageChange,
|
||||
handlePerPageChange,
|
||||
handleSearchChange,
|
||||
} = useServerTable({
|
||||
route: () => departmentIndex.url(),
|
||||
pagination,
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
if (!deleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.delete(destroy(deleting.id), {
|
||||
onSuccess: () => setDeleting(null),
|
||||
});
|
||||
}
|
||||
|
||||
const columns = createDepartmentColumns({
|
||||
handleEdit: (department) => setEditing(department),
|
||||
handleDeleteClick: (department) => setDeleting(department),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Jurusan" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Jurusan"
|
||||
description={
|
||||
highlight && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Menampilkan jurusan dari notifikasi.
|
||||
<button
|
||||
onClick={() => {
|
||||
router.get(
|
||||
departmentIndex.url(),
|
||||
{},
|
||||
{
|
||||
replace: true,
|
||||
preserveState: true,
|
||||
},
|
||||
);
|
||||
}}
|
||||
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
|
||||
>
|
||||
Tampilkan semua
|
||||
</button>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
actions={
|
||||
<Button asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</button>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<CreateForm open={createOpen} onOpenChange={setCreateOpen} />
|
||||
|
||||
<EditForm
|
||||
key={editing?.id}
|
||||
open={editing !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setEditing(null);
|
||||
}
|
||||
}}
|
||||
editing={editing}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={departments.data}
|
||||
searchKey="name"
|
||||
searchPlaceholder="Cari jurusan..."
|
||||
emptyText="Belum ada data jurusan."
|
||||
pagination={pagination}
|
||||
onPageChange={handlePageChange}
|
||||
onPerPageChange={handlePerPageChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
searchValue={search}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
target={deleting}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDeleting(null);
|
||||
}
|
||||
}}
|
||||
title="Hapus Jurusan"
|
||||
description={(department) =>
|
||||
`Apakah Anda yakin ingin menghapus jurusan "${department.name}"? Tindakan ini tidak dapat dibatalkan.`
|
||||
}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<FormDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title="Tambah Jurusan"
|
||||
action={store()}
|
||||
resetOnSuccess
|
||||
onSuccess={() => onOpenChange(false)}
|
||||
>
|
||||
{({ errors }) => (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="code">
|
||||
Kode <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input id="code" name="code" placeholder="Contoh: SI" />
|
||||
<InputError message={errors.code} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">
|
||||
Nama <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Masukkan nama jurusan"
|
||||
/>
|
||||
<InputError message={errors.name} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Jenjang</Label>
|
||||
<input type="hidden" name="degree_level" />
|
||||
<Select name="degree_level">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih jenjang" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DegreeLevels.map((level) => (
|
||||
<SelectItem key={level} value={level}>
|
||||
{level}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={errors.degree_level} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</FormDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function EditForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
editing,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editing: Department | null;
|
||||
}) {
|
||||
return (
|
||||
<FormDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title="Edit Jurusan"
|
||||
action={editing ? update(editing.id) : ''}
|
||||
resetOnSuccess
|
||||
onSuccess={() => onOpenChange(false)}
|
||||
>
|
||||
{({ errors }) =>
|
||||
editing && (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-code">
|
||||
Kode <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-code"
|
||||
name="code"
|
||||
placeholder="Contoh: SI"
|
||||
defaultValue={editing.code}
|
||||
/>
|
||||
<InputError message={errors.code} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-name">
|
||||
Nama <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
name="name"
|
||||
placeholder="Masukkan nama jurusan"
|
||||
defaultValue={editing.name}
|
||||
/>
|
||||
<InputError message={errors.name} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Jenjang</Label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="degree_level"
|
||||
defaultValue={editing.degree_level ?? ''}
|
||||
/>
|
||||
<Select
|
||||
name="degree_level"
|
||||
defaultValue={editing.degree_level ?? undefined}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih jenjang" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DegreeLevels.map((level) => (
|
||||
<SelectItem key={level} value={level}>
|
||||
{level}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={errors.degree_level} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</FormDialog>
|
||||
);
|
||||
}
|
||||
92
resources/js/pages/admin/users/administrators/columns.tsx
Normal file
92
resources/js/pages/admin/users/administrators/columns.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { RowActions } from '@/components/row-actions';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { Key, Pencil, Trash2 } from 'lucide-react';
|
||||
|
||||
export type Administrator = {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
profile: { full_name: string; phone_number: string; gender: string } | null;
|
||||
roles: { id: number; name: string }[];
|
||||
};
|
||||
|
||||
type CreateColumnsParams = {
|
||||
handleEdit: (admin: Administrator) => void;
|
||||
handleDeleteClick: (admin: Administrator) => void;
|
||||
handleResetPassword: (admin: Administrator) => void;
|
||||
};
|
||||
|
||||
export function createAdministratorColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<Administrator>[] {
|
||||
const { handleEdit, handleDeleteClick, handleResetPassword } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
accessorKey: 'profile.full_name',
|
||||
header: () => <span>Nama Lengkap</span>,
|
||||
cell: ({ row }) => row.original.profile?.full_name ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'account',
|
||||
header: () => <span>Akun</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex flex-col">
|
||||
<span>{row.original.username}</span>
|
||||
<span className="text-sm text-muted-foreground">{row.original.email}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'role',
|
||||
header: () => <span>Role</span>,
|
||||
cell: ({ row }) => row.original.roles?.[0]?.name ?? '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'profile.phone_number',
|
||||
header: () => <span>Nomor Telepon</span>,
|
||||
cell: ({ row }) => row.original.profile?.phone_number ?? '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'profile.gender',
|
||||
header: () => <span>Jenis Kelamin</span>,
|
||||
cell: ({ row }) => {
|
||||
const gender = row.original.profile?.gender;
|
||||
return gender === 'male' ? 'Laki-laki' : gender === 'female' ? 'Perempuan' : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => <span className="block text-center">Aksi</span>,
|
||||
meta: {
|
||||
className: 'w-[100px] text-center',
|
||||
headerClassName: 'w-[100px] text-center',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const admin = row.original;
|
||||
|
||||
return (
|
||||
<RowActions
|
||||
actions={[
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
onClick: () => handleEdit(admin),
|
||||
},
|
||||
{
|
||||
label: 'Reset Kata Sandi',
|
||||
icon: <Key className="h-4 w-4" />,
|
||||
onClick: () => handleResetPassword(admin),
|
||||
},
|
||||
{
|
||||
label: 'Hapus',
|
||||
icon: <Trash2 className="h-4 w-4 text-destructive" />,
|
||||
onClick: () => handleDeleteClick(admin),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
160
resources/js/pages/admin/users/administrators/create.tsx
Normal file
160
resources/js/pages/admin/users/administrators/create.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import { DatePicker } from '@/components/date-picker';
|
||||
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, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxList } 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 { Textarea } from '@/components/ui/textarea';
|
||||
import { index, store } from '@/routes/admin/users/administrators';
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { format } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Role = { id: number; name: string };
|
||||
|
||||
type Props = {
|
||||
roles: Role[];
|
||||
};
|
||||
|
||||
export default function AdministratorCreate({ roles }: Props) {
|
||||
const [gender, setGender] = useState('');
|
||||
const [birthDate, setBirthDate] = useState<Date | undefined>();
|
||||
const [role, setRole] = useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Tambah Administrator" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Tambah Administrator"
|
||||
actions={
|
||||
<Button variant="outline" asChild>
|
||||
<a href={index.url()}>Kembali</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Form action={store()} className="space-y-6">
|
||||
{({ errors, processing }) => (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Akun</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-3">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">
|
||||
Username <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Input id="email" name="email" type="email" placeholder="Masukkan email" />
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Role <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<input type="hidden" name="role" value={role} />
|
||||
<Combobox value={role} onValueChange={(v) => setRole(v ?? '')}>
|
||||
<ComboboxInput placeholder="Pilih role" className="w-full" />
|
||||
<ComboboxContent>
|
||||
<ComboboxList>
|
||||
{roles.map((r) => (
|
||||
<ComboboxItem key={r.id} value={r.name}>
|
||||
{r.name}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
<InputError message={errors.role} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profil Pribadi</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="female" id="female" />
|
||||
<Label htmlFor="female" className="font-normal">Perempuan</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<InputError message={errors.gender} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Tempat Lahir <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Textarea id="address" name="address" placeholder="Masukkan alamat" />
|
||||
<InputError message={errors.address} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={processing}>
|
||||
Simpan
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
171
resources/js/pages/admin/users/administrators/edit.tsx
Normal file
171
resources/js/pages/admin/users/administrators/edit.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
import { DatePicker } from '@/components/date-picker';
|
||||
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, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxList } 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 { Textarea } from '@/components/ui/textarea';
|
||||
import { index, update } from '@/routes/admin/users/administrators';
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { format } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Role = { id: number; name: string };
|
||||
|
||||
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;
|
||||
roles: { id: number; name: string }[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
roles: Role[];
|
||||
};
|
||||
|
||||
export default function AdministratorEdit({ user, roles }: 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
|
||||
);
|
||||
const [role, setRole] = useState(user.roles?.[0]?.name ?? '');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Edit Administrator" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Edit Administrator"
|
||||
actions={
|
||||
<Button variant="outline" asChild>
|
||||
<a href={index.url()}>Kembali</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Form action={update(user.id)} className="space-y-6">
|
||||
{({ errors, processing }) => (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Akun</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-3">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">
|
||||
Username <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Input id="email" name="email" type="email" defaultValue={user.email} />
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Role <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<input type="hidden" name="role" value={role} />
|
||||
<Combobox value={role} onValueChange={(v) => setRole(v ?? '')}>
|
||||
<ComboboxInput placeholder="Pilih role" className="w-full" />
|
||||
<ComboboxContent>
|
||||
<ComboboxList>
|
||||
{roles.map((r) => (
|
||||
<ComboboxItem key={r.id} value={r.name}>
|
||||
{r.name}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
<InputError message={errors.role} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profil Pribadi</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="female" id="female" />
|
||||
<Label htmlFor="female" className="font-normal">Perempuan</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<InputError message={errors.gender} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Tempat Lahir <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Textarea id="address" name="address" defaultValue={user.profile?.address ?? ''} />
|
||||
<InputError message={errors.address} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={processing}>
|
||||
Simpan
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
144
resources/js/pages/admin/users/administrators/index.tsx
Normal file
144
resources/js/pages/admin/users/administrators/index.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
import {
|
||||
index as administratorsIndex,
|
||||
create,
|
||||
destroy,
|
||||
edit,
|
||||
reset_password
|
||||
} from '@/routes/admin/users/administrators';
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { Administrator } from './columns';
|
||||
import { createAdministratorColumns } from './columns';
|
||||
|
||||
type Props = {
|
||||
administrators: {
|
||||
data: Administrator[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default function AdministratorIndex({ administrators }: Props) {
|
||||
const [deleting, setDeleting] = useState<Administrator | null>(null);
|
||||
const [resetting, setResetting] = useState<Administrator | null>(null);
|
||||
|
||||
const pagination: PaginationState = {
|
||||
current_page: administrators.current_page,
|
||||
last_page: administrators.last_page,
|
||||
per_page: administrators.per_page,
|
||||
total: administrators.total,
|
||||
};
|
||||
|
||||
const {
|
||||
search,
|
||||
handlePageChange,
|
||||
handlePerPageChange,
|
||||
handleSearchChange,
|
||||
} = useServerTable({
|
||||
route: () => administratorsIndex.url(),
|
||||
pagination,
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
if (!deleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.delete(destroy(deleting.id), {
|
||||
onSuccess: () => setDeleting(null),
|
||||
});
|
||||
}
|
||||
|
||||
function handleResetPassword() {
|
||||
if (!resetting) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.patch(reset_password.url(resetting.id), {}, {
|
||||
onSuccess: () => setResetting(null),
|
||||
});
|
||||
}
|
||||
|
||||
const columns = createAdministratorColumns({
|
||||
handleEdit: (admin) => {
|
||||
router.get(edit.url(admin.id));
|
||||
},
|
||||
handleDeleteClick: (admin) => setDeleting(admin),
|
||||
handleResetPassword: (admin) => setResetting(admin),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Administrator" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Administrator"
|
||||
actions={
|
||||
<Button asChild>
|
||||
<a href={create.url()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={administrators.data}
|
||||
searchKey="name"
|
||||
searchPlaceholder="Cari administrator..."
|
||||
emptyText="Belum ada data administrator."
|
||||
pagination={pagination}
|
||||
onPageChange={handlePageChange}
|
||||
onPerPageChange={handlePerPageChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
searchValue={search}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
target={deleting}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDeleting(null);
|
||||
}
|
||||
}}
|
||||
title="Hapus Administrator"
|
||||
description={(admin) =>
|
||||
`Apakah Anda yakin ingin menghapus administrator "${admin.profile?.full_name}"? Tindakan ini tidak dapat dibatalkan.`
|
||||
}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={resetting !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setResetting(null);
|
||||
}
|
||||
}}
|
||||
title="Reset Kata Sandi"
|
||||
description={
|
||||
resetting
|
||||
? `Apakah Anda yakin ingin mereset kata sandi administrator "${resetting.profile?.full_name}" ke kata sandi default?`
|
||||
: ''
|
||||
}
|
||||
confirmLabel="Reset"
|
||||
variant="default"
|
||||
onConfirm={handleResetPassword}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
84
resources/js/pages/admin/users/lecturers/columns.tsx
Normal file
84
resources/js/pages/admin/users/lecturers/columns.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import { RowActions } from '@/components/row-actions';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { Key, Pencil, Trash2 } from 'lucide-react';
|
||||
|
||||
export type Lecturer = {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
profile: { full_name: string } | null;
|
||||
lecturer: { lecturer_number: string; department: { name: string } | null } | null;
|
||||
};
|
||||
|
||||
type CreateColumnsParams = {
|
||||
handleEdit: (lecturer: Lecturer) => void;
|
||||
handleDeleteClick: (lecturer: Lecturer) => void;
|
||||
handleResetPassword: (lecturer: Lecturer) => void;
|
||||
};
|
||||
|
||||
export function createLecturerColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<Lecturer>[] {
|
||||
const { handleEdit, handleDeleteClick, handleResetPassword } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
accessorKey: 'lecturer.lecturer_number',
|
||||
header: () => <span>NIDN</span>,
|
||||
cell: ({ row }) => row.original.lecturer?.lecturer_number ?? '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'profile.full_name',
|
||||
header: () => <span>Nama Lengkap</span>,
|
||||
cell: ({ row }) => row.original.profile?.full_name ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'account',
|
||||
header: () => <span>Akun</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex flex-col">
|
||||
<span>{row.original.username}</span>
|
||||
<span className="text-sm text-muted-foreground">{row.original.email}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'lecturer.department.name',
|
||||
header: () => <span>Jurusan</span>,
|
||||
cell: ({ row }) => row.original.lecturer?.department?.name ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => <span className="block text-center">Aksi</span>,
|
||||
meta: {
|
||||
className: 'w-[100px] text-center',
|
||||
headerClassName: 'w-[100px] text-center',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const lecturer = row.original;
|
||||
|
||||
return (
|
||||
<RowActions
|
||||
actions={[
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
onClick: () => handleEdit(lecturer),
|
||||
},
|
||||
{
|
||||
label: 'Reset Password',
|
||||
icon: <Key className="h-4 w-4" />,
|
||||
onClick: () => handleResetPassword(lecturer),
|
||||
},
|
||||
{
|
||||
label: 'Hapus',
|
||||
icon: <Trash2 className="h-4 w-4 text-destructive" />,
|
||||
onClick: () => handleDeleteClick(lecturer),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
174
resources/js/pages/admin/users/lecturers/create.tsx
Normal file
174
resources/js/pages/admin/users/lecturers/create.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import { DatePicker } from '@/components/date-picker';
|
||||
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 { 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';
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { format } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Department = { id: number; name: string };
|
||||
|
||||
type Props = {
|
||||
departments: Department[];
|
||||
};
|
||||
|
||||
export default function LecturerCreate({ departments }: Props) {
|
||||
const [gender, setGender] = useState('');
|
||||
const [birthDate, setBirthDate] = useState<Date | undefined>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Tambah Dosen" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Tambah Dosen"
|
||||
actions={
|
||||
<Button variant="outline" asChild>
|
||||
<a href={index.url()}>Kembali</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Form action={store()} className="space-y-6">
|
||||
{({ errors, processing }) => (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Akun</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">
|
||||
Username <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Input id="email" name="email" type="email" placeholder="Masukkan email" />
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profil Pribadi</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="female" id="female" />
|
||||
<Label htmlFor="female" className="font-normal">Perempuan</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<InputError message={errors.gender} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Tempat Lahir <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Textarea id="address" name="address" placeholder="Masukkan alamat" />
|
||||
<InputError message={errors.address} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Dosen</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</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} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={processing}>
|
||||
Simpan
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
184
resources/js/pages/admin/users/lecturers/edit.tsx
Normal file
184
resources/js/pages/admin/users/lecturers/edit.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
import { DatePicker } from '@/components/date-picker';
|
||||
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 { 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';
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { format } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Department = { id: number; name: string };
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
departments: Department[];
|
||||
};
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Edit Dosen" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Edit Dosen"
|
||||
actions={
|
||||
<Button variant="outline" asChild>
|
||||
<a href={index.url()}>Kembali</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Form action={update(user.id)} className="space-y-6">
|
||||
{({ errors, processing }) => (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Akun</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">
|
||||
Username <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Input id="email" name="email" type="email" defaultValue={user.email} />
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profil Pribadi</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="female" id="female" />
|
||||
<Label htmlFor="female" className="font-normal">Perempuan</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<InputError message={errors.gender} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Tempat Lahir <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Textarea id="address" name="address" defaultValue={user.profile?.address ?? ''} />
|
||||
<InputError message={errors.address} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Dosen</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</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} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={processing}>
|
||||
Simpan
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
144
resources/js/pages/admin/users/lecturers/index.tsx
Normal file
144
resources/js/pages/admin/users/lecturers/index.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { PaginationState } from '@/components/data-table';
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
import {
|
||||
index as lecturersIndex,
|
||||
create,
|
||||
edit,
|
||||
destroy,
|
||||
reset_password,
|
||||
} from '@/routes/admin/users/lecturers';
|
||||
import type { Lecturer } from './columns';
|
||||
import { createLecturerColumns } from './columns';
|
||||
|
||||
type Props = {
|
||||
lecturers: {
|
||||
data: Lecturer[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default function LecturerIndex({ lecturers }: Props) {
|
||||
const [deleting, setDeleting] = useState<Lecturer | null>(null);
|
||||
const [resetting, setResetting] = useState<Lecturer | null>(null);
|
||||
|
||||
const pagination: PaginationState = {
|
||||
current_page: lecturers.current_page,
|
||||
last_page: lecturers.last_page,
|
||||
per_page: lecturers.per_page,
|
||||
total: lecturers.total,
|
||||
};
|
||||
|
||||
const {
|
||||
search,
|
||||
handlePageChange,
|
||||
handlePerPageChange,
|
||||
handleSearchChange,
|
||||
} = useServerTable({
|
||||
route: () => lecturersIndex.url(),
|
||||
pagination,
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
if (!deleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.delete(destroy(deleting.id), {
|
||||
onSuccess: () => setDeleting(null),
|
||||
});
|
||||
}
|
||||
|
||||
function handleResetPassword() {
|
||||
if (!resetting) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.patch(reset_password.url(resetting.id), {}, {
|
||||
onSuccess: () => setResetting(null),
|
||||
});
|
||||
}
|
||||
|
||||
const columns = createLecturerColumns({
|
||||
handleEdit: (lecturer) => {
|
||||
router.get(edit.url(lecturer.id));
|
||||
},
|
||||
handleDeleteClick: (lecturer) => setDeleting(lecturer),
|
||||
handleResetPassword: (lecturer) => setResetting(lecturer),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Dosen" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Dosen"
|
||||
actions={
|
||||
<Button asChild>
|
||||
<a href={create.url()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={lecturers.data}
|
||||
searchKey="name"
|
||||
searchPlaceholder="Cari dosen..."
|
||||
emptyText="Belum ada data dosen."
|
||||
pagination={pagination}
|
||||
onPageChange={handlePageChange}
|
||||
onPerPageChange={handlePerPageChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
searchValue={search}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
target={deleting}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDeleting(null);
|
||||
}
|
||||
}}
|
||||
title="Hapus Dosen"
|
||||
description={(lecturer) =>
|
||||
`Apakah Anda yakin ingin menghapus dosen "${lecturer.profile?.full_name}"? Tindakan ini tidak dapat dibatalkan.`
|
||||
}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={resetting !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setResetting(null);
|
||||
}
|
||||
}}
|
||||
title="Reset Kata Sandi"
|
||||
description={
|
||||
resetting
|
||||
? `Apakah Anda yakin ingin mereset kata sandi dosen "${resetting.profile?.full_name}" ke kata sandi default?`
|
||||
: ''
|
||||
}
|
||||
confirmLabel="Reset"
|
||||
variant="default"
|
||||
onConfirm={handleResetPassword}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
94
resources/js/pages/admin/users/students/columns.tsx
Normal file
94
resources/js/pages/admin/users/students/columns.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { RowActions } from '@/components/row-actions';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { Key, Pencil, Trash2 } from 'lucide-react';
|
||||
|
||||
export type Student = {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
profile: { full_name: string } | null;
|
||||
student: { student_number: string; department: { name: string } | null; enrollment_year: number; status: string | null } | null;
|
||||
};
|
||||
|
||||
type CreateColumnsParams = {
|
||||
handleEdit: (student: Student) => void;
|
||||
handleDeleteClick: (student: Student) => void;
|
||||
handleResetPassword: (student: Student) => void;
|
||||
};
|
||||
|
||||
export function createStudentColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<Student>[] {
|
||||
const { handleEdit, handleDeleteClick, handleResetPassword } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
accessorKey: 'student.student_number',
|
||||
header: () => <span>NIM</span>,
|
||||
cell: ({ row }) => row.original.student?.student_number ?? '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'profile.full_name',
|
||||
header: () => <span>Nama Lengkap</span>,
|
||||
cell: ({ row }) => row.original.profile?.full_name ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'account',
|
||||
header: () => <span>Akun</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex flex-col">
|
||||
<span>{row.original.username}</span>
|
||||
<span className="text-sm text-muted-foreground">{row.original.email}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'student.department.name',
|
||||
header: () => <span>Jurusan</span>,
|
||||
cell: ({ row }) => row.original.student?.department?.name ?? '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'student.enrollment_year',
|
||||
header: () => <span>Angkatan</span>,
|
||||
cell: ({ row }) => row.original.student?.enrollment_year ?? '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'student.status',
|
||||
header: () => <span>Status</span>,
|
||||
cell: ({ row }) => row.original.student?.status ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => <span className="block text-center">Aksi</span>,
|
||||
meta: {
|
||||
className: 'w-[100px] text-center',
|
||||
headerClassName: 'w-[100px] text-center',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const student = row.original;
|
||||
|
||||
return (
|
||||
<RowActions
|
||||
actions={[
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
onClick: () => handleEdit(student),
|
||||
},
|
||||
{
|
||||
label: 'Reset Password',
|
||||
icon: <Key className="h-4 w-4" />,
|
||||
onClick: () => handleResetPassword(student),
|
||||
},
|
||||
{
|
||||
label: 'Hapus',
|
||||
icon: <Trash2 className="h-4 w-4 text-destructive" />,
|
||||
onClick: () => handleDeleteClick(student),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
202
resources/js/pages/admin/users/students/create.tsx
Normal file
202
resources/js/pages/admin/users/students/create.tsx
Normal file
@ -0,0 +1,202 @@
|
||||
import { DatePicker } from '@/components/date-picker';
|
||||
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 { 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/students';
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { format } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Department = { id: number; name: string };
|
||||
type Lecturer = { id: number; lecturer_number: string; user: { profile: { full_name: string } | null } | null };
|
||||
|
||||
type Props = {
|
||||
departments: Department[];
|
||||
lecturers: Lecturer[];
|
||||
};
|
||||
|
||||
export default function StudentCreate({ departments, lecturers }: Props) {
|
||||
const [gender, setGender] = useState('');
|
||||
const [birthDate, setBirthDate] = useState<Date | undefined>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Tambah Mahasiswa" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Tambah Mahasiswa"
|
||||
actions={
|
||||
<Button variant="outline" asChild>
|
||||
<a href={index.url()}>Kembali</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Form action={store()} className="space-y-6">
|
||||
{({ errors, processing }) => (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Akun</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">
|
||||
Username <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Input id="email" name="email" type="email" placeholder="Masukkan email" />
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profil Pribadi</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="female" id="female" />
|
||||
<Label htmlFor="female" className="font-normal">Perempuan</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<InputError message={errors.gender} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Tempat Lahir <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Textarea id="address" name="address" placeholder="Masukkan alamat" />
|
||||
<InputError message={errors.address} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Mahasiswa</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="student_number">
|
||||
NIM <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input id="student_number" name="student_number" placeholder="Masukkan NIM" />
|
||||
<InputError message={errors.student_number} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
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} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="enrollment_year">
|
||||
Tahun Masuk <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input id="enrollment_year" name="enrollment_year" type="number" placeholder="Contoh: 2024" />
|
||||
<InputError message={errors.enrollment_year} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Dosen Wali <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<input type="hidden" name="academic_advisor_id" />
|
||||
<Select name="academic_advisor_id">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih dosen wali" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{lecturers.map((lect) => (
|
||||
<SelectItem key={lect.id} value={String(lect.id)}>
|
||||
{lect.user?.profile?.full_name ?? 'N/A'} - {lect.lecturer_number}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={errors.academic_advisor_id} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={processing}>
|
||||
Simpan
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
228
resources/js/pages/admin/users/students/edit.tsx
Normal file
228
resources/js/pages/admin/users/students/edit.tsx
Normal file
@ -0,0 +1,228 @@
|
||||
import { DatePicker } from '@/components/date-picker';
|
||||
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 { 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/students';
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { format } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Department = { id: number; name: string };
|
||||
type Lecturer = { id: number; lecturer_number: string; user: { profile: { full_name: string } | null } | null };
|
||||
|
||||
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;
|
||||
student: { student_number: string; department_id: number; enrollment_year: number; academic_advisor_id: number; status: string | null } | null;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
departments: Department[];
|
||||
lecturers: Lecturer[];
|
||||
};
|
||||
|
||||
export default function StudentEdit({ user, departments, lecturers }: 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
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Edit Mahasiswa" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Edit Mahasiswa"
|
||||
actions={
|
||||
<Button variant="outline" asChild>
|
||||
<a href={index.url()}>Kembali</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Form action={update(user.id)} className="space-y-6">
|
||||
{({ errors, processing }) => (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Akun</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">
|
||||
Username <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Input id="email" name="email" type="email" defaultValue={user.email} />
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profil Pribadi</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="female" id="female" />
|
||||
<Label htmlFor="female" className="font-normal">Perempuan</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<InputError message={errors.gender} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Tempat Lahir <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<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>
|
||||
</Label>
|
||||
<Textarea id="address" name="address" defaultValue={user.profile?.address ?? ''} />
|
||||
<InputError message={errors.address} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Mahasiswa</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="student_number">
|
||||
NIM <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input id="student_number" name="student_number" defaultValue={user.student?.student_number ?? ''} />
|
||||
<InputError message={errors.student_number} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Jurusan <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select name="department_id" defaultValue={user.student?.department_id ? String(user.student.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} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="enrollment_year">
|
||||
Tahun Masuk <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input id="enrollment_year" name="enrollment_year" type="number" defaultValue={user.student?.enrollment_year ?? ''} />
|
||||
<InputError message={errors.enrollment_year} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Dosen Wali <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select name="academic_advisor_id" defaultValue={user.student?.academic_advisor_id ? String(user.student.academic_advisor_id) : undefined}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih dosen wali" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{lecturers.map((lect) => (
|
||||
<SelectItem key={lect.id} value={String(lect.id)}>
|
||||
{lect.user?.profile?.full_name ?? 'N/A'} - {lect.lecturer_number}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={errors.academic_advisor_id} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Status <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select name="status" defaultValue={user.student?.status ?? undefined}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">Aktif</SelectItem>
|
||||
<SelectItem value="on_leave">Cuti</SelectItem>
|
||||
<SelectItem value="graduated">Lulus</SelectItem>
|
||||
<SelectItem value="dropped_out">Drop Out</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={errors.status} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={processing}>
|
||||
Simpan
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
143
resources/js/pages/admin/users/students/index.tsx
Normal file
143
resources/js/pages/admin/users/students/index.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { PaginationState } from '@/components/data-table';
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
import {
|
||||
create,
|
||||
destroy,
|
||||
edit,
|
||||
reset_password,
|
||||
index as studentsIndex,
|
||||
} from '@/routes/admin/users/students';
|
||||
import { createStudentColumns, type Student } from './columns';
|
||||
|
||||
type Props = {
|
||||
students: {
|
||||
data: Student[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default function StudentIndex({ students }: Props) {
|
||||
const [deleting, setDeleting] = useState<Student | null>(null);
|
||||
const [resetting, setResetting] = useState<Student | null>(null);
|
||||
|
||||
const pagination: PaginationState = {
|
||||
current_page: students.current_page,
|
||||
last_page: students.last_page,
|
||||
per_page: students.per_page,
|
||||
total: students.total,
|
||||
};
|
||||
|
||||
const {
|
||||
search,
|
||||
handlePageChange,
|
||||
handlePerPageChange,
|
||||
handleSearchChange,
|
||||
} = useServerTable({
|
||||
route: () => studentsIndex.url(),
|
||||
pagination,
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
if (!deleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.delete(destroy(deleting.id), {
|
||||
onSuccess: () => setDeleting(null),
|
||||
});
|
||||
}
|
||||
|
||||
function handleResetPassword() {
|
||||
if (!resetting) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.patch(reset_password.url(resetting.id), {}, {
|
||||
onSuccess: () => setResetting(null),
|
||||
});
|
||||
}
|
||||
|
||||
const columns = createStudentColumns({
|
||||
handleEdit: (student) => {
|
||||
router.get(edit.url(student.id));
|
||||
},
|
||||
handleDeleteClick: (student) => setDeleting(student),
|
||||
handleResetPassword: (student) => setResetting(student),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Mahasiswa" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Mahasiswa"
|
||||
actions={
|
||||
<Button asChild>
|
||||
<a href={create.url()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={students.data}
|
||||
searchKey="name"
|
||||
searchPlaceholder="Cari mahasiswa..."
|
||||
emptyText="Belum ada data mahasiswa."
|
||||
pagination={pagination}
|
||||
onPageChange={handlePageChange}
|
||||
onPerPageChange={handlePerPageChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
searchValue={search}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
target={deleting}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDeleting(null);
|
||||
}
|
||||
}}
|
||||
title="Hapus Mahasiswa"
|
||||
description={(student) =>
|
||||
`Apakah Anda yakin ingin menghapus mahasiswa "${student.profile?.full_name}"? Tindakan ini tidak dapat dibatalkan.`
|
||||
}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={resetting !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setResetting(null);
|
||||
}
|
||||
}}
|
||||
title="Reset Kata Sandi"
|
||||
description={
|
||||
resetting
|
||||
? `Apakah Anda yakin ingin mereset kata sandi mahasiswa "${resetting.profile?.full_name}" ke kata sandi default?`
|
||||
: ''
|
||||
}
|
||||
confirmLabel="Reset"
|
||||
variant="default"
|
||||
onConfirm={handleResetPassword}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
import { ChartAreaInteractive } from "@/components/chart-area-interactive";
|
||||
import { SectionCards } from "@/components/section-cards";
|
||||
import { SiteHeader } from "@/components/site-header";
|
||||
@ -17,7 +16,6 @@ export default function Page() {
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<AppSidebar variant="inset" />
|
||||
<SidebarInset>
|
||||
<SiteHeader />
|
||||
<div className="flex flex-1 flex-col">
|
||||
|
||||
12
resources/js/types/department.ts
Normal file
12
resources/js/types/department.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const DegreeLevels = ['D3', 'D4', 'S1', 'S2', 'S3'] as const;
|
||||
|
||||
export type DegreeLevel = (typeof DegreeLevels)[number];
|
||||
|
||||
export type Department = {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
degree_level: DegreeLevel | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
@ -2,3 +2,4 @@ export type * from './academic-term';
|
||||
export type * from './auth';
|
||||
export type * from './navigation';
|
||||
export type * from './ui';
|
||||
export type * from './user';
|
||||
|
||||
76
resources/js/types/user.ts
Normal file
76
resources/js/types/user.ts
Normal file
@ -0,0 +1,76 @@
|
||||
export enum Gender {
|
||||
Male = 'male',
|
||||
Female = 'female',
|
||||
}
|
||||
|
||||
export const GenderLabels: Record<Gender, string> = {
|
||||
[Gender.Male]: 'Laki-laki',
|
||||
[Gender.Female]: 'Perempuan',
|
||||
};
|
||||
|
||||
export enum StudentStatus {
|
||||
Active = 'active',
|
||||
OnLeave = 'on_leave',
|
||||
Graduated = 'graduated',
|
||||
DroppedOut = 'dropped_out',
|
||||
}
|
||||
|
||||
export const StudentStatusLabels: Record<StudentStatus, string> = {
|
||||
[StudentStatus.Active]: 'Aktif',
|
||||
[StudentStatus.OnLeave]: 'Cuti',
|
||||
[StudentStatus.Graduated]: 'Lulus',
|
||||
[StudentStatus.DroppedOut]: 'Drop Out',
|
||||
};
|
||||
|
||||
export type Department = {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type UserProfile = {
|
||||
full_name: string;
|
||||
phone_number: string | null;
|
||||
address: string | null;
|
||||
gender: Gender | null;
|
||||
birth_date: string | null;
|
||||
birth_place: string | null;
|
||||
};
|
||||
|
||||
export type Lecturer = {
|
||||
id: number;
|
||||
lecturer_number: string;
|
||||
department: Department | null;
|
||||
user?: {
|
||||
profile?: {
|
||||
full_name: string;
|
||||
} | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type Student = {
|
||||
id: number;
|
||||
student_number: string;
|
||||
department: Department | null;
|
||||
enrollment_year: number;
|
||||
academic_advisor: Lecturer | null;
|
||||
status: StudentStatus;
|
||||
};
|
||||
|
||||
export type Role = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type AdminUser = {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
is_active: boolean;
|
||||
roles: Role[];
|
||||
profile: UserProfile | null;
|
||||
lecturer: Lecturer | null;
|
||||
student: Student | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
@ -1,8 +1,26 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\Master\AcademicTermController;
|
||||
use App\Http\Controllers\Admin\Master\DepartmentController;
|
||||
use App\Http\Controllers\Admin\Users\AdministratorController;
|
||||
use App\Http\Controllers\Admin\Users\LecturerController;
|
||||
use App\Http\Controllers\Admin\Users\StudentController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(['auth', 'verified'])->prefix('admin/master')->name('admin.master.')->group(function () {
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::prefix('admin/master')->name('admin.master.')->group(function () {
|
||||
Route::resource('academic-terms', AcademicTermController::class)->except(['create', 'edit', 'show']);
|
||||
Route::resource('departments', DepartmentController::class)->except(['create', 'edit', 'show']);
|
||||
});
|
||||
|
||||
Route::prefix('admin/users')->name('admin.users.')->group(function () {
|
||||
Route::resource('lecturers', LecturerController::class)->except(['show'])->parameters(['lecturers' => 'user']);
|
||||
Route::patch('lecturers/{user}/reset-password', [LecturerController::class, 'resetPassword'])->name('lecturers.reset_password');
|
||||
|
||||
Route::resource('students', StudentController::class)->except(['show'])->parameters(['students' => 'user']);
|
||||
Route::patch('students/{user}/reset-password', [StudentController::class, 'resetPassword'])->name('students.reset_password');
|
||||
|
||||
Route::resource('administrators', AdministratorController::class)->except(['show'])->parameters(['administrators' => 'user']);
|
||||
Route::patch('administrators/{user}/reset-password', [AdministratorController::class, 'resetPassword'])->name('administrators.reset_password');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user