feat: implement bulk creation of course classes and enhance validation for lecturer assignment
This commit is contained in:
parent
584056e379
commit
03c3fc61af
@ -41,9 +41,14 @@ public function index(PaginatedRequest $request): Response
|
|||||||
|
|
||||||
public function store(CourseClassRequest $request): RedirectResponse
|
public function store(CourseClassRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->service->create($request->validated());
|
$created = $this->service->createMany($request->validated());
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kelas mata kuliah berhasil ditambahkan.']);
|
Inertia::flash('toast', [
|
||||||
|
'type' => 'success',
|
||||||
|
'message' => $created->count() > 1
|
||||||
|
? "{$created->count()} kelas mata kuliah berhasil ditambahkan."
|
||||||
|
: 'Kelas mata kuliah berhasil ditambahkan.',
|
||||||
|
]);
|
||||||
|
|
||||||
return to_route('admin.manage.course-classes.index');
|
return to_route('admin.manage.course-classes.index');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Enums\ClassMethod;
|
use App\Enums\ClassMethod;
|
||||||
use App\Models\Course;
|
use App\Models\Course;
|
||||||
|
use App\Models\Lecturer;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
@ -16,10 +17,51 @@ public function authorize(): bool
|
|||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
$academicTermId = $this->input('academic_term_id');
|
||||||
|
|
||||||
|
if ($this->isMethod('post')) {
|
||||||
|
return [
|
||||||
|
'course_ids' => ['required', 'array', 'min:1'],
|
||||||
|
'course_ids.*' => [
|
||||||
|
'integer',
|
||||||
|
Rule::exists('courses', 'id'),
|
||||||
|
Rule::unique('course_classes', 'course_id')
|
||||||
|
->where('academic_term_id', $academicTermId)
|
||||||
|
->whereNull('deleted_at'),
|
||||||
|
],
|
||||||
|
'lecturer_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
function ($attribute, $value, $fail) {
|
||||||
|
$courseIds = (array) $this->input('course_ids', []);
|
||||||
|
$departmentIds = Course::whereIn('id', $courseIds)->pluck('department_id')->unique();
|
||||||
|
$lecturer = Lecturer::find($value);
|
||||||
|
$coveredDepartmentIds = $lecturer
|
||||||
|
? $lecturer->departments()->pluck('departments.id')
|
||||||
|
: collect();
|
||||||
|
|
||||||
|
if ($departmentIds->diff($coveredDepartmentIds)->isNotEmpty()) {
|
||||||
|
$fail('Dosen tidak terdaftar di jurusan seluruh mata kuliah yang dipilih.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
|
||||||
|
'method' => ['nullable', 'string', Rule::in(ClassMethod::values())],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$departmentId = Course::find($this->input('course_id'))?->department_id;
|
$departmentId = Course::find($this->input('course_id'))?->department_id;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'course_id' => ['required', 'integer', Rule::exists('courses', 'id')],
|
'course_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('courses', 'id'),
|
||||||
|
Rule::unique('course_classes', 'course_id')
|
||||||
|
->where('academic_term_id', $academicTermId)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->ignore($this->route('course_class')?->id),
|
||||||
|
],
|
||||||
'lecturer_id' => [
|
'lecturer_id' => [
|
||||||
'required',
|
'required',
|
||||||
'integer',
|
'integer',
|
||||||
|
|||||||
@ -28,9 +28,18 @@ public function paginated(int $perPage = 25, string $search = '', ?int $academic
|
|||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): CourseClass
|
/**
|
||||||
|
* @param array{course_ids: array<int, int>, lecturer_id: int, academic_term_id: int, method?: string|null} $data
|
||||||
|
* @return Collection<int, CourseClass>
|
||||||
|
*/
|
||||||
|
public function createMany(array $data): Collection
|
||||||
{
|
{
|
||||||
return CourseClass::create($data);
|
return new Collection(array_map(fn (int $courseId) => CourseClass::create([
|
||||||
|
'course_id' => $courseId,
|
||||||
|
'lecturer_id' => $data['lecturer_id'],
|
||||||
|
'academic_term_id' => $data['academic_term_id'],
|
||||||
|
'method' => $data['method'] ?? null,
|
||||||
|
]), $data['course_ids']));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(CourseClass $courseClass, array $data): CourseClass
|
public function update(CourseClass $courseClass, array $data): CourseClass
|
||||||
|
|||||||
@ -12,6 +12,9 @@ import { PageHeader } from '@/components/page-header';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Combobox,
|
Combobox,
|
||||||
|
ComboboxChip,
|
||||||
|
ComboboxChips,
|
||||||
|
ComboboxChipsInput,
|
||||||
ComboboxCollection,
|
ComboboxCollection,
|
||||||
ComboboxContent,
|
ComboboxContent,
|
||||||
ComboboxEmpty,
|
ComboboxEmpty,
|
||||||
@ -20,6 +23,7 @@ import {
|
|||||||
ComboboxItem,
|
ComboboxItem,
|
||||||
ComboboxLabel,
|
ComboboxLabel,
|
||||||
ComboboxList,
|
ComboboxList,
|
||||||
|
useComboboxAnchor,
|
||||||
} from '@/components/ui/combobox';
|
} from '@/components/ui/combobox';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import {
|
import {
|
||||||
@ -299,27 +303,37 @@ function CreateForm({
|
|||||||
courseClassAssignments: CourseClassAssignment[];
|
courseClassAssignments: CourseClassAssignment[];
|
||||||
}) {
|
}) {
|
||||||
const [academicTermId, setAcademicTermId] = useState('');
|
const [academicTermId, setAcademicTermId] = useState('');
|
||||||
const [course, setCourse] = useState<Course | null>(null);
|
const [selectedCourses, setSelectedCourses] = useState<Course[]>([]);
|
||||||
const [lecturer, setLecturer] = useState<Lecturer | null>(null);
|
const [lecturer, setLecturer] = useState<Lecturer | null>(null);
|
||||||
|
const courseAnchor = useComboboxAnchor();
|
||||||
|
|
||||||
const takenCourseIds = new Set(
|
const takenCourseIds = new Set(
|
||||||
courseClassAssignments
|
courseClassAssignments
|
||||||
.filter((a) => String(a.academic_term_id) === academicTermId)
|
.filter((a) => String(a.academic_term_id) === academicTermId)
|
||||||
.map((a) => a.course_id),
|
.map((a) => a.course_id),
|
||||||
);
|
);
|
||||||
const availableCourses = courses.filter((c) => !takenCourseIds.has(c.id));
|
const selectedCourseIds = new Set(selectedCourses.map((c) => c.id));
|
||||||
|
const availableCourses = courses.filter(
|
||||||
|
(c) => !takenCourseIds.has(c.id) && !selectedCourseIds.has(c.id),
|
||||||
|
);
|
||||||
const courseGroups = groupCoursesByDepartmentAndSemester(availableCourses);
|
const courseGroups = groupCoursesByDepartmentAndSemester(availableCourses);
|
||||||
const availableLecturers = course
|
const selectedDepartmentIds = new Set(
|
||||||
? lecturers.filter((l) =>
|
selectedCourses.map((c) => c.department_id),
|
||||||
l.departments.some(
|
);
|
||||||
(department) => department.id === course.department_id,
|
const availableLecturers =
|
||||||
),
|
selectedCourses.length > 0
|
||||||
)
|
? lecturers.filter((l) =>
|
||||||
: lecturers;
|
[...selectedDepartmentIds].every((departmentId) =>
|
||||||
|
l.departments.some(
|
||||||
|
(department) => department.id === departmentId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: lecturers;
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
setAcademicTermId('');
|
setAcademicTermId('');
|
||||||
setCourse(null);
|
setSelectedCourses([]);
|
||||||
setLecturer(null);
|
setLecturer(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,7 +365,7 @@ function CreateForm({
|
|||||||
value={academicTermId}
|
value={academicTermId}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setAcademicTermId(value);
|
setAcademicTermId(value);
|
||||||
setCourse(null);
|
setSelectedCourses([]);
|
||||||
setLecturer(null);
|
setLecturer(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -376,26 +390,43 @@ function CreateForm({
|
|||||||
Mata Kuliah{' '}
|
Mata Kuliah{' '}
|
||||||
<span className="text-destructive">*</span>
|
<span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<input
|
{selectedCourses.map((c) => (
|
||||||
type="hidden"
|
<input
|
||||||
name="course_id"
|
key={c.id}
|
||||||
value={course?.id ?? ''}
|
type="hidden"
|
||||||
/>
|
name="course_ids[]"
|
||||||
|
value={c.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<Combobox
|
<Combobox
|
||||||
items={courseGroups}
|
items={courseGroups}
|
||||||
value={course}
|
multiple
|
||||||
|
value={selectedCourses}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setCourse(value);
|
setSelectedCourses(value);
|
||||||
setLecturer(null);
|
setLecturer(null);
|
||||||
}}
|
}}
|
||||||
itemToStringLabel={(c) => `${c.code} - ${c.name}`}
|
itemToStringLabel={(c) => `${c.code} - ${c.name}`}
|
||||||
isItemEqualToValue={(a, b) => a.id === b.id}
|
isItemEqualToValue={(a, b) => a.id === b.id}
|
||||||
>
|
>
|
||||||
<ComboboxInput
|
<ComboboxChips ref={courseAnchor}>
|
||||||
placeholder="Pilih mata kuliah"
|
{selectedCourses.map((c) => (
|
||||||
className="w-full"
|
<ComboboxChip
|
||||||
/>
|
key={c.id}
|
||||||
<ComboboxContent>
|
aria-label={`${c.code} - ${c.name}`}
|
||||||
|
>
|
||||||
|
{c.code}
|
||||||
|
</ComboboxChip>
|
||||||
|
))}
|
||||||
|
<ComboboxChipsInput
|
||||||
|
placeholder={
|
||||||
|
selectedCourses.length === 0
|
||||||
|
? 'Pilih mata kuliah (bisa lebih dari satu)'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ComboboxChips>
|
||||||
|
<ComboboxContent anchor={courseAnchor}>
|
||||||
<ComboboxEmpty>
|
<ComboboxEmpty>
|
||||||
Mata kuliah tidak ditemukan.
|
Mata kuliah tidak ditemukan.
|
||||||
</ComboboxEmpty>
|
</ComboboxEmpty>
|
||||||
@ -423,7 +454,7 @@ function CreateForm({
|
|||||||
</ComboboxList>
|
</ComboboxList>
|
||||||
</ComboboxContent>
|
</ComboboxContent>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
<InputError message={errors.course_id} />
|
<InputError message={errors.course_ids} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
@ -443,12 +474,12 @@ function CreateForm({
|
|||||||
`${lect.user?.profile?.full_name ?? 'N/A'} - ${lect.lecturer_number}`
|
`${lect.user?.profile?.full_name ?? 'N/A'} - ${lect.lecturer_number}`
|
||||||
}
|
}
|
||||||
isItemEqualToValue={(a, b) => a.id === b.id}
|
isItemEqualToValue={(a, b) => a.id === b.id}
|
||||||
disabled={!course}
|
disabled={selectedCourses.length === 0}
|
||||||
>
|
>
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
disabled={!course}
|
disabled={selectedCourses.length === 0}
|
||||||
placeholder={
|
placeholder={
|
||||||
course
|
selectedCourses.length > 0
|
||||||
? 'Pilih dosen pengampu'
|
? 'Pilih dosen pengampu'
|
||||||
: 'Pilih mata kuliah terlebih dahulu'
|
: 'Pilih mata kuliah terlebih dahulu'
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user