feat: add lecturer management to Department module, including leadership fields and display in UI
This commit is contained in:
parent
10c3e42228
commit
2bb892ef50
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -55,6 +55,21 @@ export function createDepartmentColumns(
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'kaprodi',
|
||||
header: () => <span>Kaprodi</span>,
|
||||
cell: ({ row }) => {
|
||||
const leader = row.original.current_leader;
|
||||
|
||||
return leader ? (
|
||||
<span>
|
||||
{leader.lecturer?.user?.profile?.full_name ?? 'N/A'}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (canUpdate || canDelete) {
|
||||
|
||||
@ -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<Department | null>(null);
|
||||
const [deleting, setDeleting] = useState<Department | null>(null);
|
||||
@ -127,7 +146,11 @@ export default function DepartmentIndex({ departments, highlight }: Props) {
|
||||
}
|
||||
/>
|
||||
|
||||
<CreateForm open={createOpen} onOpenChange={setCreateOpen} />
|
||||
<CreateForm
|
||||
open={createOpen}
|
||||
onOpenChange={setCreateOpen}
|
||||
lecturers={lecturers}
|
||||
/>
|
||||
|
||||
<EditForm
|
||||
key={editing?.id}
|
||||
@ -138,6 +161,7 @@ export default function DepartmentIndex({ departments, highlight }: Props) {
|
||||
}
|
||||
}}
|
||||
editing={editing}
|
||||
lecturers={lecturers}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
@ -169,12 +193,113 @@ export default function DepartmentIndex({ departments, highlight }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function KaprodiFields({
|
||||
errors,
|
||||
lecturers,
|
||||
editing,
|
||||
}: {
|
||||
errors: Record<string, string>;
|
||||
lecturers: DepartmentLecturer[];
|
||||
editing?: Department | null;
|
||||
}) {
|
||||
const [lecturer, setLecturer] = useState<DepartmentLecturer | null>(
|
||||
editing?.current_leader?.lecturer ?? null,
|
||||
);
|
||||
const [startedAt, setStartedAt] = useState<Date | undefined>(
|
||||
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 (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Kaprodi{' '}
|
||||
{editing && (
|
||||
<span className="text-destructive">*</span>
|
||||
)}
|
||||
</Label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="lecturer_id"
|
||||
value={lecturer?.id ?? ''}
|
||||
/>
|
||||
<Combobox
|
||||
items={availableLecturers}
|
||||
value={lecturer}
|
||||
disabled={!editing}
|
||||
onValueChange={(value) => {
|
||||
setLecturer(value);
|
||||
|
||||
if (value && !startedAt) {
|
||||
setStartedAt(new Date());
|
||||
}
|
||||
}}
|
||||
itemToStringLabel={(l) => lecturerLabel(l)}
|
||||
isItemEqualToValue={(a, b) => a.id === b.id}
|
||||
>
|
||||
<ComboboxInput
|
||||
placeholder={
|
||||
editing
|
||||
? 'Pilih kaprodi'
|
||||
: 'Simpan jurusan terlebih dahulu'
|
||||
}
|
||||
disabled={!editing}
|
||||
className="w-full"
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>
|
||||
{editing
|
||||
? 'Belum ada dosen di jurusan ini.'
|
||||
: 'Simpan jurusan terlebih dahulu, lalu tambahkan dosen ke jurusan ini.'}
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(l: DepartmentLecturer) => (
|
||||
<ComboboxItem key={l.id} value={l}>
|
||||
{lecturerLabel(l)}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
<InputError message={errors.lecturer_id} />
|
||||
</div>
|
||||
{lecturer && (
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Mulai Menjabat{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="leadership_started_at"
|
||||
value={
|
||||
startedAt ? format(startedAt, 'yyyy-MM-dd') : ''
|
||||
}
|
||||
/>
|
||||
<DatePicker value={startedAt} onChange={setStartedAt} />
|
||||
<InputError message={errors.leadership_started_at} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
lecturers,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
lecturers: DepartmentLecturer[];
|
||||
}) {
|
||||
return (
|
||||
<FormDialog
|
||||
@ -222,6 +347,7 @@ function CreateForm({
|
||||
</Select>
|
||||
<InputError message={errors.degree_level} />
|
||||
</div>
|
||||
<KaprodiFields errors={errors} lecturers={lecturers} />
|
||||
</div>
|
||||
)}
|
||||
</FormDialog>
|
||||
@ -232,10 +358,12 @@ function EditForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
editing,
|
||||
lecturers,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editing: Department | null;
|
||||
lecturers: DepartmentLecturer[];
|
||||
}) {
|
||||
return (
|
||||
<FormDialog
|
||||
@ -297,6 +425,11 @@ function EditForm({
|
||||
</Select>
|
||||
<InputError message={errors.degree_level} />
|
||||
</div>
|
||||
<KaprodiFields
|
||||
errors={errors}
|
||||
lecturers={lecturers}
|
||||
editing={editing}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user