👨🏫 Form Tambah Dosen Baru
+Lengkapi informasi dosen dengan data yang akurat dan benar.
+-
+ @foreach ($errors->all() as $error)
+
- {{ $error }} + @endforeach +
diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 40d8090..7e3b65d 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -2,19 +2,20 @@ namespace App\Http\Controllers; -use App\Models\User; +use App\Models\UserDosen; +use App\Models\UserStaff; class DashboardController extends Controller { public function index() { try { - $dosen = User::query() + $dosen = UserDosen::query() ->where('role', 'dosen') ->orderBy('nama') ->get(); - $teknisi = User::query() + $teknisi = UserStaff::query() ->whereIn('role', ['teknisi', 'staff']) ->orderBy('nama') ->get(); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..7509519 --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,302 @@ +query('q', '')); + $role = trim((string) $request->query('role', '')); + $perPageInput = (string) $request->query('per_page', '10'); + + $allowedPerPage = ['10', '25', '50', '100', 'all']; + if (!in_array($perPageInput, $allowedPerPage, true)) { + $perPageInput = '10'; + } + + $perPage = $perPageInput === 'all' ? 100000 : (int) $perPageInput; + + // Query dari users_dosen (Dosen) + $dosenQuery = UserDosen::query(); + + // Query dari users_staff (Staff & Teknisi) + $staffQuery = UserStaff::query(); + + // Apply search filter + if ($q !== '') { + $dosenQuery->where(function ($sub) use ($q) { + $sub->where('nama', 'ilike', "%{$q}%") + ->orWhere('nip', 'ilike', "%{$q}%") + ->orWhere('nidn', 'ilike', "%{$q}%"); + }); + + $staffQuery->where(function ($sub) use ($q) { + $sub->where('nama', 'ilike', "%{$q}%") + ->orWhere('nip', 'ilike', "%{$q}%") + ->orWhere('nidn', 'ilike', "%{$q}%"); + }); + } + + // Apply role filter + if ($role !== '') { + if ($role === 'dosen') { + $staffQuery = null; // Only show dosen + } else { + $dosenQuery = null; // Only show staff/teknisi + if (in_array($role, ['staff', 'teknisi'])) { + $staffQuery->where('role', $role); + } + } + } + + // Merge results + $dosen = $dosenQuery ? $dosenQuery->get() : collect(); + $staff = $staffQuery ? $staffQuery->get() : collect(); + + $allUsers = $dosen->merge($staff)->sortBy('nama'); + $totalCount = $allUsers->count(); + + // Manual pagination + $page = (int) $request->query('page', 1); + $offset = ($page - 1) * $perPage; + $items = $allUsers->slice($offset, $perPage)->values(); + + // Create a Length Aware paginator instance + $users = new \Illuminate\Pagination\LengthAwarePaginator( + $items, + $totalCount, + $perPage, + $page, + [ + 'path' => route('users.index'), + 'query' => $request->query(), + ] + ); + + return view('user.index', [ + 'users' => $users, + 'q' => $q, + 'role' => $role, + 'perPage' => $perPageInput, + 'roleOptions' => ['dosen', 'teknisi', 'staff'], + 'totalCount' => $totalCount, + ]); + } + + public function dosen(Request $request): View + { + $q = trim((string) $request->query('q', '')); + $perPageInput = (string) $request->query('per_page', '10'); + + $allowedPerPage = ['10', '25', '50', '100', 'all']; + if (!in_array($perPageInput, $allowedPerPage, true)) { + $perPageInput = '10'; + } + + $perPage = $perPageInput === 'all' ? 100000 : (int) $perPageInput; + + $query = UserDosen::query()->where('role', 'dosen')->orderBy('nama'); + + if ($q !== '') { + $query->where(function ($sub) use ($q) { + $sub->where('nama', 'ilike', "%{$q}%") + ->orWhere('nip', 'ilike', "%{$q}%") + ->orWhere('nidn', 'ilike', "%{$q}%"); + }); + } + + $dosen = $query->paginate($perPage)->withQueryString(); + $dosenCount = UserDosen::where('role', 'dosen')->count(); + + return view('Dosen.dosen', [ + 'dosen' => $dosen, + 'dosenCount' => $dosenCount, + 'q' => $q, + 'perPage' => $perPageInput, + ]); + } + + public function staff(Request $request): View + { + $q = trim((string) $request->query('q', '')); + $role = trim((string) $request->query('role', '')); + $perPageInput = (string) $request->query('per_page', '10'); + + $allowedPerPage = ['10', '25', '50', '100', 'all']; + if (!in_array($perPageInput, $allowedPerPage, true)) { + $perPageInput = '10'; + } + + $perPage = $perPageInput === 'all' ? 100000 : (int) $perPageInput; + + $query = UserStaff::query()->orderBy('nama'); + + if ($q !== '') { + $query->where(function ($sub) use ($q) { + $sub->where('nama', 'ilike', "%{$q}%") + ->orWhere('nip', 'ilike', "%{$q}%") + ->orWhere('nidn', 'ilike', "%{$q}%"); + }); + } + + if ($role !== '') { + $query->where('role', $role); + } + + $staff = $query->paginate($perPage)->withQueryString(); + $staffCount = UserStaff::count(); + + return view('staff.staff', [ + 'staff' => $staff, + 'staffCount' => $staffCount, + 'q' => $q, + 'role' => $role, + 'perPage' => $perPageInput, + ]); + } + + public function create(): View + { + $roleOptions = ['dosen', 'teknisi', 'staff']; + + // Determine which view to show based on the URL path + if (request()->path() === 'dosen/create') { + return view('Dosen.create', [ + 'roleOptions' => $roleOptions, + ]); + } + + // Default to staff create view + return view('staff.create', [ + 'roleOptions' => $roleOptions, + ]); + } + + public function store(Request $request): RedirectResponse + { + // Clean up input - trim whitespace and convert empty strings to null + $request->merge([ + 'nama' => trim((string) $request->input('nama')), + 'nip' => ($nip = trim((string) $request->input('nip'))) === '' ? null : $nip, + 'nidn' => ($nidn = trim((string) $request->input('nidn'))) === '' ? null : $nidn, + 'bagian' => trim((string) $request->input('bagian')), + 'foto' => trim((string) $request->input('foto')), + ]); + + $role = $request->input('role'); + + if ($role === 'dosen') { + // Validate for Dosen + $validated = $request->validate([ + 'nama' => ['required', 'string', 'max:255'], + 'nip' => ['nullable', 'string', 'max:255', 'unique:users_dosen,nip'], + 'nidn' => ['nullable', 'string', 'max:255', 'unique:users_dosen,nidn'], + 'prodi' => ['nullable', 'string', 'max:255'], + 'foto' => ['nullable', 'string', 'max:2048'], + 'role' => ['required', 'in:dosen'], + ]); + + UserDosen::create($validated); + + return redirect()->route('users.dosen')->with('success', 'Data dosen berhasil ditambahkan.'); + } else { + // Validate for Staff/Teknisi + $validated = $request->validate([ + 'nama' => ['required', 'string', 'max:255'], + 'nip' => ['nullable', 'string', 'max:255', 'unique:users_staff,nip'], + 'nidn' => ['nullable', 'string', 'max:255', 'unique:users_staff,nidn'], + 'bagian' => ['nullable', 'string', 'max:255'], + 'foto' => ['nullable', 'string', 'max:2048'], + 'role' => ['required', 'in:staff,teknisi'], + ]); + + UserStaff::create($validated); + + return redirect()->route('users.staff')->with('success', 'Data staff/teknisi berhasil ditambahkan.'); + } + } + + public function edit(UserDosen | UserStaff $user): View + { + $roleOptions = ['dosen', 'teknisi', 'staff']; + + // Determine which view to show based on the URL path or user type + if (request()->path() === "dosen/{$user->getKey()}/edit" || $user instanceof UserDosen) { + return view('Dosen.edit', [ + 'user' => $user, + 'roleOptions' => $roleOptions, + ]); + } + + return view('staff.edit', [ + 'user' => $user, + 'roleOptions' => $roleOptions, + ]); + } + + public function update(Request $request, UserDosen | UserStaff $user): RedirectResponse + { + // Clean up input - trim whitespace and convert empty strings to null + $request->merge([ + 'nama' => trim((string) $request->input('nama')), + 'nip' => ($nip = trim((string) $request->input('nip'))) === '' ? null : $nip, + 'nidn' => ($nidn = trim((string) $request->input('nidn'))) === '' ? null : $nidn, + 'bagian' => trim((string) $request->input('bagian')), + 'foto' => trim((string) $request->input('foto')), + ]); + + if ($user instanceof UserDosen) { + // Update Dosen + $validated = $request->validate([ + 'nama' => ['required', 'string', 'max:255'], + 'nip' => ['nullable', 'string', 'max:255', 'unique:users_dosen,nip,' . $user->getKey() . ',id'], + 'nidn' => ['nullable', 'string', 'max:255', 'unique:users_dosen,nidn,' . $user->getKey() . ',id'], + 'prodi' => ['nullable', 'string', 'max:255'], + 'foto' => ['nullable', 'string', 'max:2048'], + 'role' => ['required', 'in:dosen'], + ]); + + $user->update($validated); + + return redirect()->route('users.dosen')->with('success', 'Data dosen berhasil diperbarui.'); + } else { + // Update Staff/Teknisi + $validated = $request->validate([ + 'nama' => ['required', 'string', 'max:255'], + 'nip' => ['nullable', 'string', 'max:255', 'unique:users_staff,nip,' . $user->getKey() . ',id'], + 'nidn' => ['nullable', 'string', 'max:255', 'unique:users_staff,nidn,' . $user->getKey() . ',id'], + 'bagian' => ['nullable', 'string', 'max:255'], + 'foto' => ['nullable', 'string', 'max:2048'], + 'role' => ['required', 'in:staff,teknisi'], + ]); + + $user->update($validated); + + return redirect()->route('users.staff')->with('success', 'Data staff/teknisi berhasil diperbarui.'); + } + } + + public function destroy(Request $request, string $id): RedirectResponse + { + // Try to find in UserDosen first, then UserStaff + $userDosen = UserDosen::find($id); + $userStaff = UserStaff::find($id); + + if ($userDosen) { + $userDosen->delete(); + return redirect()->route('users.dosen')->with('success', 'Data dosen berhasil dihapus.'); + } elseif ($userStaff) { + $userStaff->delete(); + return redirect()->route('users.staff')->with('success', 'Data staff/teknisi berhasil dihapus.'); + } + + return redirect()->back()->with('error', 'Data user tidak ditemukan.'); + } +} diff --git a/app/Http/Controllers/WelcomeController.php b/app/Http/Controllers/WelcomeController.php new file mode 100644 index 0000000..ac68b3e --- /dev/null +++ b/app/Http/Controllers/WelcomeController.php @@ -0,0 +1,156 @@ +nama ?? null; + + // Ambil data durasi mingguan dari attendance table + $durasiMingguan = Attendance::getDurationByDayThisWeek($user->id); + } else { + // Data default jika tidak ada user terautentikasi + $nama = 'Dosen/Staff'; + $durasiMingguan = [ + 'Senin' => 7.5, + 'Selasa' => 6.8, + 'Rabu' => 8.2, + 'Kamis' => 7.1, + 'Jumat' => 5.4, + ]; + } + + return view('welcome', [ + 'nama' => $nama, + 'durasiMingguan' => $durasiMingguan, + ]); + } + + /** + * Get attendance data for a specific user + * API endpoint untuk parsing data + */ + public function getAttendanceData(Request $request) + { + if (!Auth::check()) { + return response()->json([ + 'success' => false, + 'message' => 'User tidak terautentikasi', + ], 401); + } + + $user = Auth::user(); + $durasiMingguan = Attendance::getDurationByDayThisWeek($user->id); + $totalJam = array_sum($durasiMingguan); + + return response()->json([ + 'success' => true, + 'nama' => $user->nama, + 'durasiMingguan' => $durasiMingguan, + 'totalJam' => round($totalJam, 2), + ]); + } + + /** + * Record user check-in/check-out attendance + */ + public function recordAttendance(Request $request) + { + if (!Auth::check()) { + return response()->json([ + 'success' => false, + 'message' => 'User tidak terautentikasi', + ], 401); + } + + $validated = $request->validate([ + 'action' => 'required|in:check_in,check_out', // check_in atau check_out + ]); + + $user = Auth::user(); + $today = now()->toDateString(); + + // Cari atau buat record attendance untuk hari ini + $attendance = Attendance::firstOrCreate( + [ + 'user_id' => $user->id, + 'tanggal' => $today, + ], + [ + 'user_type' => $user instanceof UserDosen ? 'dosen' : 'staff', + ] + ); + + if ($validated['action'] === 'check_in') { + $attendance->jam_masuk = now()->toTimeString(); + $attendance->keterangan = 'hadir'; + } else { + $attendance->jam_keluar = now()->toTimeString(); + + // Hitung durasi jika sudah ada jam_masuk + if ($attendance->jam_masuk) { + $masuk = \Carbon\Carbon::createFromTimeString($attendance->jam_masuk); + $keluar = \Carbon\Carbon::createFromTimeString($attendance->jam_keluar); + $durasi = $masuk->diffInMinutes($keluar) / 60; // convert ke jam + $attendance->durasi_jam = round($durasi, 2); + } + } + + $attendance->save(); + + return response()->json([ + 'success' => true, + 'message' => $validated['action'] === 'check_in' ? 'Check-in berhasil' : 'Check-out berhasil', + 'attendance' => $attendance, + ]); + } + + /** + * Update status (online/tidak bisa diganggu) + */ + public function updateStatus(Request $request) + { + if (!Auth::check()) { + return response()->json([ + 'success' => false, + 'message' => 'User tidak terautentikasi', + ], 401); + } + + $validated = $request->validate([ + 'status' => 'required|in:online,dnd', // dnd = do not disturb + ]); + + $user = Auth::user(); + + // Simpan status ke session atau database + // Untuk sekarang, simpan di session + session(['user_status' => $validated['status']]); + + return response()->json([ + 'success' => true, + 'message' => 'Status diperbarui', + 'status' => $validated['status'], + ]); + } +} diff --git a/app/Models/Attendance.php b/app/Models/Attendance.php new file mode 100644 index 0000000..6e5be8d --- /dev/null +++ b/app/Models/Attendance.php @@ -0,0 +1,98 @@ + 'date', + 'durasi_jam' => 'decimal:2', + ]; + + /** + * Get the user (bisa UserDosen atau UserStaff) + */ + public function user() + { + return $this->belongsTo(UserDosen::class, 'user_id') + ->orWhere('user_type', 'dosen') + ->union(\DB::table('users_staff')->whereColumn('id', 'attendances.user_id')); + } + + /** + * Scope untuk filter minggu ini + */ + public function scopeThisWeek($query) + { + $startOfWeek = now()->startOfWeek(); + $endOfWeek = now()->endOfWeek(); + + return $query->whereBetween('tanggal', [$startOfWeek, $endOfWeek]); + } + + /** + * Scope untuk filter hari kerja (Senin-Jumat) + */ + public function scopeWorkDays($query) + { + return $query->whereNotIn(\DB::raw('DAYOFWEEK(tanggal)'), [1, 7]); // 1=Sunday, 7=Saturday + } + + /** + * Get total durasi minggu ini + */ + public static function getTotalDurationThisWeek($userId) + { + return self::where('user_id', $userId) + ->thisWeek() + ->workDays() + ->sum('durasi_jam'); + } + + /** + * Get durasi per hari minggu ini (Senin-Jumat) + */ + public static function getDurationByDayThisWeek($userId) + { + $days = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat']; + $dayQuery = self::where('user_id', $userId) + ->thisWeek() + ->workDays() + ->get() + ->groupBy(function ($item) { + $dayOfWeek = $item->tanggal->dayName; + return match ($dayOfWeek) { + 'Monday' => 'Senin', + 'Tuesday' => 'Selasa', + 'Wednesday' => 'Rabu', + 'Thursday' => 'Kamis', + 'Friday' => 'Jumat', + default => null, + }; + }); + + $result = []; + foreach ($days as $day) { + $result[$day] = $dayQuery->get($day)?->sum('durasi_jam') ?? 0; + } + + return $result; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index b396533..ff4f55a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,14 +6,84 @@ class User extends Model { - protected $table = 'users'; + protected $table = 'users_dosen'; + + public $incrementing = false; + + protected $keyType = 'string'; protected $fillable = [ + 'id', 'nama', 'nip', 'nidn', 'foto', - 'role' + 'role', + 'password' + ]; + + protected $hidden = [ + 'password', + ]; + + public $timestamps = true; +} +{$model->getKeyName()})) { + $model->{$model->getKeyName()} = Str::uuid()->toString(); + } + }); + } +} \ No newline at end of file diff --git a/app/Models/UserStaff.php b/app/Models/UserStaff.php new file mode 100644 index 0000000..def17b6 --- /dev/null +++ b/app/Models/UserStaff.php @@ -0,0 +1,31 @@ +dropColumn(['name', 'email', 'password', 'remember_token', 'email_verified_at']); + // Only drop columns if they exist + if (Schema::hasColumn('users', 'name')) { + $table->dropColumn(['name']); + } + if (Schema::hasColumn('users', 'email')) { + $table->dropColumn(['email']); + } + if (Schema::hasColumn('users', 'password')) { + $table->dropColumn(['password']); + } + if (Schema::hasColumn('users', 'remember_token')) { + $table->dropColumn(['remember_token']); + } + if (Schema::hasColumn('users', 'email_verified_at')) { + $table->dropColumn(['email_verified_at']); + } }); Schema::table('users', function (Blueprint $table) { - $table->string('nama')->after('id'); - $table->string('nip')->unique()->nullable()->after('nama'); - $table->string('nidn')->unique()->nullable()->after('nip'); - $table->string('foto')->nullable()->after('nidn'); - $table->string('role')->default('staff')->after('foto'); + // Only add columns if they don't exist + if (!Schema::hasColumn('users', 'nama')) { + $table->string('nama')->after('id'); + } + if (!Schema::hasColumn('users', 'nip')) { + $table->string('nip')->unique()->nullable()->after('nama'); + } + if (!Schema::hasColumn('users', 'nidn')) { + $table->string('nidn')->unique()->nullable()->after('nip'); + } + if (!Schema::hasColumn('users', 'foto')) { + $table->string('foto')->nullable()->after('nidn'); + } + if (!Schema::hasColumn('users', 'role')) { + $table->string('role')->default('staff')->after('foto'); + } }); } diff --git a/database/migrations/2026_04_14_000001_create_users_staff_table.php b/database/migrations/2026_04_14_000001_create_users_staff_table.php new file mode 100644 index 0000000..dd84dbc --- /dev/null +++ b/database/migrations/2026_04_14_000001_create_users_staff_table.php @@ -0,0 +1,35 @@ +uuid('id')->primary(); + $table->string('nama'); + $table->string('nip')->unique()->nullable(); + $table->string('nidn')->unique()->nullable(); + $table->string('foto')->nullable(); + $table->enum('role', ['staff', 'teknisi'])->default('staff'); + $table->string('password')->nullable(); + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users_staff'); + } +}; diff --git a/database/migrations/2026_04_14_000002_create_users_dosen_table.php b/database/migrations/2026_04_14_000002_create_users_dosen_table.php new file mode 100644 index 0000000..2d8a199 --- /dev/null +++ b/database/migrations/2026_04_14_000002_create_users_dosen_table.php @@ -0,0 +1,37 @@ +uuid('id')->primary(); + $table->string('nama'); + $table->string('nip')->unique()->nullable(); + $table->string('nidn')->unique()->nullable(); + $table->string('prodi')->nullable(); + $table->string('foto')->nullable(); + $table->enum('role', ['dosen'])->default('dosen'); + $table->string('password')->nullable(); + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users_dosen'); + } +}; diff --git a/database/migrations/2026_04_15_000001_create_attendances_table.php b/database/migrations/2026_04_15_000001_create_attendances_table.php new file mode 100644 index 0000000..f97af6a --- /dev/null +++ b/database/migrations/2026_04_15_000001_create_attendances_table.php @@ -0,0 +1,42 @@ +uuid('id')->primary(); + $table->uuid('user_id')->nullable(); + $table->string('user_type')->nullable(); // 'dosen' atau 'staff' + $table->date('tanggal'); + $table->time('jam_masuk')->nullable(); + $table->time('jam_keluar')->nullable(); + $table->decimal('durasi_jam', 5, 2)->default(0); // durasi dalam jam (misal 7.5) + $table->string('keterangan')->nullable(); // hadir, sakit, izin, dll + $table->timestamps(); + + // Unique constraint agar 1 user hanya bisa 1 record per hari + $table->unique(['user_id', 'tanggal']); + + // Index untuk query cepat + $table->index('user_id'); + $table->index('tanggal'); + $table->index(['user_type', 'tanggal']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('attendances'); + } +}; diff --git a/resources/views/Dosen/create.blade.php b/resources/views/Dosen/create.blade.php new file mode 100644 index 0000000..0bb6387 --- /dev/null +++ b/resources/views/Dosen/create.blade.php @@ -0,0 +1,616 @@ + + +
+ + +Data dosen baru
+Lengkapi informasi dosen dengan data yang akurat dan benar.
+Lengkapi data staff atau teknisi dengan informasi yang akurat.
+