feat: update LetterRequestRequest validation and enhance student selection with Combobox in forms

This commit is contained in:
Yoga Pangestu 2026-08-31 16:25:36 +07:00
parent 8be0d57e16
commit 04f8f770bb
5 changed files with 119 additions and 74 deletions

View File

@ -30,7 +30,7 @@ public function rules(): array
'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
'letter_type' => ['required', 'string', 'max:100'],
'purpose' => ['nullable', 'string'],
'status' => ['nullable', Rule::enum(LetterStatus::class)],
'status' => ['required', Rule::enum(LetterStatus::class)],
'result' => ['nullable', 'file', 'max:10240', 'mimes:pdf,jpg,jpeg,png,doc,docx'],
];
}

View File

@ -62,7 +62,7 @@ export function FormDialog({
<DialogHeader className="shrink-0">
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
<div className="grid flex-1 gap-4 overflow-y-auto py-4">
<div className="grid flex-1 gap-4 overflow-x-hidden overflow-y-auto py-4">
{typeof children === 'function'
? children({ errors, processing })
: children}

View File

@ -1,3 +1,7 @@
import { Head, Link, router } from '@inertiajs/react';
import { format } from 'date-fns';
import { Info, 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';
@ -27,10 +31,6 @@ import {
} from '@/routes/admin/feedback';
import type { Feedback } from '@/types/feedback';
import { FeedbackStatusLabels, FeedbackTypeLabels } from '@/types/feedback';
import { Head, Link, router } from '@inertiajs/react';
import { format } from 'date-fns';
import { Info, Plus } from 'lucide-react';
import { useState } from 'react';
import { createFeedbackColumns, FeedbackStatusVariants } from './columns';
type FeedbackTypeOption = { value: string; label: string };
@ -159,8 +159,8 @@ export default function FeedbackIndex({
<Info />
<AlertTitle>Ubah status Kritik dan Saran</AlertTitle>
<AlertDescription>
Klik badge Status pada tabel untuk mengubah status Kritik dan Saran
secara langsung.
Klik badge Status pada tabel untuk mengubah status
Kritik dan Saran secara langsung.
</AlertDescription>
</Alert>
@ -217,7 +217,7 @@ function ViewDetailDialog({
</DialogHeader>
{feedback && (
<div className="grid min-h-0 flex-1 gap-4 overflow-y-auto">
<div className="grid min-h-0 flex-1 gap-4 overflow-x-hidden overflow-y-auto">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline">
{FeedbackTypeLabels[feedback.type]}

View File

@ -11,15 +11,16 @@ 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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { usePermissions } from '@/hooks/use-permissions';
import { useServerTable } from '@/hooks/use-server-table';
@ -218,60 +219,80 @@ function AdvisingLogFields({
students: AcademicAdvisingLogStudent[];
lecturers: AcademicAdvisingLogLecturer[];
}) {
const [student, setStudent] = useState<AcademicAdvisingLogStudent | null>(
editing?.student ?? null,
);
const [lecturer, setLecturer] =
useState<AcademicAdvisingLogLecturer | null>(editing?.lecturer ?? null);
return (
<>
<div className="grid gap-2">
<Label>
Mahasiswa <span className="text-destructive">*</span>
</Label>
{!editing && <input type="hidden" name="student_id" />}
<Select
<input
type="hidden"
name="student_id"
defaultValue={
editing ? String(editing.student_id) : undefined
}
value={student?.id ?? ''}
/>
<Combobox
items={students}
value={student}
onValueChange={setStudent}
itemToStringLabel={(s) => studentLabel(s)}
isItemEqualToValue={(a, b) => a.id === b.id}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Pilih mahasiswa" />
</SelectTrigger>
<SelectContent>
{students.map((student) => (
<SelectItem
key={student.id}
value={String(student.id)}
>
{studentLabel(student)}
</SelectItem>
))}
</SelectContent>
</Select>
<ComboboxInput
placeholder="Pilih mahasiswa"
className="w-full"
/>
<ComboboxContent>
<ComboboxEmpty>
Mahasiswa tidak ditemukan.
</ComboboxEmpty>
<ComboboxList>
{(s: AcademicAdvisingLogStudent) => (
<ComboboxItem key={s.id} value={s}>
{studentLabel(s)}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
<InputError message={errors.student_id} />
</div>
<div className="grid gap-2">
<Label>
Dosen Wali <span className="text-destructive">*</span>
</Label>
{!editing && <input type="hidden" name="lecturer_id" />}
<Select
<input
type="hidden"
name="lecturer_id"
defaultValue={
editing ? String(editing.lecturer_id) : undefined
}
value={lecturer?.id ?? ''}
/>
<Combobox
items={lecturers}
value={lecturer}
onValueChange={setLecturer}
itemToStringLabel={(l) => lecturerLabel(l)}
isItemEqualToValue={(a, b) => a.id === b.id}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Pilih dosen" />
</SelectTrigger>
<SelectContent>
{lecturers.map((lecturer) => (
<SelectItem
key={lecturer.id}
value={String(lecturer.id)}
>
{lecturerLabel(lecturer)}
</SelectItem>
))}
</SelectContent>
</Select>
<ComboboxInput
placeholder="Pilih dosen"
className="w-full"
/>
<ComboboxContent>
<ComboboxEmpty>Dosen tidak ditemukan.</ComboboxEmpty>
<ComboboxList>
{(l: AcademicAdvisingLogLecturer) => (
<ComboboxItem key={l.id} value={l}>
{lecturerLabel(l)}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
<InputError message={errors.lecturer_id} />
</div>
<div className="grid gap-2">
@ -330,6 +351,7 @@ function CreateForm({
{({ errors }) => (
<div className="grid gap-4">
<AdvisingLogFields
key={open ? 'open' : 'closed'}
errors={errors}
students={students}
lecturers={lecturers}

View File

@ -11,6 +11,14 @@ 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 {
@ -210,33 +218,45 @@ function LetterRequestFields({
students: LetterRequestStudent[];
resetKey?: string;
}) {
const [student, setStudent] = useState<LetterRequestStudent | null>(
editing?.student ?? null,
);
return (
<>
<div className="grid gap-2">
<Label>
Mahasiswa <span className="text-destructive">*</span>
</Label>
{!editing && <input type="hidden" name="student_id" />}
<Select
<input
type="hidden"
name="student_id"
defaultValue={
editing ? String(editing.student_id) : undefined
}
value={student?.id ?? ''}
/>
<Combobox
items={students}
value={student}
onValueChange={setStudent}
itemToStringLabel={(s) => studentLabel(s)}
isItemEqualToValue={(a, b) => a.id === b.id}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Pilih mahasiswa" />
</SelectTrigger>
<SelectContent>
{students.map((student) => (
<SelectItem
key={student.id}
value={String(student.id)}
>
{studentLabel(student)}
</SelectItem>
))}
</SelectContent>
</Select>
<ComboboxInput
placeholder="Pilih mahasiswa"
className="w-full"
/>
<ComboboxContent>
<ComboboxEmpty>
Mahasiswa tidak ditemukan.
</ComboboxEmpty>
<ComboboxList>
{(s: LetterRequestStudent) => (
<ComboboxItem key={s.id} value={s}>
{studentLabel(s)}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
<InputError message={errors.student_id} />
</div>
<div className="grid gap-2">
@ -264,7 +284,9 @@ function LetterRequestFields({
<InputError message={errors.purpose} />
</div>
<div className="grid gap-2">
<Label>Status</Label>
<Label>
Status <span className="text-destructive">*</span>
</Label>
<input type="hidden" name="status" />
<Select
name="status"
@ -316,6 +338,7 @@ function CreateForm({
{({ errors }) => (
<div className="grid gap-4">
<LetterRequestFields
key={open ? 'open' : 'closed'}
errors={errors}
students={students}
resetKey={open ? 'open' : 'closed'}