diff --git a/app/Http/Controllers/Admin/Master/DepartmentController.php b/app/Http/Controllers/Admin/Master/DepartmentController.php index 19b12ab..fd5983d 100644 --- a/app/Http/Controllers/Admin/Master/DepartmentController.php +++ b/app/Http/Controllers/Admin/Master/DepartmentController.php @@ -7,6 +7,7 @@ use App\Http\Requests\PaginatedRequest; use App\Models\Department; use App\Services\Admin\Master\DepartmentService; +use App\Services\Admin\Users\LecturerService; use Illuminate\Http\RedirectResponse; use Inertia\Inertia; use Inertia\Response; @@ -15,12 +16,14 @@ class DepartmentController extends Controller { public function __construct( private readonly DepartmentService $service, + private readonly LecturerService $lecturerService, ) {} public function index(PaginatedRequest $request): Response { return Inertia::render('admin/master/departments/index', [ 'departments' => $this->service->paginated(...$request->validatedWithDefaults()), + 'lecturers' => $this->lecturerService->getAllForSelect(), ]); } diff --git a/app/Http/Requests/Admin/Master/DepartmentRequest.php b/app/Http/Requests/Admin/Master/DepartmentRequest.php index 0f84faf..8db44cf 100644 --- a/app/Http/Requests/Admin/Master/DepartmentRequest.php +++ b/app/Http/Requests/Admin/Master/DepartmentRequest.php @@ -23,6 +23,14 @@ public function rules(): array ], 'name' => ['required', 'string', 'max:100'], 'degree_level' => ['nullable', 'string', Rule::in(['D3', 'D4', 'S1', 'S2', 'S3'])], + 'lecturer_id' => [ + 'nullable', + Rule::requiredIf($this->route('department') !== null), + 'integer', + Rule::exists('lecturer_department', 'lecturer_id') + ->where('department_id', $this->route('department')?->id ?? 0), + ], + 'leadership_started_at' => ['nullable', 'date', 'required_with:lecturer_id'], ]; } } diff --git a/app/Services/Admin/Master/DepartmentService.php b/app/Services/Admin/Master/DepartmentService.php index eb1252b..591136f 100644 --- a/app/Services/Admin/Master/DepartmentService.php +++ b/app/Services/Admin/Master/DepartmentService.php @@ -3,6 +3,7 @@ namespace App\Services\Admin\Master; use App\Models\Department; +use App\Models\DepartmentLeadership; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Collection; @@ -17,6 +18,7 @@ public function paginated(int $perPage = 25, string $search = ''): LengthAwarePa { return Department::query() ->select(['id', 'code', 'name', 'degree_level']) + ->with(['currentLeader.lecturer.user.profile']) ->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('code', 'like', "%{$search}%")) ->latest() ->paginate($perPage); @@ -24,7 +26,15 @@ public function paginated(int $perPage = 25, string $search = ''): LengthAwarePa public function create(array $data): Department { - return Department::create($data); + $department = Department::create([ + 'code' => $data['code'], + 'name' => $data['name'], + 'degree_level' => $data['degree_level'] ?? null, + ]); + + $this->syncLeadership($department, $data['lecturer_id'] ?? null, $data['leadership_started_at'] ?? null); + + return $department; } public function update(Department $department, array $data): Department @@ -34,9 +44,38 @@ public function update(Department $department, array $data): Department $department->degree_level = $data['degree_level'] ?? null; $department->update(); + $this->syncLeadership($department, $data['lecturer_id'] ?? null, $data['leadership_started_at'] ?? null); + return $department; } + private function syncLeadership(Department $department, ?int $lecturerId, ?string $startedAt): void + { + $currentLeader = DepartmentLeadership::query() + ->where('department_id', $department->id) + ->whereNull('ended_at') + ->first(); + + if (! $lecturerId) { + $currentLeader?->update(['ended_at' => now()]); + + return; + } + + if ($currentLeader && $currentLeader->lecturer_id === $lecturerId) { + $currentLeader->update(['started_at' => $startedAt ?? $currentLeader->started_at]); + + return; + } + + $currentLeader?->update(['ended_at' => now()]); + + $department->leaderships()->create([ + 'lecturer_id' => $lecturerId, + 'started_at' => $startedAt ?? now(), + ]); + } + public function delete(Department $department): bool { return $department->delete(); diff --git a/resources/js/pages/admin/master/departments/columns.tsx b/resources/js/pages/admin/master/departments/columns.tsx index 2394106..921a5bd 100644 --- a/resources/js/pages/admin/master/departments/columns.tsx +++ b/resources/js/pages/admin/master/departments/columns.tsx @@ -55,6 +55,21 @@ export function createDepartmentColumns( ); }, }, + { + id: 'kaprodi', + header: () => Kaprodi, + cell: ({ row }) => { + const leader = row.original.current_leader; + + return leader ? ( + + {leader.lecturer?.user?.profile?.full_name ?? 'N/A'} + + ) : ( + - + ); + }, + }, ]; if (canUpdate || canDelete) { diff --git a/resources/js/pages/admin/master/departments/index.tsx b/resources/js/pages/admin/master/departments/index.tsx index 362532b..ae15349 100644 --- a/resources/js/pages/admin/master/departments/index.tsx +++ b/resources/js/pages/admin/master/departments/index.tsx @@ -1,13 +1,23 @@ import { Head, router } from '@inertiajs/react'; +import { format } from 'date-fns'; import { Plus } from 'lucide-react'; import { useState } from 'react'; import type { PaginationState } from '@/components/data-table'; import { DataTable } from '@/components/data-table'; +import { DatePicker } from '@/components/date-picker'; 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 { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, +} from '@/components/ui/combobox'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { @@ -26,7 +36,7 @@ import { update, } from '@/routes/admin/master/departments'; import { DegreeLevels } from '@/types/department'; -import type { Department } from '@/types/department'; +import type { Department, DepartmentLecturer } from '@/types/department'; import { createDepartmentColumns } from './columns'; type Props = { @@ -37,10 +47,19 @@ type Props = { per_page: number; total: number; }; + lecturers: DepartmentLecturer[]; highlight?: number; }; -export default function DepartmentIndex({ departments, highlight }: Props) { +function lecturerLabel(lecturer: DepartmentLecturer): string { + return `${lecturer.user?.profile?.full_name ?? 'N/A'} - ${lecturer.lecturer_number}`; +} + +export default function DepartmentIndex({ + departments, + lecturers, + highlight, +}: Props) { const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); @@ -127,7 +146,11 @@ export default function DepartmentIndex({ departments, highlight }: Props) { } /> - + ; + lecturers: DepartmentLecturer[]; + editing?: Department | null; +}) { + const [lecturer, setLecturer] = useState( + editing?.current_leader?.lecturer ?? null, + ); + const [startedAt, setStartedAt] = useState( + editing?.current_leader + ? new Date(editing.current_leader.started_at) + : undefined, + ); + + const availableLecturers = editing + ? lecturers.filter((l) => + l.departments.some((d) => d.id === editing.id), + ) + : []; + + return ( + <> +
+ + + { + setLecturer(value); + + if (value && !startedAt) { + setStartedAt(new Date()); + } + }} + itemToStringLabel={(l) => lecturerLabel(l)} + isItemEqualToValue={(a, b) => a.id === b.id} + > + + + + {editing + ? 'Belum ada dosen di jurusan ini.' + : 'Simpan jurusan terlebih dahulu, lalu tambahkan dosen ke jurusan ini.'} + + + {(l: DepartmentLecturer) => ( + + {lecturerLabel(l)} + + )} + + + + +
+ {lecturer && ( +
+ + + + +
+ )} + + ); +} + function CreateForm({ open, onOpenChange, + lecturers, }: { open: boolean; onOpenChange: (open: boolean) => void; + lecturers: DepartmentLecturer[]; }) { return ( + )} @@ -232,10 +358,12 @@ function EditForm({ open, onOpenChange, editing, + lecturers, }: { open: boolean; onOpenChange: (open: boolean) => void; editing: Department | null; + lecturers: DepartmentLecturer[]; }) { return ( + ) } diff --git a/resources/js/types/department.ts b/resources/js/types/department.ts index e555475..537a796 100644 --- a/resources/js/types/department.ts +++ b/resources/js/types/department.ts @@ -2,11 +2,27 @@ export const DegreeLevels = ['D3', 'D4', 'S1', 'S2', 'S3'] as const; export type DegreeLevel = (typeof DegreeLevels)[number]; +export type DepartmentLecturer = { + id: number; + lecturer_number: string; + user: { profile: { full_name: string } | null } | null; + departments: { id: number }[]; +}; + +export type DepartmentLeadership = { + id: number; + lecturer_id: number; + started_at: string; + ended_at: string | null; + lecturer: DepartmentLecturer | null; +}; + export type Department = { id: number; code: string; name: string; degree_level: DegreeLevel | null; + current_leader: DepartmentLeadership | null; created_at: string; updated_at: string; };