feat: add semester parity validation for course duplication and enhance UI for target term selection

This commit is contained in:
Yoga Pangestu 2026-09-02 17:53:40 +07:00
parent 5613078c00
commit c57787eb20
3 changed files with 60 additions and 5 deletions

View File

@ -3,6 +3,8 @@
namespace App\Http\Requests\Admin\Manage;
use App\Enums\ClassMethod;
use App\Enums\Semester;
use App\Models\AcademicTerm;
use App\Models\Course;
use App\Models\Lecturer;
use Illuminate\Foundation\Http\FormRequest;
@ -18,6 +20,26 @@ public function authorize(): bool
public function rules(): array
{
$academicTermId = $this->input('academic_term_id');
$academicTerm = $academicTermId ? AcademicTerm::find($academicTermId) : null;
$semesterParityRule = function ($attribute, $value, $fail) use ($academicTerm) {
if (! $academicTerm) {
return;
}
$semesterNumber = Course::find($value)?->semester_number;
if ($semesterNumber === null) {
return;
}
$isOddSemester = $semesterNumber % 2 === 1;
$matches = $academicTerm->semester === Semester::odd ? $isOddSemester : ! $isOddSemester;
if (! $matches) {
$fail('Mata kuliah semester '.$semesterNumber.' tidak sesuai dengan periode '.$academicTerm->semester->label().'.');
}
};
if ($this->isMethod('post')) {
return [
@ -28,6 +50,7 @@ public function rules(): array
Rule::unique('course_classes', 'course_id')
->where('academic_term_id', $academicTermId)
->whereNull('deleted_at'),
$semesterParityRule,
],
'lecturer_id' => [
'required',
@ -61,6 +84,7 @@ function ($attribute, $value, $fail) {
->where('academic_term_id', $academicTermId)
->whereNull('deleted_at')
->ignore($this->route('course_class')?->id),
$semesterParityRule,
],
'lecturer_id' => [
'required',

View File

@ -2,6 +2,7 @@
namespace App\Http\Requests\Admin\Manage;
use App\Models\AcademicTerm;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
@ -26,6 +27,15 @@ public function withValidator(Validator $validator): void
$validator->after(function (Validator $validator) {
if ($this->input('source_academic_term_id') === $this->input('target_academic_term_id')) {
$validator->errors()->add('target_academic_term_id', 'Periode tujuan harus berbeda dari periode sumber.');
return;
}
$sourceTerm = AcademicTerm::find($this->input('source_academic_term_id'));
$targetTerm = AcademicTerm::find($this->input('target_academic_term_id'));
if ($sourceTerm && $targetTerm && $sourceTerm->semester !== $targetTerm->semester) {
$validator->errors()->add('target_academic_term_id', 'Periode tujuan harus semester yang sama dengan periode sumber (Ganjil ke Ganjil, Genap ke Genap).');
}
});
}

View File

@ -810,6 +810,15 @@ function DuplicateForm({
const [sourceTermId, setSourceTermId] = useState('');
const [targetTermId, setTargetTermId] = useState('');
const sourceTerm = academicTerms.find(
(term) => String(term.id) === sourceTermId,
);
const targetOptions = academicTerms.filter(
(term) =>
String(term.id) !== sourceTermId &&
(!sourceTerm || term.semester === sourceTerm.semester),
);
function reset() {
setSourceTermId('');
setTargetTermId('');
@ -834,8 +843,10 @@ function DuplicateForm({
<p className="text-sm text-muted-foreground">
Menyalin semua kelas mata kuliah (mata kuliah, dosen
pengampu, metode) dari periode sumber ke periode
tujuan. Mata kuliah yang sudah punya kelas di periode
tujuan akan dilewati.
tujuan. Periode tujuan hanya bisa dipilih dari
semester yang sama (Ganjil ke Ganjil, Genap ke Genap).
Mata kuliah yang sudah punya kelas di periode tujuan
akan dilewati.
</p>
<div className="grid gap-2">
<Label>
@ -849,7 +860,10 @@ function DuplicateForm({
/>
<Select
value={sourceTermId}
onValueChange={setSourceTermId}
onValueChange={(value) => {
setSourceTermId(value);
setTargetTermId('');
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Pilih periode sumber" />
@ -882,12 +896,19 @@ function DuplicateForm({
<Select
value={targetTermId}
onValueChange={setTargetTermId}
disabled={!sourceTermId}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Pilih periode tujuan" />
<SelectValue
placeholder={
sourceTermId
? 'Pilih periode tujuan'
: 'Pilih periode sumber terlebih dahulu'
}
/>
</SelectTrigger>
<SelectContent>
{academicTerms.map((term) => (
{targetOptions.map((term) => (
<SelectItem
key={term.id}
value={String(term.id)}