feat: update LetterRequestRequest validation and enhance student selection with Combobox in forms
This commit is contained in:
parent
8be0d57e16
commit
04f8f770bb
@ -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'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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]}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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'}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user