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')], 'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
'letter_type' => ['required', 'string', 'max:100'], 'letter_type' => ['required', 'string', 'max:100'],
'purpose' => ['nullable', 'string'], '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'], '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"> <DialogHeader className="shrink-0">
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
</DialogHeader> </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' {typeof children === 'function'
? children({ errors, processing }) ? children({ errors, processing })
: children} : 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 type { PaginationState } from '@/components/data-table';
import { DataTable } from '@/components/data-table'; import { DataTable } from '@/components/data-table';
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
@ -27,10 +31,6 @@ import {
} from '@/routes/admin/feedback'; } from '@/routes/admin/feedback';
import type { Feedback } from '@/types/feedback'; import type { Feedback } from '@/types/feedback';
import { FeedbackStatusLabels, FeedbackTypeLabels } 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'; import { createFeedbackColumns, FeedbackStatusVariants } from './columns';
type FeedbackTypeOption = { value: string; label: string }; type FeedbackTypeOption = { value: string; label: string };
@ -159,8 +159,8 @@ export default function FeedbackIndex({
<Info /> <Info />
<AlertTitle>Ubah status Kritik dan Saran</AlertTitle> <AlertTitle>Ubah status Kritik dan Saran</AlertTitle>
<AlertDescription> <AlertDescription>
Klik badge Status pada tabel untuk mengubah status Kritik dan Saran Klik badge Status pada tabel untuk mengubah status
secara langsung. Kritik dan Saran secara langsung.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@ -217,7 +217,7 @@ function ViewDetailDialog({
</DialogHeader> </DialogHeader>
{feedback && ( {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"> <div className="flex flex-wrap items-center gap-2">
<Badge variant="outline"> <Badge variant="outline">
{FeedbackTypeLabels[feedback.type]} {FeedbackTypeLabels[feedback.type]}

View File

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

View File

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