From 73f41729d18115e2faaa32ae942cc1d8e9ffca15 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Mon, 24 Aug 2026 18:32:44 +0700 Subject: [PATCH] feat: add schedule management functionality with CRUD operations --- .../Admin/Manage/ScheduleController.php | 55 ++ .../Requests/Admin/Manage/ScheduleRequest.php | 26 + app/Models/CourseClass.php | 5 + app/Models/Schedule.php | 19 + app/Services/Admin/Manage/ScheduleService.php | 48 ++ ...26_08_24_000003_create_schedules_table.php | 27 + database/seeders/DatabaseSeeder.php | 1 + database/seeders/ScheduleSeeder.php | 35 ++ resources/js/components/app-sidebar.tsx | 7 + .../js/pages/admin/manage/schedules/index.tsx | 486 ++++++++++++++++++ resources/js/types/schedule.ts | 38 ++ routes/admin.php | 3 + 12 files changed, 750 insertions(+) create mode 100644 app/Http/Controllers/Admin/Manage/ScheduleController.php create mode 100644 app/Http/Requests/Admin/Manage/ScheduleRequest.php create mode 100644 app/Models/Schedule.php create mode 100644 app/Services/Admin/Manage/ScheduleService.php create mode 100644 database/migrations/2026_08_24_000003_create_schedules_table.php create mode 100644 database/seeders/ScheduleSeeder.php create mode 100644 resources/js/pages/admin/manage/schedules/index.tsx create mode 100644 resources/js/types/schedule.ts diff --git a/app/Http/Controllers/Admin/Manage/ScheduleController.php b/app/Http/Controllers/Admin/Manage/ScheduleController.php new file mode 100644 index 0000000..ab2f482 --- /dev/null +++ b/app/Http/Controllers/Admin/Manage/ScheduleController.php @@ -0,0 +1,55 @@ + $this->service->all(), + 'courseClasses' => $this->courseClassService->getAllForSelect(), + ]); + } + + public function store(ScheduleRequest $request): RedirectResponse + { + $this->service->create($request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Jadwal berhasil ditambahkan.']); + + return to_route('admin.manage.schedules.index'); + } + + public function update(ScheduleRequest $request, Schedule $schedule): RedirectResponse + { + $this->service->update($schedule, $request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Jadwal berhasil diperbarui.']); + + return to_route('admin.manage.schedules.index'); + } + + public function destroy(Schedule $schedule): RedirectResponse + { + $this->service->delete($schedule); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Jadwal berhasil dihapus.']); + + return back(); + } +} diff --git a/app/Http/Requests/Admin/Manage/ScheduleRequest.php b/app/Http/Requests/Admin/Manage/ScheduleRequest.php new file mode 100644 index 0000000..769f6c7 --- /dev/null +++ b/app/Http/Requests/Admin/Manage/ScheduleRequest.php @@ -0,0 +1,26 @@ + ['required', 'integer', Rule::exists('course_classes', 'id')], + 'day_of_week' => ['nullable', 'string', 'max:15'], + 'start_time' => ['nullable', 'date_format:H:i'], + 'end_time' => ['nullable', 'date_format:H:i', 'after:start_time'], + 'room' => ['nullable', 'string', 'max:20'], + 'online_link' => ['nullable', 'string', 'max:255', 'url'], + ]; + } +} diff --git a/app/Models/CourseClass.php b/app/Models/CourseClass.php index ed23153..a41ecde 100644 --- a/app/Models/CourseClass.php +++ b/app/Models/CourseClass.php @@ -51,4 +51,9 @@ public function assignments(): HasMany { return $this->hasMany(Assignment::class); } + + public function schedules(): HasMany + { + return $this->hasMany(Schedule::class); + } } diff --git a/app/Models/Schedule.php b/app/Models/Schedule.php new file mode 100644 index 0000000..c0d8d5e --- /dev/null +++ b/app/Models/Schedule.php @@ -0,0 +1,19 @@ +belongsTo(CourseClass::class); + } +} diff --git a/app/Services/Admin/Manage/ScheduleService.php b/app/Services/Admin/Manage/ScheduleService.php new file mode 100644 index 0000000..23cc88b --- /dev/null +++ b/app/Services/Admin/Manage/ScheduleService.php @@ -0,0 +1,48 @@ +select(['id', 'course_class_id', 'day_of_week', 'start_time', 'end_time', 'room', 'online_link']) + ->with('courseClass.course:id,code,name') + ->orderBy('start_time') + ->get(); + } + + public function create(array $data): Schedule + { + return Schedule::create([ + 'course_class_id' => $data['course_class_id'], + 'day_of_week' => $data['day_of_week'] ?? null, + 'start_time' => $data['start_time'] ?? null, + 'end_time' => $data['end_time'] ?? null, + 'room' => $data['room'] ?? null, + 'online_link' => $data['online_link'] ?? null, + ]); + } + + public function update(Schedule $schedule, array $data): Schedule + { + $schedule->course_class_id = $data['course_class_id']; + $schedule->day_of_week = $data['day_of_week'] ?? null; + $schedule->start_time = $data['start_time'] ?? null; + $schedule->end_time = $data['end_time'] ?? null; + $schedule->room = $data['room'] ?? null; + $schedule->online_link = $data['online_link'] ?? null; + $schedule->update(); + + return $schedule; + } + + public function delete(Schedule $schedule): bool + { + return $schedule->delete(); + } +} diff --git a/database/migrations/2026_08_24_000003_create_schedules_table.php b/database/migrations/2026_08_24_000003_create_schedules_table.php new file mode 100644 index 0000000..d41eca8 --- /dev/null +++ b/database/migrations/2026_08_24_000003_create_schedules_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('course_class_id')->constrained()->cascadeOnDelete(); + $table->string('day_of_week', 15)->nullable(); + $table->time('start_time')->nullable(); + $table->time('end_time')->nullable(); + $table->string('room', 20)->nullable(); + $table->string('online_link', 255)->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('schedules'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index b7cb346..ea9058c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -23,6 +23,7 @@ public function run(): void MaterialSeeder::class, AssignmentSeeder::class, SubmissionSeeder::class, + ScheduleSeeder::class, ]); } } diff --git a/database/seeders/ScheduleSeeder.php b/database/seeders/ScheduleSeeder.php new file mode 100644 index 0000000..6465276 --- /dev/null +++ b/database/seeders/ScheduleSeeder.php @@ -0,0 +1,35 @@ + 1, + 'day_of_week' => 'Monday', + 'start_time' => '08:00:00', + 'end_time' => '10:00:00', + 'room' => 'R.301', + 'online_link' => null, + 'created_at' => '2026-01-15 08:00:00', + 'updated_at' => '2026-01-15 08:00:00', + ], + [ + 'course_class_id' => 2, + 'day_of_week' => 'Tuesday', + 'start_time' => '10:00:00', + 'end_time' => '12:00:00', + 'room' => null, + 'online_link' => 'https://meet.google.com/xyz-abcd', + 'created_at' => '2026-01-15 08:00:00', + 'updated_at' => '2026-01-15 08:00:00', + ], + ]); + } +} diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index a7c07e2..9138639 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -10,6 +10,7 @@ import { BookOpen, Building2, Calendar, + CalendarClock, ClipboardList, FileText, GraduationCap, @@ -35,6 +36,7 @@ import { index as assignmentsRoute } from '@/routes/admin/manage/assignments'; import { index as courseClassesRoute } from '@/routes/admin/manage/course-classes'; import { index as coursesRoute } from '@/routes/admin/manage/courses'; import { index as materialsRoute } from '@/routes/admin/manage/materials'; +import { index as schedulesRoute } from '@/routes/admin/manage/schedules'; import { index as academicTerm } from '@/routes/admin/master/academic-terms'; import { index as departmentsRoute } from '@/routes/admin/master/departments'; import { index as administratorsRoute } from '@/routes/admin/users/administrators'; @@ -94,6 +96,11 @@ const data: { url: assignmentsRoute.url(), icon: ClipboardList, }, + { + name: 'Jadwal', + url: schedulesRoute.url(), + icon: CalendarClock, + }, ], }, { diff --git a/resources/js/pages/admin/manage/schedules/index.tsx b/resources/js/pages/admin/manage/schedules/index.tsx new file mode 100644 index 0000000..1701e1c --- /dev/null +++ b/resources/js/pages/admin/manage/schedules/index.tsx @@ -0,0 +1,486 @@ +import { Head, router } from '@inertiajs/react'; +import { Clock, MapPin, Pencil, Plus, Trash2, Video } from 'lucide-react'; +import { useState } from 'react'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { FormDialog } from '@/components/form-dialog'; +import InputError from '@/components/input-error'; +import { PageHeader } from '@/components/page-header'; +import { RowActions } from '@/components/row-actions'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { cn } from '@/lib/utils'; +import { destroy, store, update } from '@/routes/admin/manage/schedules'; +import type { Schedule } from '@/types/schedule'; +import { DayOfWeekLabels, DaysOfWeek } from '@/types/schedule'; + +type CourseClassOption = { + id: number; + class_name: string | null; + course: { id: number; code: string; name: string } | null; +}; + +type Props = { + schedules: Schedule[]; + courseClasses: CourseClassOption[]; + highlight?: number; +}; + +const UNSCHEDULED = '__unscheduled__'; + +const BOARD_COLUMNS = [...DaysOfWeek, UNSCHEDULED] as const; + +function courseClassLabel(courseClass: CourseClassOption): string { + const namePart = courseClass.class_name + ? `${courseClass.class_name} - ` + : ''; + + return `${namePart}${courseClass.course?.code ?? ''} ${courseClass.course?.name ?? ''}`; +} + +function toTimeInput(value: string | null): string { + return value ? value.slice(0, 5) : ''; +} + +export default function ScheduleIndex({ + schedules, + courseClasses, + highlight, +}: Props) { + const [createOpen, setCreateOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [deleting, setDeleting] = useState(null); + + function handleDelete() { + if (!deleting) { + return; + } + + router.delete(destroy(deleting.id), { + onSuccess: () => setDeleting(null), + }); + } + + const grouped = new Map(); + + for (const schedule of schedules) { + const key = schedule.day_of_week ?? UNSCHEDULED; + grouped.set(key, [...(grouped.get(key) ?? []), schedule]); + } + + const columns = BOARD_COLUMNS.filter( + (day) => + day !== UNSCHEDULED || (grouped.get(UNSCHEDULED)?.length ?? 0) > 0, + ); + + return ( + <> + + +
+ + + + } + /> + + + + { + if (!open) { + setEditing(null); + } + }} + editing={editing} + courseClasses={courseClasses} + /> + + {schedules.length === 0 ? ( +

+ Belum ada data jadwal. +

+ ) : ( +
+ {columns.map((day) => { + const items = grouped.get(day) ?? []; + + return ( +
+
+ + {day === UNSCHEDULED + ? 'Belum Diatur' + : DayOfWeekLabels[day]} + + + {items.length} + +
+ +
+ {items.length === 0 ? ( +

+ Tidak ada jadwal +

+ ) : ( + items.map((schedule) => ( + + + + {courseClassLabel( + schedule.course_class ?? { + id: 0, + class_name: + null, + course: null, + }, + )} + + + ), + onClick: + () => + setEditing( + schedule, + ), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: + () => + setDeleting( + schedule, + ), + }, + ]} + /> + + + {(schedule.start_time || + schedule.end_time) && ( + + + {toTimeInput( + schedule.start_time, + ) || '-'}{' '} + -{' '} + {toTimeInput( + schedule.end_time, + ) || '-'} + + )} + {schedule.room && ( + + + {schedule.room} + + )} + {schedule.online_link && ( + + + )} + + + )) + )} +
+
+ ); + })} +
+ )} + + { + if (!open) { + setDeleting(null); + } + }} + title="Hapus Jadwal" + description={(schedule) => + `Apakah Anda yakin ingin menghapus jadwal "${schedule.course_class?.class_name ?? schedule.course_class?.course?.name ?? 'ini'}"? Tindakan ini tidak dapat dibatalkan.` + } + onConfirm={handleDelete} + /> +
+ + ); +} + +function CreateForm({ + open, + onOpenChange, + courseClasses, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + courseClasses: CourseClassOption[]; +}) { + return ( + onOpenChange(false)} + > + {({ errors }) => ( +
+
+ + + + +
+
+ + + + +
+
+
+ + + +
+
+ + + +
+
+
+ + + +
+
+ + + +
+
+ )} +
+ ); +} + +function EditForm({ + open, + onOpenChange, + editing, + courseClasses, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + editing: Schedule | null; + courseClasses: CourseClassOption[]; +}) { + return ( + onOpenChange(false)} + > + {({ errors }) => + editing && ( +
+
+ + + +
+
+ + + +
+
+
+ + + +
+
+ + + +
+
+
+ + + +
+
+ + + +
+
+ ) + } +
+ ); +} diff --git a/resources/js/types/schedule.ts b/resources/js/types/schedule.ts new file mode 100644 index 0000000..63f5129 --- /dev/null +++ b/resources/js/types/schedule.ts @@ -0,0 +1,38 @@ +export const DaysOfWeek = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', +] as const; + +export type DayOfWeek = (typeof DaysOfWeek)[number]; + +export const DayOfWeekLabels: Record = { + Monday: 'Senin', + Tuesday: 'Selasa', + Wednesday: 'Rabu', + Thursday: 'Kamis', + Friday: 'Jumat', + Saturday: 'Sabtu', + Sunday: 'Minggu', +}; + +export type Schedule = { + id: number; + course_class_id: number; + course_class: { + id: number; + class_name: string | null; + course: { id: number; code: string; name: string } | null; + } | null; + day_of_week: string | null; + start_time: string | null; + end_time: string | null; + room: string | null; + online_link: string | null; + created_at: string; + updated_at: string; +}; diff --git a/routes/admin.php b/routes/admin.php index 20b2385..de9b5ed 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Admin\Manage\CourseClassController; use App\Http\Controllers\Admin\Manage\CourseController; use App\Http\Controllers\Admin\Manage\MaterialController; +use App\Http\Controllers\Admin\Manage\ScheduleController; use App\Http\Controllers\Admin\Manage\SubmissionController; use App\Http\Controllers\Admin\Master\AcademicTermController; use App\Http\Controllers\Admin\Master\DepartmentController; @@ -39,6 +40,8 @@ Route::put('{submission}', [SubmissionController::class, 'update'])->name('update'); Route::delete('{submission}', [SubmissionController::class, 'destroy'])->name('destroy'); }); + + Route::resource('schedules', ScheduleController::class)->except(['create', 'edit', 'show']); }); Route::prefix('admin/users')->name('admin.users.')->group(function () { -- 2.45.2