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\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\Department;
|
use App\Models\Department;
|
||||||
use App\Services\Admin\Master\DepartmentService;
|
use App\Services\Admin\Master\DepartmentService;
|
||||||
|
use App\Services\Admin\Users\LecturerService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
@ -15,12 +16,14 @@ class DepartmentController extends Controller
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly DepartmentService $service,
|
private readonly DepartmentService $service,
|
||||||
|
private readonly LecturerService $lecturerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/master/departments/index', [
|
return Inertia::render('admin/master/departments/index', [
|
||||||
'departments' => $this->service->paginated(...$request->validatedWithDefaults()),
|
'departments' => $this->service->paginated(...$request->validatedWithDefaults()),
|
||||||
|
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,14 @@ public function rules(): array
|
|||||||
],
|
],
|
||||||
'name' => ['required', 'string', 'max:100'],
|
'name' => ['required', 'string', 'max:100'],
|
||||||
'degree_level' => ['nullable', 'string', Rule::in(['D3', 'D4', 'S1', 'S2', 'S3'])],
|
'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;
|
namespace App\Services\Admin\Master;
|
||||||
|
|
||||||
use App\Models\Department;
|
use App\Models\Department;
|
||||||
|
use App\Models\DepartmentLeadership;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ public function paginated(int $perPage = 25, string $search = ''): LengthAwarePa
|
|||||||
{
|
{
|
||||||
return Department::query()
|
return Department::query()
|
||||||
->select(['id', 'code', 'name', 'degree_level'])
|
->select(['id', 'code', 'name', 'degree_level'])
|
||||||
|
->with(['currentLeader.lecturer.user.profile'])
|
||||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('code', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('code', 'like', "%{$search}%"))
|
||||||
->latest()
|
->latest()
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
@ -24,7 +26,15 @@ public function paginated(int $perPage = 25, string $search = ''): LengthAwarePa
|
|||||||
|
|
||||||
public function create(array $data): Department
|
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
|
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->degree_level = $data['degree_level'] ?? null;
|
||||||
$department->update();
|
$department->update();
|
||||||
|
|
||||||
|
$this->syncLeadership($department, $data['lecturer_id'] ?? null, $data['leadership_started_at'] ?? null);
|
||||||
|
|
||||||
return $department;
|
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
|
public function delete(Department $department): bool
|
||||||
{
|
{
|
||||||
return $department->delete();
|
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) {
|
if (canUpdate || canDelete) {
|
||||||
|
|||||||
@ -1,13 +1,23 @@
|
|||||||
import { Head, router } from '@inertiajs/react';
|
import { Head, router } from '@inertiajs/react';
|
||||||
|
import { format } from 'date-fns';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { PaginationState } from '@/components/data-table';
|
import type { PaginationState } from '@/components/data-table';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
|
import { DatePicker } from '@/components/date-picker';
|
||||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||||
import { FormDialog } from '@/components/form-dialog';
|
import { FormDialog } from '@/components/form-dialog';
|
||||||
import InputError from '@/components/input-error';
|
import InputError from '@/components/input-error';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Combobox,
|
||||||
|
ComboboxContent,
|
||||||
|
ComboboxEmpty,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxItem,
|
||||||
|
ComboboxList,
|
||||||
|
} from '@/components/ui/combobox';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import {
|
import {
|
||||||
@ -26,7 +36,7 @@ import {
|
|||||||
update,
|
update,
|
||||||
} from '@/routes/admin/master/departments';
|
} from '@/routes/admin/master/departments';
|
||||||
import { DegreeLevels } from '@/types/department';
|
import { DegreeLevels } from '@/types/department';
|
||||||
import type { Department } from '@/types/department';
|
import type { Department, DepartmentLecturer } from '@/types/department';
|
||||||
import { createDepartmentColumns } from './columns';
|
import { createDepartmentColumns } from './columns';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -37,10 +47,19 @@ type Props = {
|
|||||||
per_page: number;
|
per_page: number;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
lecturers: DepartmentLecturer[];
|
||||||
highlight?: number;
|
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 [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<Department | null>(null);
|
const [editing, setEditing] = useState<Department | null>(null);
|
||||||
const [deleting, setDeleting] = 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
|
<EditForm
|
||||||
key={editing?.id}
|
key={editing?.id}
|
||||||
@ -138,6 +161,7 @@ export default function DepartmentIndex({ departments, highlight }: Props) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
editing={editing}
|
editing={editing}
|
||||||
|
lecturers={lecturers}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataTable
|
<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({
|
function CreateForm({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
lecturers,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
|
lecturers: DepartmentLecturer[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<FormDialog
|
<FormDialog
|
||||||
@ -222,6 +347,7 @@ function CreateForm({
|
|||||||
</Select>
|
</Select>
|
||||||
<InputError message={errors.degree_level} />
|
<InputError message={errors.degree_level} />
|
||||||
</div>
|
</div>
|
||||||
|
<KaprodiFields errors={errors} lecturers={lecturers} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormDialog>
|
</FormDialog>
|
||||||
@ -232,10 +358,12 @@ function EditForm({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
editing,
|
editing,
|
||||||
|
lecturers,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
editing: Department | null;
|
editing: Department | null;
|
||||||
|
lecturers: DepartmentLecturer[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<FormDialog
|
<FormDialog
|
||||||
@ -297,6 +425,11 @@ function EditForm({
|
|||||||
</Select>
|
</Select>
|
||||||
<InputError message={errors.degree_level} />
|
<InputError message={errors.degree_level} />
|
||||||
</div>
|
</div>
|
||||||
|
<KaprodiFields
|
||||||
|
errors={errors}
|
||||||
|
lecturers={lecturers}
|
||||||
|
editing={editing}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,11 +2,27 @@ export const DegreeLevels = ['D3', 'D4', 'S1', 'S2', 'S3'] as const;
|
|||||||
|
|
||||||
export type DegreeLevel = (typeof DegreeLevels)[number];
|
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 = {
|
export type Department = {
|
||||||
id: number;
|
id: number;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
degree_level: DegreeLevel | null;
|
degree_level: DegreeLevel | null;
|
||||||
|
current_leader: DepartmentLeadership | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user