From ca05c2459a864dd4363718c019821eec5f809859 Mon Sep 17 00:00:00 2001 From: HelgaFaisa <158024195+HelgaFaisa@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:46:24 +0700 Subject: [PATCH] mobile --- .../Admin/AbsensiKegiatanController.php | 2 +- .../Admin/KategoriKegiatanController.php | 2 +- .../Controllers/Admin/KegiatanController.php | 2 +- .../Admin/KepulanganController.php | 263 +++++-- .../Admin/RiwayatKegiatanController.php | 2 +- .../Controllers/Admin/SantriController.php | 47 +- .../Controllers/Api/ApiAuthController.php | 164 ++++ .../Http/Controllers/DashboardController.php | 270 +++++-- .../RiwayatKegiatanSantriController.php | 178 +++++ .../Santri/SantriProfileController.php | 5 +- sim-pkpps/app/Models/AbsensiKegiatan.php | 56 +- sim-pkpps/app/Models/KategoriKegiatan.php | 2 +- sim-pkpps/app/Models/Kegiatan.php | 2 +- sim-pkpps/app/Models/Kepulangan.php | 354 +++++++-- sim-pkpps/app/Models/Santri.php | 22 +- sim-pkpps/app/Models/User.php | 42 +- ...5_10_20_080216_create_kepulangan_table.php | 45 -- ...5_11_24_064857_create_kepulangan_table.php | 54 ++ ...64906_create_kepulangan_settings_table.php | 42 ++ ...914_create_kepulangan_reset_logs_table.php | 48 ++ ...12_19_131348_add_foto_to_santris_table.php | 29 + .../views/admin/kepulangan/create.blade.php | 244 ++++-- .../views/admin/kepulangan/index.blade.php | 91 ++- .../admin/kepulangan/over-limit.blade.php | 361 +++++++++ .../views/admin/kepulangan/settings.blade.php | 424 +++++++++++ .../views/admin/kepulangan/show.blade.php | 227 ++++-- .../admin/kepulangan/surat-pdf.blade.php | 296 +++++++- .../views/admin/santri/edit.blade.php | 37 +- .../views/admin/santri/form.blade.php | 84 ++- .../views/admin/santri/index.blade.php | 24 +- .../views/admin/santri/show.blade.php | 30 +- .../views/errors/500.blade.php} | 17 +- .../layouts/santri-wali-sidebar.blade.php | 6 +- .../views/santri/dashboardSantri.blade.php | 519 +++++++++++-- .../views/santri/kegiatan/index.blade.php | 365 +++++++++ .../views/santri/kegiatan/show.blade.php | 211 ++++++ .../views/santri/profil/edit.blade.php | 1 + .../views/santri/profil/index.blade.php | 21 +- sim-pkpps/routes/api.php | 29 +- sim-pkpps/routes/web.php | 73 +- .../D6FZEBgpwe0tXYewWTPUoB8jkSCmj1vaOfTQZXC2 | 1 - .../VZHgNRG1TSdcy9Emm4N24LHRBagOaImjTjDq7Jpu | 1 - .../ZQn8Fh75omh050uqkPl9hiHXz5sB1FivecNuUxuQ | 1 + .../eBnUEgVY9OnCfHthj9oQV8KuSYSvjftEAMHEOe72 | 1 + .../g11HwTdR2XIUjyb8Ix2frVaf3dZj8VdBDNCTZCi6 | 1 - .../hfGaVtADo1PNDKL2u7BoV0nWAuU1VlUbLe5VJRe0 | 1 + .../02f062229c85f0045f8e936b2cc6931a.php | 5 + .../105000334f95beae17d9647d42ae0af0.php | 194 ----- .../1bcee449d95fd7f4af3e834ca3f373a2.php | 121 --- .../1f84328a43f16c0ffe12040f0b03985b.php | 247 ++++++ .../212db3ecdbae68eaa4799a7f40fb56c4.php | 108 +++ .../31a477eb975396d9e3dfe350a914f63d.php | 184 ----- .../34a382bc0d43efb21e7b74c864109136.php | 295 -------- .../35ee7d3140dd2cb469794c46a4c597db.php | 185 ----- .../3c6140ea8a742aaf2aeffd1e7082af63.php | 35 + .../42e0614621fd9cbad7786f12bdf5b5c9.php | 134 ---- .../43f661f38fbd4f777442ca71c4728fc9.php | 195 ----- .../46fa72a4caaf31d2712a8a5f738b8308.php | 105 +++ .../4a999323131d3721d637c79f8c52d221.php | 5 + .../63b4c8088e9d5267c951fcd038ca0865.php | 113 +++ .../66e2096cb3c35849e326885249110f62.php | 275 ------- .../6aa14f1db90c01f76f48db151799fa79.php | 189 +++++ .../72906883c2124eae424a49669807e619.php | 69 -- .../7a81f1a630ebc977e42ecb6c9c5e1d09.php | 166 +++++ .../87643fe3b359872f8ce3c66a3684eab2.php | 91 ++- .../989bf831e05944415577fe6c40822c4c.php | 105 +++ .../9a6f4f2c1b504e5b114f81714b7179e6.php | 132 ++++ .../9b446f86a5179b2559bd927fbd573416.php | 53 -- .../9b58324ce92b42a8f9c43eb5d7bb3bd2.php | 255 +++++++ .../9e6b554cfe7514dae505b2a53e813665.php | 372 +++++++++ .../a2c2df15d8045affb5587e5e17d7ec54.php | 110 +++ .../a35bdf7f5682e1163e4e422bf7ed3156.php | 4 +- .../a55ee0bd4bb0ff61d001bcba38d0f720.php | 25 +- .../b130696dd20b50a4932a7b40f32091c1.php | 282 +++++++ .../b189c66d92090748551bf01fbdf8d452.php | 82 -- .../b69985eddcba7aac0c7eb4717aa1fb3f.php | 135 ++++ .../b97350e57d8d92217317f5f41c8d18a5.php | 536 ++++++++++--- .../bbf90b40396229b9af5ccf996fe33770.php | 194 ----- .../c5f0b5e8cf684d75aae3123501a0192f.php | 213 ------ .../c9c645ef9ca26f7f8150d4f3aae0f194.php | 231 ------ .../e126e1062982b7622152c3d631f3ccc4.php | 20 +- .../e4596b8fb6973ba3dcdcaa2fc4b535f5.php | 76 ++ .../e696522374dbc0de12cb6db367c6eb8c.php | 379 ++++++++++ .../e74e8e013b6b8f514e13c2f787a411bb.php | 501 +++++++++++++ .../e9890b3d59ea4f35d70423e626925754.php | 166 +++++ .../f00f9dae67317e274040c9fcaec81e32.php | 421 ----------- .../f8ddcf549eb0df0ba7c7d10975ba0892.php | 280 +++++++ .../f9c3e31ca3780ee24993ca37d8e92eee.php | 68 -- sim_mobile/.gitignore | 45 ++ sim_mobile/.metadata | 45 ++ sim_mobile/README.md | 16 + sim_mobile/analysis_options.yaml | 28 + sim_mobile/android/.gitignore | 14 + sim_mobile/android/app/build.gradle.kts | 44 ++ .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 45 ++ .../com/example/sim_mobile/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + sim_mobile/android/build.gradle.kts | 21 + sim_mobile/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + sim_mobile/android/settings.gradle.kts | 25 + sim_mobile/ios/.gitignore | 34 + sim_mobile/ios/Flutter/AppFrameworkInfo.plist | 26 + sim_mobile/ios/Flutter/Debug.xcconfig | 1 + sim_mobile/ios/Flutter/Release.xcconfig | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 616 +++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 99 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + sim_mobile/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + sim_mobile/ios/Runner/Info.plist | 49 ++ .../ios/Runner/Runner-Bridging-Header.h | 1 + sim_mobile/ios/RunnerTests/RunnerTests.swift | 12 + sim_mobile/lib/core/api/api_service.dart | 143 ++++ sim_mobile/lib/core/config/app_config.dart | 24 + .../core/widgets/android_frame_wrapper.dart | 245 ++++++ sim_mobile/lib/features/auth/login_page.dart | 223 ++++++ .../features/dashboard/dashboard_page.dart | 314 ++++++++ .../lib/features/profil/profil_page.dart | 253 +++++++ .../lib/features/splash/splash_screen.dart | 101 +++ sim_mobile/lib/main.dart | 73 ++ sim_mobile/linux/.gitignore | 1 + sim_mobile/linux/CMakeLists.txt | 128 ++++ sim_mobile/linux/flutter/CMakeLists.txt | 88 +++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 23 + sim_mobile/linux/runner/CMakeLists.txt | 26 + sim_mobile/linux/runner/main.cc | 6 + sim_mobile/linux/runner/my_application.cc | 130 ++++ sim_mobile/linux/runner/my_application.h | 18 + sim_mobile/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 12 + .../macos/Runner.xcodeproj/project.pbxproj | 705 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 99 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + sim_mobile/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + sim_mobile/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + sim_mobile/macos/Runner/Release.entitlements | 8 + .../macos/RunnerTests/RunnerTests.swift | 12 + sim_mobile/pubspec.lock | 370 +++++++++ sim_mobile/pubspec.yaml | 93 +++ sim_mobile/test/widget_test.dart | 30 + sim_mobile/web/favicon.png | Bin 0 -> 917 bytes sim_mobile/web/icons/Icon-192.png | Bin 0 -> 5292 bytes sim_mobile/web/icons/Icon-512.png | Bin 0 -> 8252 bytes sim_mobile/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes sim_mobile/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes sim_mobile/web/index.html | 38 + sim_mobile/web/manifest.json | 35 + sim_mobile/windows/.gitignore | 17 + sim_mobile/windows/CMakeLists.txt | 108 +++ sim_mobile/windows/flutter/CMakeLists.txt | 109 +++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 23 + sim_mobile/windows/runner/CMakeLists.txt | 40 + sim_mobile/windows/runner/Runner.rc | 121 +++ sim_mobile/windows/runner/flutter_window.cpp | 71 ++ sim_mobile/windows/runner/flutter_window.h | 33 + sim_mobile/windows/runner/main.cpp | 43 ++ sim_mobile/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes sim_mobile/windows/runner/runner.exe.manifest | 14 + sim_mobile/windows/runner/utils.cpp | 65 ++ sim_mobile/windows/runner/utils.h | 19 + sim_mobile/windows/runner/win32_window.cpp | 288 +++++++ sim_mobile/windows/runner/win32_window.h | 102 +++ 224 files changed, 14773 insertions(+), 3626 deletions(-) create mode 100644 sim-pkpps/app/Http/Controllers/Api/ApiAuthController.php create mode 100644 sim-pkpps/app/Http/Controllers/Santri/RiwayatKegiatanSantriController.php delete mode 100644 sim-pkpps/database/migrations/2025_10_20_080216_create_kepulangan_table.php create mode 100644 sim-pkpps/database/migrations/2025_11_24_064857_create_kepulangan_table.php create mode 100644 sim-pkpps/database/migrations/2025_11_24_064906_create_kepulangan_settings_table.php create mode 100644 sim-pkpps/database/migrations/2025_11_24_064914_create_kepulangan_reset_logs_table.php create mode 100644 sim-pkpps/database/migrations/2025_12_19_131348_add_foto_to_santris_table.php create mode 100644 sim-pkpps/resources/views/admin/kepulangan/over-limit.blade.php create mode 100644 sim-pkpps/resources/views/admin/kepulangan/settings.blade.php rename sim-pkpps/{storage/framework/views/4c2e61e495793889f762efcce89628d8.php => resources/views/errors/500.blade.php} (86%) create mode 100644 sim-pkpps/resources/views/santri/kegiatan/index.blade.php create mode 100644 sim-pkpps/resources/views/santri/kegiatan/show.blade.php delete mode 100644 sim-pkpps/storage/framework/sessions/D6FZEBgpwe0tXYewWTPUoB8jkSCmj1vaOfTQZXC2 delete mode 100644 sim-pkpps/storage/framework/sessions/VZHgNRG1TSdcy9Emm4N24LHRBagOaImjTjDq7Jpu create mode 100644 sim-pkpps/storage/framework/sessions/ZQn8Fh75omh050uqkPl9hiHXz5sB1FivecNuUxuQ create mode 100644 sim-pkpps/storage/framework/sessions/eBnUEgVY9OnCfHthj9oQV8KuSYSvjftEAMHEOe72 delete mode 100644 sim-pkpps/storage/framework/sessions/g11HwTdR2XIUjyb8Ix2frVaf3dZj8VdBDNCTZCi6 create mode 100644 sim-pkpps/storage/framework/sessions/hfGaVtADo1PNDKL2u7BoV0nWAuU1VlUbLe5VJRe0 create mode 100644 sim-pkpps/storage/framework/views/02f062229c85f0045f8e936b2cc6931a.php delete mode 100644 sim-pkpps/storage/framework/views/105000334f95beae17d9647d42ae0af0.php delete mode 100644 sim-pkpps/storage/framework/views/1bcee449d95fd7f4af3e834ca3f373a2.php create mode 100644 sim-pkpps/storage/framework/views/1f84328a43f16c0ffe12040f0b03985b.php create mode 100644 sim-pkpps/storage/framework/views/212db3ecdbae68eaa4799a7f40fb56c4.php delete mode 100644 sim-pkpps/storage/framework/views/31a477eb975396d9e3dfe350a914f63d.php delete mode 100644 sim-pkpps/storage/framework/views/34a382bc0d43efb21e7b74c864109136.php delete mode 100644 sim-pkpps/storage/framework/views/35ee7d3140dd2cb469794c46a4c597db.php create mode 100644 sim-pkpps/storage/framework/views/3c6140ea8a742aaf2aeffd1e7082af63.php delete mode 100644 sim-pkpps/storage/framework/views/42e0614621fd9cbad7786f12bdf5b5c9.php delete mode 100644 sim-pkpps/storage/framework/views/43f661f38fbd4f777442ca71c4728fc9.php create mode 100644 sim-pkpps/storage/framework/views/46fa72a4caaf31d2712a8a5f738b8308.php create mode 100644 sim-pkpps/storage/framework/views/4a999323131d3721d637c79f8c52d221.php create mode 100644 sim-pkpps/storage/framework/views/63b4c8088e9d5267c951fcd038ca0865.php delete mode 100644 sim-pkpps/storage/framework/views/66e2096cb3c35849e326885249110f62.php create mode 100644 sim-pkpps/storage/framework/views/6aa14f1db90c01f76f48db151799fa79.php delete mode 100644 sim-pkpps/storage/framework/views/72906883c2124eae424a49669807e619.php create mode 100644 sim-pkpps/storage/framework/views/7a81f1a630ebc977e42ecb6c9c5e1d09.php create mode 100644 sim-pkpps/storage/framework/views/989bf831e05944415577fe6c40822c4c.php create mode 100644 sim-pkpps/storage/framework/views/9a6f4f2c1b504e5b114f81714b7179e6.php delete mode 100644 sim-pkpps/storage/framework/views/9b446f86a5179b2559bd927fbd573416.php create mode 100644 sim-pkpps/storage/framework/views/9b58324ce92b42a8f9c43eb5d7bb3bd2.php create mode 100644 sim-pkpps/storage/framework/views/9e6b554cfe7514dae505b2a53e813665.php create mode 100644 sim-pkpps/storage/framework/views/a2c2df15d8045affb5587e5e17d7ec54.php create mode 100644 sim-pkpps/storage/framework/views/b130696dd20b50a4932a7b40f32091c1.php delete mode 100644 sim-pkpps/storage/framework/views/b189c66d92090748551bf01fbdf8d452.php create mode 100644 sim-pkpps/storage/framework/views/b69985eddcba7aac0c7eb4717aa1fb3f.php delete mode 100644 sim-pkpps/storage/framework/views/bbf90b40396229b9af5ccf996fe33770.php delete mode 100644 sim-pkpps/storage/framework/views/c5f0b5e8cf684d75aae3123501a0192f.php delete mode 100644 sim-pkpps/storage/framework/views/c9c645ef9ca26f7f8150d4f3aae0f194.php create mode 100644 sim-pkpps/storage/framework/views/e4596b8fb6973ba3dcdcaa2fc4b535f5.php create mode 100644 sim-pkpps/storage/framework/views/e696522374dbc0de12cb6db367c6eb8c.php create mode 100644 sim-pkpps/storage/framework/views/e74e8e013b6b8f514e13c2f787a411bb.php create mode 100644 sim-pkpps/storage/framework/views/e9890b3d59ea4f35d70423e626925754.php delete mode 100644 sim-pkpps/storage/framework/views/f00f9dae67317e274040c9fcaec81e32.php create mode 100644 sim-pkpps/storage/framework/views/f8ddcf549eb0df0ba7c7d10975ba0892.php delete mode 100644 sim-pkpps/storage/framework/views/f9c3e31ca3780ee24993ca37d8e92eee.php create mode 100644 sim_mobile/.gitignore create mode 100644 sim_mobile/.metadata create mode 100644 sim_mobile/README.md create mode 100644 sim_mobile/analysis_options.yaml create mode 100644 sim_mobile/android/.gitignore create mode 100644 sim_mobile/android/app/build.gradle.kts create mode 100644 sim_mobile/android/app/src/debug/AndroidManifest.xml create mode 100644 sim_mobile/android/app/src/main/AndroidManifest.xml create mode 100644 sim_mobile/android/app/src/main/kotlin/com/example/sim_mobile/MainActivity.kt create mode 100644 sim_mobile/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 sim_mobile/android/app/src/main/res/drawable/launch_background.xml create mode 100644 sim_mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 sim_mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 sim_mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 sim_mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 sim_mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 sim_mobile/android/app/src/main/res/values-night/styles.xml create mode 100644 sim_mobile/android/app/src/main/res/values/styles.xml create mode 100644 sim_mobile/android/app/src/profile/AndroidManifest.xml create mode 100644 sim_mobile/android/build.gradle.kts create mode 100644 sim_mobile/android/gradle.properties create mode 100644 sim_mobile/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 sim_mobile/android/settings.gradle.kts create mode 100644 sim_mobile/ios/.gitignore create mode 100644 sim_mobile/ios/Flutter/AppFrameworkInfo.plist create mode 100644 sim_mobile/ios/Flutter/Debug.xcconfig create mode 100644 sim_mobile/ios/Flutter/Release.xcconfig create mode 100644 sim_mobile/ios/Runner.xcodeproj/project.pbxproj create mode 100644 sim_mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 sim_mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 sim_mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 sim_mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 sim_mobile/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 sim_mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 sim_mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 sim_mobile/ios/Runner/AppDelegate.swift create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 sim_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 sim_mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 sim_mobile/ios/Runner/Base.lproj/Main.storyboard create mode 100644 sim_mobile/ios/Runner/Info.plist create mode 100644 sim_mobile/ios/Runner/Runner-Bridging-Header.h create mode 100644 sim_mobile/ios/RunnerTests/RunnerTests.swift create mode 100644 sim_mobile/lib/core/api/api_service.dart create mode 100644 sim_mobile/lib/core/config/app_config.dart create mode 100644 sim_mobile/lib/core/widgets/android_frame_wrapper.dart create mode 100644 sim_mobile/lib/features/auth/login_page.dart create mode 100644 sim_mobile/lib/features/dashboard/dashboard_page.dart create mode 100644 sim_mobile/lib/features/profil/profil_page.dart create mode 100644 sim_mobile/lib/features/splash/splash_screen.dart create mode 100644 sim_mobile/lib/main.dart create mode 100644 sim_mobile/linux/.gitignore create mode 100644 sim_mobile/linux/CMakeLists.txt create mode 100644 sim_mobile/linux/flutter/CMakeLists.txt create mode 100644 sim_mobile/linux/flutter/generated_plugin_registrant.cc create mode 100644 sim_mobile/linux/flutter/generated_plugin_registrant.h create mode 100644 sim_mobile/linux/flutter/generated_plugins.cmake create mode 100644 sim_mobile/linux/runner/CMakeLists.txt create mode 100644 sim_mobile/linux/runner/main.cc create mode 100644 sim_mobile/linux/runner/my_application.cc create mode 100644 sim_mobile/linux/runner/my_application.h create mode 100644 sim_mobile/macos/.gitignore create mode 100644 sim_mobile/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 sim_mobile/macos/Flutter/Flutter-Release.xcconfig create mode 100644 sim_mobile/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 sim_mobile/macos/Runner.xcodeproj/project.pbxproj create mode 100644 sim_mobile/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 sim_mobile/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 sim_mobile/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 sim_mobile/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 sim_mobile/macos/Runner/AppDelegate.swift create mode 100644 sim_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 sim_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 sim_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 sim_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 sim_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 sim_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 sim_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 sim_mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 sim_mobile/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 sim_mobile/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 sim_mobile/macos/Runner/Configs/Debug.xcconfig create mode 100644 sim_mobile/macos/Runner/Configs/Release.xcconfig create mode 100644 sim_mobile/macos/Runner/Configs/Warnings.xcconfig create mode 100644 sim_mobile/macos/Runner/DebugProfile.entitlements create mode 100644 sim_mobile/macos/Runner/Info.plist create mode 100644 sim_mobile/macos/Runner/MainFlutterWindow.swift create mode 100644 sim_mobile/macos/Runner/Release.entitlements create mode 100644 sim_mobile/macos/RunnerTests/RunnerTests.swift create mode 100644 sim_mobile/pubspec.lock create mode 100644 sim_mobile/pubspec.yaml create mode 100644 sim_mobile/test/widget_test.dart create mode 100644 sim_mobile/web/favicon.png create mode 100644 sim_mobile/web/icons/Icon-192.png create mode 100644 sim_mobile/web/icons/Icon-512.png create mode 100644 sim_mobile/web/icons/Icon-maskable-192.png create mode 100644 sim_mobile/web/icons/Icon-maskable-512.png create mode 100644 sim_mobile/web/index.html create mode 100644 sim_mobile/web/manifest.json create mode 100644 sim_mobile/windows/.gitignore create mode 100644 sim_mobile/windows/CMakeLists.txt create mode 100644 sim_mobile/windows/flutter/CMakeLists.txt create mode 100644 sim_mobile/windows/flutter/generated_plugin_registrant.cc create mode 100644 sim_mobile/windows/flutter/generated_plugin_registrant.h create mode 100644 sim_mobile/windows/flutter/generated_plugins.cmake create mode 100644 sim_mobile/windows/runner/CMakeLists.txt create mode 100644 sim_mobile/windows/runner/Runner.rc create mode 100644 sim_mobile/windows/runner/flutter_window.cpp create mode 100644 sim_mobile/windows/runner/flutter_window.h create mode 100644 sim_mobile/windows/runner/main.cpp create mode 100644 sim_mobile/windows/runner/resource.h create mode 100644 sim_mobile/windows/runner/resources/app_icon.ico create mode 100644 sim_mobile/windows/runner/runner.exe.manifest create mode 100644 sim_mobile/windows/runner/utils.cpp create mode 100644 sim_mobile/windows/runner/utils.h create mode 100644 sim_mobile/windows/runner/win32_window.cpp create mode 100644 sim_mobile/windows/runner/win32_window.h diff --git a/sim-pkpps/app/Http/Controllers/Admin/AbsensiKegiatanController.php b/sim-pkpps/app/Http/Controllers/Admin/AbsensiKegiatanController.php index efb567d..89cf5f6 100644 --- a/sim-pkpps/app/Http/Controllers/Admin/AbsensiKegiatanController.php +++ b/sim-pkpps/app/Http/Controllers/Admin/AbsensiKegiatanController.php @@ -1,5 +1,5 @@ Kepulangan::count(), 'menunggu_approval' => Kepulangan::where('status', 'Menunggu')->count(), 'sedang_izin' => Kepulangan::aktif()->count(), - 'over_limit_santri' => $this->getOverLimitSantri()->count(), + 'over_limit_santri' => count(Kepulangan::getSantriOverLimit()), ]; // Get unique years for filter @@ -58,13 +58,17 @@ public function index(Request $request) ->pluck('tahun'); // Get santri yang over limit untuk highlight - $santriOverLimit = $this->getOverLimitSantri()->pluck('total_hari', 'id_santri'); + $santriOverLimit = Kepulangan::getSantriOverLimit(); + + // Get settings untuk info periode + $settings = Kepulangan::getSettings(); return view('admin.kepulangan.index', compact( 'kepulangan', 'stats', 'tahunList', - 'santriOverLimit' + 'santriOverLimit', + 'settings' )); } @@ -77,7 +81,9 @@ public function create() ->orderBy('nama_lengkap') ->get(); - return view('admin.kepulangan.create', compact('santriList')); + $settings = Kepulangan::getSettings(); + + return view('admin.kepulangan.create', compact('santriList', 'settings')); } /** @@ -102,11 +108,21 @@ public function store(Request $request) 'alasan.max' => 'Alasan maksimal 500 karakter.', ]); - // Create kepulangan - Kepulangan::create($validated); + // Create kepulangan (durasi_izin akan otomatis dihitung di model) + $kepulangan = Kepulangan::create($validated); + + // Check apakah over limit + $kuota = Kepulangan::getSisaKuotaSantri($validated['id_santri']); + $message = 'Izin kepulangan berhasil diajukan.'; + + if ($kuota['status'] === 'melebihi') { + $message .= ' ⚠️ PERHATIAN: Santri ini sudah melebihi kuota ' . $kuota['kuota_maksimal'] . ' hari per tahun (Total: ' . $kuota['total_terpakai'] . ' hari).'; + } elseif ($kuota['status'] === 'hampir_habis') { + $message .= ' ⚠️ Kuota hampir habis. Sisa: ' . $kuota['sisa_kuota'] . ' hari.'; + } return redirect()->route('admin.kepulangan.index') - ->with('success', 'Izin kepulangan berhasil diajukan.'); + ->with('success', $message); } /** @@ -114,14 +130,22 @@ public function store(Request $request) */ public function show($id_kepulangan) { - // Cari data berdasarkan id_kepulangan (KP001, KP002, dst) $kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan) ->with('santri') ->firstOrFail(); + // Get detail kuota santri + $kuotaSantri = Kepulangan::getSisaKuotaSantri($kepulangan->id_santri); + + // Get settings + $settings = Kepulangan::getSettings(); + // Get detail izin tahun ini - $tahunSekarang = Carbon::now()->year; - $detailIzin = $this->getDetailIzinSantri($kepulangan->id_santri, $tahunSekarang); + $detailIzin = $this->getDetailIzinSantri( + $kepulangan->id_santri, + $settings->periode_mulai, + $settings->periode_akhir + ); // Get history kepulangan santri (exclude current) $history = Kepulangan::where('id_santri', $kepulangan->id_santri) @@ -132,8 +156,10 @@ public function show($id_kepulangan) return view('admin.kepulangan.show', compact( 'kepulangan', + 'kuotaSantri', 'detailIzin', - 'history' + 'history', + 'settings' )); } @@ -142,10 +168,8 @@ public function show($id_kepulangan) */ public function edit($id_kepulangan) { - // Cari data berdasarkan id_kepulangan $kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail(); - // Hanya bisa edit jika status Menunggu if ($kepulangan->status !== 'Menunggu') { return redirect()->route('admin.kepulangan.index') ->with('error', 'Hanya izin dengan status "Menunggu" yang bisa diedit.'); @@ -155,7 +179,15 @@ public function edit($id_kepulangan) ->orderBy('nama_lengkap') ->get(); - return view('admin.kepulangan.edit', compact('kepulangan', 'santriList')); + $settings = Kepulangan::getSettings(); + $kuotaSantri = Kepulangan::getSisaKuotaSantri($kepulangan->id_santri); + + return view('admin.kepulangan.edit', compact( + 'kepulangan', + 'santriList', + 'settings', + 'kuotaSantri' + )); } /** @@ -163,10 +195,8 @@ public function edit($id_kepulangan) */ public function update(Request $request, $id_kepulangan) { - // Cari data berdasarkan id_kepulangan $kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail(); - // Hanya bisa update jika status Menunggu if ($kepulangan->status !== 'Menunggu') { return redirect()->route('admin.kepulangan.index') ->with('error', 'Hanya izin dengan status "Menunggu" yang bisa diubah.'); @@ -184,10 +214,19 @@ public function update(Request $request, $id_kepulangan) 'alasan.min' => 'Alasan minimal 10 karakter.', ]); + // Update (durasi_izin akan otomatis dihitung ulang di model) $kepulangan->update($validated); - return redirect()->route('admin.kepulangan.index') - ->with('success', 'Data kepulangan berhasil diperbarui.'); + // Check apakah over limit setelah update + $kuota = Kepulangan::getSisaKuotaSantri($kepulangan->id_santri); + $message = 'Data kepulangan berhasil diperbarui.'; + + if ($kuota['status'] === 'melebihi') { + $message .= ' ⚠️ PERHATIAN: Santri ini sudah melebihi kuota ' . $kuota['kuota_maksimal'] . ' hari.'; + } + + return redirect()->route('admin.kepulangan.show', $id_kepulangan) + ->with('success', $message); } /** @@ -195,10 +234,8 @@ public function update(Request $request, $id_kepulangan) */ public function destroy($id_kepulangan) { - // Cari data berdasarkan id_kepulangan $kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail(); - // Hanya bisa hapus jika status Menunggu atau Ditolak if (!in_array($kepulangan->status, ['Menunggu', 'Ditolak'])) { return response()->json([ 'success' => false, @@ -219,7 +256,6 @@ public function destroy($id_kepulangan) */ public function approve(Request $request, $id_kepulangan) { - // Cari data berdasarkan id_kepulangan $kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail(); if ($kepulangan->status !== 'Menunggu') { @@ -229,7 +265,6 @@ public function approve(Request $request, $id_kepulangan) ], 400); } - // Update status - catatan opsional (tidak perlu validasi) $kepulangan->update([ 'status' => 'Disetujui', 'approved_by' => Auth::user()->name, @@ -248,10 +283,8 @@ public function approve(Request $request, $id_kepulangan) */ public function reject(Request $request, $id_kepulangan) { - // Cari data berdasarkan id_kepulangan $kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail(); - // Validasi alasan penolakan (wajib diisi minimal 10 karakter) $validated = $request->validate([ 'alasan_penolakan' => 'required|string|min:10', ], [ @@ -280,11 +313,10 @@ public function reject(Request $request, $id_kepulangan) } /** - * Complete kepulangan (mark as selesai) + * Complete kepulangan */ public function complete($id_kepulangan) { - // Cari data berdasarkan id_kepulangan $kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan)->firstOrFail(); if ($kepulangan->status !== 'Disetujui') { @@ -294,9 +326,7 @@ public function complete($id_kepulangan) ], 400); } - $kepulangan->update([ - 'status' => 'Selesai', - ]); + $kepulangan->update(['status' => 'Selesai']); return response()->json([ 'success' => true, @@ -309,7 +339,6 @@ public function complete($id_kepulangan) */ public function print($id_kepulangan) { - // Cari data berdasarkan id_kepulangan $kepulangan = Kepulangan::where('id_kepulangan', $id_kepulangan) ->with('santri') ->firstOrFail(); @@ -332,7 +361,7 @@ public function print($id_kepulangan) } /** - * API: Get santri data with penggunaan izin + * API: Get santri data with penggunaan kuota */ public function getSantriData($idSantri) { @@ -345,36 +374,164 @@ public function getSantriData($idSantri) ], 404); } - $tahunSekarang = Carbon::now()->year; - $penggunaanIzin = $this->getDetailIzinSantri($idSantri, $tahunSekarang); + $kuotaSantri = Kepulangan::getSisaKuotaSantri($idSantri); return response()->json([ 'success' => true, 'santri' => $santri, - 'penggunaan_izin' => [ - 'total_hari' => $penggunaanIzin['total_hari'], - 'total_izin' => $penggunaanIzin['total_izin'], - 'sisa_kuota' => $penggunaanIzin['sisa_kuota'], - 'over_limit' => $penggunaanIzin['over_limit'], - ] + 'penggunaan_izin' => $kuotaSantri ]); } /** - * Helper: Get detail izin santri per tahun + * ======================================== + * FITUR PENGATURAN KUOTA + * ======================================== */ - private function getDetailIzinSantri($idSantri, $tahun) + + /** + * Show settings page + */ + public function settings() + { + $settings = Kepulangan::getSettings(); + $resetLogs = Kepulangan::getResetLogs(20); + + // Statistik periode saat ini + $totalSantri = Santri::where('status', 'Aktif')->count(); + $santriOverLimit = Kepulangan::getSantriOverLimit(); + $totalIzinPeriodeIni = Kepulangan::whereBetween('tanggal_pulang', [ + $settings->periode_mulai, + $settings->periode_akhir + ])->whereIn('status', ['Disetujui', 'Selesai'])->count(); + + return view('admin.kepulangan.settings', compact( + 'settings', + 'resetLogs', + 'totalSantri', + 'santriOverLimit', + 'totalIzinPeriodeIni' + )); + } + + /** + * Update settings + */ + public function updateSettings(Request $request) + { + $validated = $request->validate([ + 'kuota_maksimal' => 'required|integer|min:1|max:365', + 'periode_mulai' => 'required|date', + 'periode_akhir' => 'required|date|after:periode_mulai', + ], [ + 'kuota_maksimal.required' => 'Kuota maksimal wajib diisi.', + 'kuota_maksimal.min' => 'Kuota minimal 1 hari.', + 'kuota_maksimal.max' => 'Kuota maksimal 365 hari.', + 'periode_mulai.required' => 'Periode mulai wajib diisi.', + 'periode_akhir.required' => 'Periode akhir wajib diisi.', + 'periode_akhir.after' => 'Periode akhir harus setelah periode mulai.', + ]); + + Kepulangan::updateSettings( + $validated['kuota_maksimal'], + $validated['periode_mulai'], + $validated['periode_akhir'] + ); + + return redirect()->route('admin.kepulangan.settings') + ->with('success', 'Pengaturan kuota berhasil diperbarui.'); + } + + /** + * Reset kuota satu santri + */ + public function resetKuotaSantri(Request $request, $idSantri) + { + $validated = $request->validate([ + 'catatan' => 'nullable|string|max:500', + ]); + + $santri = Santri::where('id_santri', $idSantri)->firstOrFail(); + + $result = Kepulangan::resetKuotaSantri( + $idSantri, + Auth::user()->name, + $validated['catatan'] ?? 'Reset kuota individual untuk ' . $santri->nama_lengkap + ); + + return response()->json([ + 'success' => true, + 'message' => 'Kuota santri ' . $santri->nama_lengkap . ' berhasil direset. Total ' . $result['total_hari_direset'] . ' hari telah direset.' + ]); + } + + /** + * Reset kuota semua santri + */ + public function resetKuotaSemuaSantri(Request $request) + { + $validated = $request->validate([ + 'catatan' => 'nullable|string|max:500', + 'konfirmasi' => 'required|accepted', + ], [ + 'konfirmasi.accepted' => 'Anda harus mencentang konfirmasi untuk melanjutkan reset massal.', + ]); + + $result = Kepulangan::resetKuotaSemuaSantri( + Auth::user()->name, + $validated['catatan'] ?? 'Reset kuota tahunan massal' + ); + + return response()->json([ + 'success' => true, + 'message' => 'Kuota berhasil direset untuk ' . $result['total_santri'] . ' santri. Total ' . $result['total_hari_direset'] . ' hari telah direset.', + 'data' => $result + ]); + } + + /** + * Show list santri over limit + */ + public function santriOverLimit() + { + $settings = Kepulangan::getSettings(); + $santriOverLimitIds = Kepulangan::getSantriOverLimit(); + + $santriList = Santri::whereIn('id_santri', array_keys($santriOverLimitIds)) + ->with(['kepulangan' => function($query) use ($settings) { + $query->whereBetween('tanggal_pulang', [ + $settings->periode_mulai, + $settings->periode_akhir + ])->whereIn('status', ['Disetujui', 'Selesai']); + }]) + ->get() + ->map(function($santri) use ($santriOverLimitIds) { + $kuota = Kepulangan::getSisaKuotaSantri($santri->id_santri); + $santri->total_hari_izin = $santriOverLimitIds[$santri->id_santri]; + $santri->kuota_info = $kuota; + return $santri; + }) + ->sortByDesc('total_hari_izin'); + + return view('admin.kepulangan.over-limit', compact('santriList', 'settings')); + } + + /** + * Helper: Get detail izin santri + */ + private function getDetailIzinSantri($idSantri, $periodeMulai, $periodeAkhir) { $kepulanganList = Kepulangan::where('id_santri', $idSantri) - ->where('status', 'Disetujui') - ->whereYear('tanggal_pulang', $tahun) + ->whereIn('status', ['Disetujui', 'Selesai']) + ->whereBetween('tanggal_pulang', [$periodeMulai, $periodeAkhir]) ->orderBy('tanggal_pulang', 'desc') ->get(); + $settings = Kepulangan::getSettings(); $totalHari = $kepulanganList->sum('durasi_izin'); $totalIzin = $kepulanganList->count(); - $sisaKuota = max(0, 12 - $totalHari); - $overLimit = $totalHari > 12; + $sisaKuota = max(0, $settings->kuota_maksimal - $totalHari); + $overLimit = $totalHari > $settings->kuota_maksimal; $details = $kepulanganList->map(function($item) { return [ @@ -382,6 +539,7 @@ private function getDetailIzinSantri($idSantri, $tahun) 'tanggal' => $item->tanggal_pulang_formatted . ' - ' . $item->tanggal_kembali_formatted, 'durasi' => $item->durasi_izin, 'alasan' => $item->alasan, + 'status' => $item->status, ]; }); @@ -393,19 +551,4 @@ private function getDetailIzinSantri($idSantri, $tahun) 'details' => $details, ]; } - - /** - * Helper: Get santri yang over limit - */ - private function getOverLimitSantri() - { - $tahunSekarang = Carbon::now()->year; - - return Kepulangan::selectRaw('id_santri, SUM(durasi_izin) as total_hari') - ->where('status', 'Disetujui') - ->whereYear('tanggal_pulang', $tahunSekarang) - ->groupBy('id_santri') - ->having('total_hari', '>', 12) - ->get(); - } } \ No newline at end of file diff --git a/sim-pkpps/app/Http/Controllers/Admin/RiwayatKegiatanController.php b/sim-pkpps/app/Http/Controllers/Admin/RiwayatKegiatanController.php index d5d2f31..a16028f 100644 --- a/sim-pkpps/app/Http/Controllers/Admin/RiwayatKegiatanController.php +++ b/sim-pkpps/app/Http/Controllers/Admin/RiwayatKegiatanController.php @@ -1,5 +1,5 @@ orderBy('created_at', 'desc') @@ -87,15 +89,33 @@ public function store(Request $request) 'daerah_asal' => 'nullable|string|max:255', 'nama_orang_tua' => 'nullable|string|max:255', 'nomor_hp_ortu' => 'nullable|string|max:20', + 'foto' => 'nullable|image|mimes:jpg,jpeg,png|max:2048', // TAMBAHAN: max 2MB ], [ 'nis.unique' => 'NIS sudah digunakan oleh santri lain.', 'nama_lengkap.required' => 'Nama lengkap wajib diisi.', 'jenis_kelamin.required' => 'Jenis kelamin wajib dipilih.', 'kelas.required' => 'Kelas wajib dipilih.', 'status.required' => 'Status wajib dipilih.', + 'foto.image' => 'File harus berupa gambar.', + 'foto.mimes' => 'Foto harus berformat JPG, JPEG, atau PNG.', + 'foto.max' => 'Ukuran foto maksimal 2 MB.', ]); - Santri::create($validated); + // Buat santri terlebih dahulu untuk mendapatkan id_santri + $santri = Santri::create($validated); + + // Handle upload foto + if ($request->hasFile('foto')) { + $file = $request->file('foto'); + $extension = $file->getClientOriginalExtension(); + $filename = $santri->id_santri . '.' . $extension; + + // Simpan file ke storage/app/public/santri + $path = $file->storeAs('santri', $filename, 'public'); + + // Update path foto di database + $santri->update(['foto' => $path]); + } // Clear cache Cache::forget('next_santri_id'); @@ -137,14 +157,34 @@ public function update(Request $request, Santri $santri) 'daerah_asal' => 'nullable|string|max:255', 'nama_orang_tua' => 'nullable|string|max:255', 'nomor_hp_ortu' => 'nullable|string|max:20', + 'foto' => 'nullable|image|mimes:jpg,jpeg,png|max:2048', // TAMBAHAN: max 2MB ], [ 'nis.unique' => 'NIS sudah digunakan oleh santri lain.', 'nama_lengkap.required' => 'Nama lengkap wajib diisi.', 'jenis_kelamin.required' => 'Jenis kelamin wajib dipilih.', 'kelas.required' => 'Kelas wajib dipilih.', 'status.required' => 'Status wajib dipilih.', + 'foto.image' => 'File harus berupa gambar.', + 'foto.mimes' => 'Foto harus berformat JPG, JPEG, atau PNG.', + 'foto.max' => 'Ukuran foto maksimal 2 MB.', ]); + // Handle upload foto baru + if ($request->hasFile('foto')) { + // Hapus foto lama jika ada + if ($santri->foto && Storage::disk('public')->exists($santri->foto)) { + Storage::disk('public')->delete($santri->foto); + } + + $file = $request->file('foto'); + $extension = $file->getClientOriginalExtension(); + $filename = $santri->id_santri . '.' . $extension; + + // Simpan file ke storage/app/public/santri + $path = $file->storeAs('santri', $filename, 'public'); + $validated['foto'] = $path; + } + $santri->update($validated); // Clear cache @@ -162,6 +202,11 @@ public function destroy(Santri $santri) { $namaSantri = $santri->nama_lengkap; + // Hapus foto jika ada + if ($santri->foto && Storage::disk('public')->exists($santri->foto)) { + Storage::disk('public')->delete($santri->foto); + } + $santri->delete(); // Clear cache diff --git a/sim-pkpps/app/Http/Controllers/Api/ApiAuthController.php b/sim-pkpps/app/Http/Controllers/Api/ApiAuthController.php new file mode 100644 index 0000000..0085b9a --- /dev/null +++ b/sim-pkpps/app/Http/Controllers/Api/ApiAuthController.php @@ -0,0 +1,164 @@ +validate([ + 'id_santri' => 'required|string', + 'password' => 'required|string', + ]); + + // Cari user berdasarkan username (id_santri) + $user = User::where('username', $request->id_santri)->first(); + + // Validasi user dan password + if (!$user || !Hash::check($request->password, $user->password)) { + throw ValidationException::withMessages([ + 'id_santri' => ['ID Santri atau password salah.'], + ]); + } + + // Cek apakah user adalah santri atau wali + if (!in_array($user->role, ['santri', 'wali'])) { + return response()->json([ + 'success' => false, + 'message' => 'Akun ini tidak memiliki akses ke aplikasi mobile.', + ], 403); + } + + // Hapus token lama (optional, untuk keamanan) + $user->tokens()->delete(); + + // Buat token baru + $token = $user->createToken('mobile-app')->plainTextToken; + + // Prepare response data + $responseData = [ + 'success' => true, + 'message' => 'Login berhasil', + 'token' => $token, + 'user' => [ + 'name' => $user->name, + 'role' => $user->role, + 'role_id' => $user->role_id, + ], + ]; + + // Jika santri, sertakan data santri + if ($user->role === 'santri') { + $santri = Santri::where('id_santri', $user->role_id) + ->select([ + 'id_santri', + 'nis', + 'nama_lengkap', + 'jenis_kelamin', + 'kelas', + 'status', + 'alamat_santri', + 'daerah_asal', + 'nama_orang_tua', + 'nomor_hp_ortu', + 'foto' + ]) + ->first(); + + $responseData['santri'] = $santri; + } + + return response()->json($responseData, 200); + } + + /** + * Logout - Hapus token + */ + public function logout(Request $request) + { + // Hapus token yang sedang digunakan + $request->user()->currentAccessToken()->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Logout berhasil', + ], 200); + } + + /** + * Get Profile Santri yang sedang login + */ + public function profile(Request $request) + { + $user = $request->user(); + + if ($user->role !== 'santri') { + return response()->json([ + 'success' => false, + 'message' => 'Hanya santri yang bisa mengakses profil.', + ], 403); + } + + $santri = Santri::where('id_santri', $user->role_id) + ->select([ + 'id_santri', + 'nis', + 'nama_lengkap', + 'jenis_kelamin', + 'kelas', + 'status', + 'alamat_santri', + 'daerah_asal', + 'nama_orang_tua', + 'nomor_hp_ortu', + 'foto', + 'created_at' + ]) + ->first(); + + if (!$santri) { + return response()->json([ + 'success' => false, + 'message' => 'Data santri tidak ditemukan.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => [ + 'id_santri' => $santri->id_santri, + 'nis' => $santri->nis, + 'nama_lengkap' => $santri->nama_lengkap, + 'jenis_kelamin' => $santri->jenis_kelamin, + 'kelas' => $santri->kelas, + 'status' => $santri->status, + 'alamat_santri' => $santri->alamat_santri, + 'daerah_asal' => $santri->daerah_asal, + 'nama_orang_tua' => $santri->nama_orang_tua, + 'nomor_hp_ortu' => $santri->nomor_hp_ortu, + 'foto_url' => $santri->foto_url, // Accessor dari Model Santri + 'bergabung_sejak' => $santri->created_at->format('d F Y'), + ] + ], 200); + } +} \ No newline at end of file diff --git a/sim-pkpps/app/Http/Controllers/DashboardController.php b/sim-pkpps/app/Http/Controllers/DashboardController.php index 2ec1d7e..d52d4f2 100644 --- a/sim-pkpps/app/Http/Controllers/DashboardController.php +++ b/sim-pkpps/app/Http/Controllers/DashboardController.php @@ -4,13 +4,14 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\DB; use App\Models\Santri; use App\Models\User; use App\Models\RiwayatPelanggaran; use App\Models\Berita; use App\Models\KesehatanSantri; use App\Models\Kepulangan; +use App\Models\Capaian; +use App\Models\Semester; use Carbon\Carbon; class DashboardController extends Controller @@ -31,104 +32,263 @@ public function admin() } catch (\Exception $e) { Log::error('Error di Dashboard Admin: ' . $e->getMessage()); - - return response()->view('errors.500', [ - 'error' => 'Terjadi kesalahan saat memuat dashboard', - 'message' => $e->getMessage() - ], 500); + abort(500, 'Terjadi kesalahan saat memuat dashboard Admin: ' . $e->getMessage()); } } /** - * Dashboard Santri/Wali - OPTIMIZED ✅ + * Dashboard Santri/Wali - FIXED VERSION ✅ */ public function santri() { try { $user = Auth::user(); + Log::info('=== DASHBOARD SANTRI START ==='); + Log::info('User ID: ' . $user->id); + Log::info('Role: ' . $user->role); + Log::info('Role ID: ' . $user->role_id); + // Validasi role if (!in_array($user->role, ['santri', 'wali'])) { Log::error('Role tidak sesuai: ' . $user->role); abort(403, 'Akses ditolak. Role Anda: ' . $user->role); } - // ✅ QUERY OPTIMIZED - Ambil data santri dengan kolom minimal + // ✅ Ambil data santri $santri = Santri::where('id_santri', $user->role_id) ->select('id_santri', 'nama_lengkap', 'kelas') - ->firstOrFail(); + ->first(); + + if (!$santri) { + Log::error('Santri tidak ditemukan dengan role_id: ' . $user->role_id); + abort(404, 'Data santri tidak ditemukan.'); + } + + Log::info('Santri ditemukan: ' . $santri->nama_lengkap); $idSantri = $santri->id_santri; $today = Carbon::today(); $weekAgo = Carbon::now()->subDays(7); - // ✅ PARALLEL QUERIES - Eksekusi query secara bersamaan untuk performa + // ✅ Ambil semester aktif dengan FALLBACK + $semesterAktif = null; + try { + $semesterAktif = Semester::where('status', 'aktif') + ->select('id_semester', 'nama_semester', 'tahun_ajaran') + ->first(); + + if (!$semesterAktif) { + $semesterAktif = Semester::select('id_semester', 'nama_semester', 'tahun_ajaran') + ->orderBy('tahun_ajaran', 'desc') + ->orderBy('periode', 'desc') + ->first(); + } + + Log::info('Semester aktif: ' . ($semesterAktif ? $semesterAktif->nama_semester : 'Tidak ada')); + } catch (\Exception $e) { + Log::warning('Error mengambil semester: ' . $e->getMessage()); + $semesterAktif = null; + } + + // ✅ AMBIL PROGRES AL-QUR'AN dengan FALLBACK + $progresAlquran = 0; + try { + $query = Capaian::where('id_santri', $idSantri); + + if ($semesterAktif) { + $query->where('id_semester', $semesterAktif->id_semester); + } + + $progresAlquran = $query->whereHas('materi', function($q) { + $q->where('kategori', 'Al-Qur\'an'); + })->avg('persentase') ?? 0; + + Log::info('Progres Al-Quran: ' . $progresAlquran); + } catch (\Exception $e) { + Log::warning('Error progres Al-Quran: ' . $e->getMessage()); + $progresAlquran = 0; + } + + // ✅ AMBIL PROGRES HADIST dengan FALLBACK + $progresHadist = 0; + try { + $query = Capaian::where('id_santri', $idSantri); + + if ($semesterAktif) { + $query->where('id_semester', $semesterAktif->id_semester); + } + + $progresHadist = $query->whereHas('materi', function($q) { + $q->where('kategori', 'Hadist'); + })->avg('persentase') ?? 0; + + Log::info('Progres Hadist: ' . $progresHadist); + } catch (\Exception $e) { + Log::warning('Error progres Hadist: ' . $e->getMessage()); + $progresHadist = 0; + } + + // ✅ AMBIL PROGRES MATERI TAMBAHAN dengan FALLBACK + $progresMateriTambahan = 0; + try { + $query = Capaian::where('id_santri', $idSantri); + + if ($semesterAktif) { + $query->where('id_semester', $semesterAktif->id_semester); + } + + $progresMateriTambahan = $query->whereHas('materi', function($q) { + $q->where('kategori', 'Materi Tambahan'); + })->avg('persentase') ?? 0; + + Log::info('Progres Materi Tambahan: ' . $progresMateriTambahan); + } catch (\Exception $e) { + Log::warning('Error progres Materi Tambahan: ' . $e->getMessage()); + $progresMateriTambahan = 0; + } + + // ✅ DATA UNTUK GRAFIK 1: Progress per Materi dengan FALLBACK + $capaianPerMateri = collect([]); + try { + $query = Capaian::with(['materi' => function($q) { + $q->select('id_materi', 'nama_kitab', 'kategori', 'total_halaman'); + }]) + ->where('id_santri', $idSantri); + + if ($semesterAktif) { + $query->where('id_semester', $semesterAktif->id_semester); + } + + $capaianPerMateri = $query->select('id', 'id_materi', 'persentase', 'halaman_selesai') + ->orderBy('persentase', 'desc') + ->limit(10) + ->get(); + + Log::info('Capaian per materi: ' . $capaianPerMateri->count() . ' items'); + } catch (\Exception $e) { + Log::warning('Error capaian per materi: ' . $e->getMessage()); + $capaianPerMateri = collect([]); + } + + // ✅ DATA UNTUK GRAFIK 2: Distribusi Status dengan FALLBACK + $distribusiStatus = [ + 'selesai' => 0, + 'hampir_selesai' => 0, + 'sedang_berjalan' => 0, + 'baru_dimulai' => 0, + ]; + + try { + $baseQuery = Capaian::where('id_santri', $idSantri); + + if ($semesterAktif) { + $baseQuery->where('id_semester', $semesterAktif->id_semester); + } + + $distribusiStatus = [ + 'selesai' => (clone $baseQuery)->where('persentase', '>=', 100)->count(), + 'hampir_selesai' => (clone $baseQuery)->whereBetween('persentase', [75, 99.99])->count(), + 'sedang_berjalan' => (clone $baseQuery)->whereBetween('persentase', [25, 74.99])->count(), + 'baru_dimulai' => (clone $baseQuery)->whereBetween('persentase', [0, 24.99])->count(), + ]; + + Log::info('Distribusi status: ' . json_encode($distribusiStatus)); + } catch (\Exception $e) { + Log::warning('Error distribusi status: ' . $e->getMessage()); + } + + // ✅ Data dashboard utama $data = [ 'nama_santri' => $santri->nama_lengkap, 'kelas' => $santri->kelas, - 'progres_quran' => 0, // Nanti diisi dari database capaian - 'progres_hadist' => 0, // Nanti diisi dari database capaian - - // Query langsung untuk saldo uang saku (dari accessor model) - 'saldo_uang_saku' => $santri->saldo_uang_saku, - - // Query optimized untuk poin pelanggaran - 'poin_pelanggaran' => RiwayatPelanggaran::where('id_santri', $idSantri) - ->sum('poin'), + 'progres_quran' => round($progresAlquran, 1), + 'progres_hadist' => round($progresHadist, 1), + 'progres_materi_tambahan' => round($progresMateriTambahan, 1), + 'saldo_uang_saku' => method_exists($santri, 'getSaldoUangSakuAttribute') + ? $santri->saldo_uang_saku + : 0, + 'poin_pelanggaran' => RiwayatPelanggaran::where('id_santri', $idSantri)->sum('poin') ?? 0, ]; - // ✅ Query status kesehatan (hanya jika sedang dirawat) - $statusKesehatan = KesehatanSantri::where('id_santri', $idSantri) - ->where('status', 'dirawat') - ->select('id', 'keluhan', 'tanggal_masuk') - ->orderBy('tanggal_masuk', 'desc') - ->first(); + Log::info('Data array: ' . json_encode($data)); - // ✅ Query kepulangan aktif (hanya jika sedang dalam periode pulang) - $kepulanganAktif = Kepulangan::where('id_santri', $idSantri) - ->where('status', 'Disetujui') - ->whereDate('tanggal_pulang', '<=', $today) - ->whereDate('tanggal_kembali', '>=', $today) - ->select('id_kepulangan', 'tanggal_pulang', 'tanggal_kembali', 'alasan') - ->first(); + // ✅ Query status kesehatan dengan FALLBACK + $statusKesehatan = null; + try { + $statusKesehatan = KesehatanSantri::where('id_santri', $idSantri) + ->where('status', 'dirawat') + ->select('id', 'keluhan', 'tanggal_masuk') + ->orderBy('tanggal_masuk', 'desc') + ->first(); + } catch (\Exception $e) { + Log::warning('Error status kesehatan: ' . $e->getMessage()); + } - // ✅ Query berita terbaru (7 hari terakhir) - OPTIMIZED - $beritaTerbaru = Berita::select('id_berita', 'judul', 'created_at') - ->where('status', 'published') - ->where('created_at', '>=', $weekAgo) - ->where(function($query) use ($santri) { - $query->where('target_berita', 'semua') - ->orWhere(function($q) use ($santri) { - $q->where('target_berita', 'kelas_tertentu') - ->whereJsonContains('target_kelas', $santri->kelas); - }) - ->orWhereHas('santriTertentu', function($q) use ($santri) { - $q->where('santris.id_santri', $santri->id_santri); - }); - }) - ->orderBy('created_at', 'desc') - ->limit(5) - ->get(); + // ✅ Query kepulangan aktif dengan FALLBACK + $kepulanganAktif = null; + try { + $kepulanganAktif = Kepulangan::where('id_santri', $idSantri) + ->where('status', 'Disetujui') + ->whereDate('tanggal_pulang', '<=', $today) + ->whereDate('tanggal_kembali', '>=', $today) + ->select('id_kepulangan', 'tanggal_pulang', 'tanggal_kembali', 'alasan') + ->first(); + } catch (\Exception $e) { + Log::warning('Error kepulangan aktif: ' . $e->getMessage()); + } - // Return view dengan data yang sudah dioptimasi + // ✅ Query berita terbaru dengan FALLBACK + $beritaTerbaru = collect([]); + try { + $beritaTerbaru = Berita::select('id_berita', 'judul', 'created_at') + ->where('status', 'published') + ->where('created_at', '>=', $weekAgo) + ->where(function($query) use ($santri) { + $query->where('target_berita', 'semua') + ->orWhere(function($q) use ($santri) { + $q->where('target_berita', 'kelas_tertentu') + ->whereJsonContains('target_kelas', $santri->kelas); + }); + }) + ->orderBy('created_at', 'desc') + ->limit(5) + ->get(); + + Log::info('Berita terbaru: ' . $beritaTerbaru->count() . ' items'); + } catch (\Exception $e) { + Log::warning('Error berita terbaru: ' . $e->getMessage()); + $beritaTerbaru = collect([]); + } + + Log::info('=== DASHBOARD SANTRI SUCCESS ==='); + + // Return view dengan semua data return view('santri.dashboardSantri', compact( 'data', 'santri', 'user', 'beritaTerbaru', 'statusKesehatan', - 'kepulanganAktif' + 'kepulanganAktif', + 'capaianPerMateri', + 'distribusiStatus', + 'semesterAktif' )); } catch (\Exception $e) { - Log::error('=== ERROR DI DASHBOARD SANTRI ==='); + Log::error('=== FATAL ERROR DI DASHBOARD SANTRI ==='); Log::error('Message: ' . $e->getMessage()); Log::error('File: ' . $e->getFile()); Log::error('Line: ' . $e->getLine()); + Log::error('Trace: ' . $e->getTraceAsString()); - return response()->view('errors.500', [ - 'error' => $e->getMessage(), - ], 500); + // Tampilkan error detail jika debug mode + if (config('app.debug')) { + abort(500, 'Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); + } else { + abort(500, 'Terjadi kesalahan saat memuat dashboard. Silakan hubungi administrator.'); + } } } } \ No newline at end of file diff --git a/sim-pkpps/app/Http/Controllers/Santri/RiwayatKegiatanSantriController.php b/sim-pkpps/app/Http/Controllers/Santri/RiwayatKegiatanSantriController.php new file mode 100644 index 0000000..a61d9e7 --- /dev/null +++ b/sim-pkpps/app/Http/Controllers/Santri/RiwayatKegiatanSantriController.php @@ -0,0 +1,178 @@ +role !== 'santri') { + abort(403, 'Akses ditolak.'); + } + + $santri = Santri::where('id_santri', $user->role_id) + ->select('id_santri', 'nama_lengkap', 'kelas') + ->firstOrFail(); + + $idSantri = $santri->id_santri; + $today = Carbon::today(); + $hariIni = Carbon::now()->locale('id')->dayName; // Senin, Selasa, etc. + + // ✅ JADWAL KEGIATAN HARI INI (Tetap) + $jadwalHariIni = Kegiatan::with('kategori') + ->where('hari', ucfirst($hariIni)) + ->select('kegiatan_id', 'kategori_id', 'nama_kegiatan', 'waktu_mulai', 'waktu_selesai', 'materi') + ->orderBy('waktu_mulai') + ->get(); + + // ✅ CEK STATUS ABSENSI HARI INI + $absensiHariIni = AbsensiKegiatan::where('id_santri', $idSantri) + ->whereDate('tanggal', $today) + ->pluck('status', 'kegiatan_id') + ->toArray(); + + // ✅ RIWAYAT ABSENSI (dengan filter) + $query = AbsensiKegiatan::with('kegiatan.kategori') + ->where('id_santri', $idSantri); + + // Filter Bulan + if ($request->filled('bulan')) { + $bulan = Carbon::parse($request->bulan); + $query->whereMonth('tanggal', $bulan->month) + ->whereYear('tanggal', $bulan->year); + } + + // Filter Status + if ($request->filled('status')) { + $query->where('status', $request->status); + } + + $riwayats = $query->orderBy('tanggal', 'desc') + ->orderBy('waktu_absen', 'desc') + ->paginate(15) + ->appends(request()->query()); + + // ✅ STATISTIK KEHADIRAN (30 HARI TERAKHIR) + $stats30Hari = AbsensiKegiatan::where('id_santri', $idSantri) + ->whereDate('tanggal', '>=', Carbon::now()->subDays(30)) + ->select('status', DB::raw('count(*) as total')) + ->groupBy('status') + ->pluck('total', 'status') + ->toArray(); + + $totalKegiatan30Hari = array_sum($stats30Hari); + $persentaseKehadiran = $totalKegiatan30Hari > 0 + ? round(($stats30Hari['Hadir'] ?? 0) / $totalKegiatan30Hari * 100, 1) + : 0; + + // ✅ DATA GRAFIK: Kehadiran per Minggu (4 Minggu Terakhir) + $dataGrafikMingguan = []; + for ($i = 3; $i >= 0; $i--) { + $startWeek = Carbon::now()->subWeeks($i)->startOfWeek(); + $endWeek = Carbon::now()->subWeeks($i)->endOfWeek(); + + $hadir = AbsensiKegiatan::where('id_santri', $idSantri) + ->whereBetween('tanggal', [$startWeek, $endWeek]) + ->where('status', 'Hadir') + ->count(); + + $total = AbsensiKegiatan::where('id_santri', $idSantri) + ->whereBetween('tanggal', [$startWeek, $endWeek]) + ->count(); + + $dataGrafikMingguan[] = [ + 'minggu' => 'Minggu ' . (4 - $i), + 'hadir' => $hadir, + 'total' => $total, + 'persentase' => $total > 0 ? round($hadir / $total * 100, 1) : 0, + ]; + } + + // ✅ STATISTIK PER KATEGORI KEGIATAN + $statsByKategori = AbsensiKegiatan::where('id_santri', $idSantri) + ->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id') + ->join('kategori_kegiatans', 'kegiatans.kategori_id', '=', 'kategori_kegiatans.kategori_id') + ->select( + 'kategori_kegiatans.nama_kategori', + DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Hadir" THEN 1 ELSE 0 END) as hadir'), + DB::raw('COUNT(*) as total') + ) + ->groupBy('kategori_kegiatans.nama_kategori') + ->get(); + + return view('santri.kegiatan.index', compact( + 'santri', + 'jadwalHariIni', + 'absensiHariIni', + 'riwayats', + 'stats30Hari', + 'totalKegiatan30Hari', + 'persentaseKehadiran', + 'dataGrafikMingguan', + 'statsByKategori', + 'hariIni' + )); + } + + /** + * Detail Riwayat Absensi per Kegiatan + */ + public function show($kegiatan_id) + { + $user = Auth::user(); + + if ($user->role !== 'santri') { + abort(403, 'Akses ditolak.'); + } + + $santri = Santri::where('id_santri', $user->role_id) + ->select('id_santri', 'nama_lengkap') + ->firstOrFail(); + + $kegiatan = Kegiatan::with('kategori') + ->where('kegiatan_id', $kegiatan_id) + ->firstOrFail(); + + // Riwayat absensi untuk kegiatan ini + $riwayats = AbsensiKegiatan::where('id_santri', $santri->id_santri) + ->where('kegiatan_id', $kegiatan_id) + ->orderBy('tanggal', 'desc') + ->paginate(20); + + // Statistik kehadiran untuk kegiatan ini + $stats = AbsensiKegiatan::where('id_santri', $santri->id_santri) + ->where('kegiatan_id', $kegiatan_id) + ->select('status', DB::raw('count(*) as total')) + ->groupBy('status') + ->pluck('total', 'status') + ->toArray(); + + $totalAbsensi = array_sum($stats); + $persentaseHadir = $totalAbsensi > 0 + ? round(($stats['Hadir'] ?? 0) / $totalAbsensi * 100, 1) + : 0; + + return view('santri.kegiatan.show', compact( + 'santri', + 'kegiatan', + 'riwayats', + 'stats', + 'totalAbsensi', + 'persentaseHadir' + )); + } +} \ No newline at end of file diff --git a/sim-pkpps/app/Http/Controllers/Santri/SantriProfileController.php b/sim-pkpps/app/Http/Controllers/Santri/SantriProfileController.php index 54af299..3551737 100644 --- a/sim-pkpps/app/Http/Controllers/Santri/SantriProfileController.php +++ b/sim-pkpps/app/Http/Controllers/Santri/SantriProfileController.php @@ -43,6 +43,7 @@ function () use ($user) { 'nama_orang_tua', 'nomor_hp_ortu', 'rfid_uid', + 'foto', // ✅ TAMBAHAN INI - PENTING! 'created_at' ]) ->firstOrFail(); @@ -68,8 +69,10 @@ public function edit() 'id', 'id_santri', 'nama_lengkap', + 'jenis_kelamin', // ✅ TAMBAHAN untuk fallback foto default 'alamat_santri', - 'nomor_hp_ortu' + 'nomor_hp_ortu', + 'foto' // ✅ TAMBAHAN INI - PENTING! ]) ->firstOrFail(); diff --git a/sim-pkpps/app/Models/AbsensiKegiatan.php b/sim-pkpps/app/Models/AbsensiKegiatan.php index b22e7cb..e76ed61 100644 --- a/sim-pkpps/app/Models/AbsensiKegiatan.php +++ b/sim-pkpps/app/Models/AbsensiKegiatan.php @@ -1,9 +1,10 @@ status] ?? $this->status; } + + // ============================================ + // ✅ TAMBAHKAN METHOD-METHOD BARU DI BAWAH INI + // ============================================ + + /** + * Accessor: Tanggal Formatted (untuk view santri) + */ + public function getTanggalFormattedAttribute() + { + return Carbon::parse($this->tanggal)->locale('id')->isoFormat('dddd, D MMMM YYYY'); + } + + /** + * Accessor: Waktu Absen Formatted (untuk view santri) + */ + public function getWaktuAbsenFormattedAttribute() + { + return $this->waktu_absen ? Carbon::parse($this->waktu_absen)->format('H:i') : '-'; + } + + /** + * Accessor: Status Badge Class (CSS class only - untuk view santri) + */ + public function getStatusBadgeClassAttribute() + { + return match($this->status) { + 'Hadir' => 'badge-success', + 'Izin' => 'badge-info', + 'Sakit' => 'badge-warning', + 'Alpa' => 'badge-danger', + default => 'badge-secondary', + }; + } + + /** + * Scope: Filter by date range + */ + public function scopeDateRange($query, $start, $end) + { + return $query->whereBetween('tanggal', [$start, $end]); + } + + /** + * Scope: Filter by month + */ + public function scopeByMonth($query, $month, $year) + { + return $query->whereMonth('tanggal', $month) + ->whereYear('tanggal', $year); + } } \ No newline at end of file diff --git a/sim-pkpps/app/Models/KategoriKegiatan.php b/sim-pkpps/app/Models/KategoriKegiatan.php index b32ddff..0c09bc1 100644 --- a/sim-pkpps/app/Models/KategoriKegiatan.php +++ b/sim-pkpps/app/Models/KategoriKegiatan.php @@ -1,5 +1,5 @@ 'date', 'tanggal_kembali' => 'date', 'approved_at' => 'datetime', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', ]; /** - * Boot method untuk auto-generate ID Kepulangan + * Boot method - Auto generate ID & calculate durasi */ protected static function boot() { parent::boot(); static::creating(function ($model) { + // Generate ID Kepulangan if (empty($model->id_kepulangan)) { $last = Kepulangan::orderBy('id', 'desc')->first(); $num = $last ? intval(substr($last->id_kepulangan, 2)) + 1 : 1; $model->id_kepulangan = 'KP' . str_pad($num, 3, '0', STR_PAD_LEFT); } - // Auto-calculate durasi_izin + // PENTING: Hitung durasi_izin otomatis if ($model->tanggal_pulang && $model->tanggal_kembali) { - $pulang = Carbon::parse($model->tanggal_pulang); - $kembali = Carbon::parse($model->tanggal_kembali); - $model->durasi_izin = $pulang->diffInDays($kembali) + 1; + $model->durasi_izin = $model->hitungDurasiIzin( + $model->tanggal_pulang, + $model->tanggal_kembali + ); } - // Set tanggal_izin ke hari ini jika tidak diisi + // Set tanggal_izin jika kosong if (empty($model->tanggal_izin)) { $model->tanggal_izin = now(); } }); static::updating(function ($model) { - // Recalculate durasi_izin saat update + // Recalculate durasi_izin saat update tanggal if ($model->isDirty(['tanggal_pulang', 'tanggal_kembali'])) { - $pulang = Carbon::parse($model->tanggal_pulang); - $kembali = Carbon::parse($model->tanggal_kembali); - $model->durasi_izin = $pulang->diffInDays($kembali) + 1; + $model->durasi_izin = $model->hitungDurasiIzin( + $model->tanggal_pulang, + $model->tanggal_kembali + ); } }); } + /** + * Method untuk menghitung durasi izin dalam hari + * Formula: (tanggal_kembali - tanggal_pulang) + 1 + */ + private function hitungDurasiIzin($tanggalPulang, $tanggalKembali) + { + $pulang = Carbon::parse($tanggalPulang); + $kembali = Carbon::parse($tanggalKembali); + + // +1 karena hari pertama juga dihitung + return $pulang->diffInDays($kembali) + 1; + } + /** * Relasi ke Santri */ @@ -82,52 +97,38 @@ public function santri() } /** - * Accessor: Format tanggal izin + * Accessor: Format tanggal */ public function getTanggalIzinFormattedAttribute() { return $this->tanggal_izin ? $this->tanggal_izin->format('d F Y') : '-'; } - /** - * Accessor: Format tanggal pulang - */ public function getTanggalPulangFormattedAttribute() { return $this->tanggal_pulang ? $this->tanggal_pulang->format('d F Y') : '-'; } - /** - * Accessor: Format tanggal kembali - */ public function getTanggalKembaliFormattedAttribute() { return $this->tanggal_kembali ? $this->tanggal_kembali->format('d F Y') : '-'; } - /** - * Accessor: Format approved_at - */ public function getApprovedAtFormattedAttribute() { return $this->approved_at ? $this->approved_at->format('d F Y H:i') : '-'; } /** - * Accessor: Durasi izin calculated + * Accessor: Durasi izin calculated (untuk backward compatibility) */ public function getDurasiIzinCalculatedAttribute() { - if ($this->tanggal_pulang && $this->tanggal_kembali) { - $pulang = Carbon::parse($this->tanggal_pulang); - $kembali = Carbon::parse($this->tanggal_kembali); - return $pulang->diffInDays($kembali) + 1; - } - return $this->durasi_izin ?? 0; + return $this->durasi_izin; } /** - * Accessor: Status badge HTML + * Accessor: Status badge */ public function getStatusBadgeAttribute() { @@ -137,7 +138,6 @@ public function getStatusBadgeAttribute() 'Ditolak' => 'badge-danger', 'Selesai' => 'badge-secondary', ]; - return $badges[$this->status] ?? 'badge-secondary'; } @@ -163,24 +163,18 @@ public function getIsTerlambatAttribute() } /** - * Scope: Filter berdasarkan status + * Scopes */ public function scopeStatus($query, $status) { return $query->where('status', $status); } - /** - * Scope: Filter berdasarkan santri - */ public function scopeSantri($query, $idSantri) { return $query->where('id_santri', $idSantri); } - /** - * Scope: Kepulangan yang sedang aktif - */ public function scopeAktif($query) { $today = Carbon::today(); @@ -189,18 +183,12 @@ public function scopeAktif($query) ->whereDate('tanggal_kembali', '>=', $today); } - /** - * Scope: Kepulangan yang terlambat - */ public function scopeTerlambat($query) { return $query->where('status', 'Disetujui') ->whereDate('tanggal_kembali', '<', Carbon::today()); } - /** - * Scope: Search kepulangan - */ public function scopeSearch($query, $search) { return $query->where(function($q) use ($search) { @@ -215,33 +203,285 @@ public function scopeSearch($query, $search) } /** - * Static method: Get total hari izin per santri per tahun + * ======================================== + * FITUR KUOTA TAHUNAN + * ======================================== */ - public static function getTotalHariIzinSantri($idSantri, $tahun = null) + + /** + * Get settings kepulangan (kuota, periode) + */ + public static function getSettings() { - $tahun = $tahun ?? Carbon::now()->year; + $settings = DB::table('kepulangan_settings')->latest()->first(); + if (!$settings) { + // Create default settings + DB::table('kepulangan_settings')->insert([ + 'kuota_maksimal' => 12, + 'periode_mulai' => now()->startOfYear()->format('Y-m-d'), + 'periode_akhir' => now()->endOfYear()->format('Y-m-d'), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $settings = DB::table('kepulangan_settings')->latest()->first(); + } + + return (object) [ + 'id' => $settings->id, + 'kuota_maksimal' => $settings->kuota_maksimal, + 'periode_mulai' => Carbon::parse($settings->periode_mulai), + 'periode_akhir' => Carbon::parse($settings->periode_akhir), + 'terakhir_reset' => $settings->terakhir_reset ? Carbon::parse($settings->terakhir_reset) : null, + 'reset_by' => $settings->reset_by, + ]; + } + + /** + * Update settings kepulangan + */ + public static function updateSettings($kuotaMaksimal, $periodeMulai, $periodeAkhir) + { + $existing = DB::table('kepulangan_settings')->latest()->first(); + + if ($existing) { + DB::table('kepulangan_settings') + ->where('id', $existing->id) + ->update([ + 'kuota_maksimal' => $kuotaMaksimal, + 'periode_mulai' => $periodeMulai, + 'periode_akhir' => $periodeAkhir, + 'updated_at' => now(), + ]); + } else { + DB::table('kepulangan_settings')->insert([ + 'kuota_maksimal' => $kuotaMaksimal, + 'periode_mulai' => $periodeMulai, + 'periode_akhir' => $periodeAkhir, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + /** + * Get total hari izin santri dalam periode tertentu + */ + public static function getTotalHariIzinSantri($idSantri, $periodeMulai = null, $periodeAkhir = null) + { + if (!$periodeMulai || !$periodeAkhir) { + $settings = self::getSettings(); + $periodeMulai = $settings->periode_mulai; + $periodeAkhir = $settings->periode_akhir; + } + return self::where('id_santri', $idSantri) - ->where('status', 'Disetujui') - ->whereYear('tanggal_pulang', $tahun) + ->whereIn('status', ['Disetujui', 'Selesai']) + ->whereBetween('tanggal_pulang', [$periodeMulai, $periodeAkhir]) ->sum('durasi_izin'); } /** - * Static method: Check apakah santri over limit (lebih dari 12 hari/tahun) + * Get detail kuota santri */ - public static function isOverLimit($idSantri, $tahun = null) + public static function getSisaKuotaSantri($idSantri) { - $totalHari = self::getTotalHariIzinSantri($idSantri, $tahun); - return $totalHari > 12; + $settings = self::getSettings(); + + $totalTerpakai = self::getTotalHariIzinSantri( + $idSantri, + $settings->periode_mulai, + $settings->periode_akhir + ); + + $sisaKuota = $settings->kuota_maksimal - $totalTerpakai; + $persentase = $settings->kuota_maksimal > 0 ? + ($totalTerpakai / $settings->kuota_maksimal) * 100 : 0; + + // Tentukan status berdasarkan persentase + $status = 'aman'; // 0-80% + if ($persentase >= 80 && $persentase < 100) { + $status = 'hampir_habis'; // 80-100% + } elseif ($persentase >= 100) { + $status = 'melebihi'; // >100% + } + + // Warna badge + $badgeColor = 'success'; // Hijau + if ($persentase >= 80 && $persentase < 100) { + $badgeColor = 'warning'; // Kuning + } elseif ($persentase >= 100) { + $badgeColor = 'danger'; // Merah + } + + return [ + 'kuota_maksimal' => $settings->kuota_maksimal, + 'total_terpakai' => $totalTerpakai, + 'sisa_kuota' => max(0, $sisaKuota), + 'persentase' => round($persentase, 1), + 'status' => $status, + 'badge_color' => $badgeColor, + 'periode_mulai' => $settings->periode_mulai->format('d M Y'), + 'periode_akhir' => $settings->periode_akhir->format('d M Y'), + 'terakhir_reset' => $settings->terakhir_reset ? + $settings->terakhir_reset->format('d M Y') : '-', + ]; } /** - * Static method: Get sisa kuota izin santri + * Check apakah santri over limit */ - public static function getSisaKuota($idSantri, $tahun = null) + public static function isOverLimit($idSantri) { - $totalHari = self::getTotalHariIzinSantri($idSantri, $tahun); - return max(0, 12 - $totalHari); + $kuota = self::getSisaKuotaSantri($idSantri); + return $kuota['status'] === 'melebihi'; + } + + /** + * Get list santri yang over limit + */ + public static function getSantriOverLimit() + { + $settings = self::getSettings(); + + $santriIds = Santri::where('status', 'Aktif')->pluck('id_santri'); + $overLimitList = []; + + foreach ($santriIds as $idSantri) { + $totalHari = self::getTotalHariIzinSantri( + $idSantri, + $settings->periode_mulai, + $settings->periode_akhir + ); + + if ($totalHari > $settings->kuota_maksimal) { + $overLimitList[$idSantri] = $totalHari; + } + } + + return $overLimitList; + } + + /** + * ======================================== + * FITUR RESET KUOTA + * ======================================== + */ + + /** + * Reset kuota untuk satu santri + */ + public static function resetKuotaSantri($idSantri, $resetBy, $catatan = null) + { + $settings = self::getSettings(); + + // Hitung total hari sebelum reset + $totalHariSebelumReset = self::getTotalHariIzinSantri( + $idSantri, + $settings->periode_mulai, + $settings->periode_akhir + ); + + // Catat log reset + DB::table('kepulangan_reset_logs')->insert([ + 'id_santri' => $idSantri, + 'total_hari_sebelum_reset' => $totalHariSebelumReset, + 'periode_mulai' => $settings->periode_mulai, + 'periode_akhir' => $settings->periode_akhir, + 'kuota_tahunan' => $settings->kuota_maksimal, + 'jenis_reset' => 'individual', + 'reset_by' => $resetBy, + 'catatan' => $catatan, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // Update semua kepulangan santri yang Disetujui menjadi Selesai + // Ini cara "reset" dengan menandai semua izin lama sebagai selesai + self::where('id_santri', $idSantri) + ->where('status', 'Disetujui') + ->whereBetween('tanggal_pulang', [$settings->periode_mulai, $settings->periode_akhir]) + ->update(['status' => 'Selesai']); + + return [ + 'success' => true, + 'total_hari_direset' => $totalHariSebelumReset, + ]; + } + + /** + * Reset kuota untuk semua santri + */ + public static function resetKuotaSemuaSantri($resetBy, $catatan = null) + { + $settings = self::getSettings(); + + // Get semua santri aktif + $santriIds = Santri::where('status', 'Aktif')->pluck('id_santri'); + $totalSantri = $santriIds->count(); + $totalHariDireset = 0; + + foreach ($santriIds as $idSantri) { + $totalHari = self::getTotalHariIzinSantri( + $idSantri, + $settings->periode_mulai, + $settings->periode_akhir + ); + + $totalHariDireset += $totalHari; + } + + // Catat log reset massal + DB::table('kepulangan_reset_logs')->insert([ + 'id_santri' => null, // Null untuk reset massal + 'total_hari_sebelum_reset' => $totalHariDireset, + 'periode_mulai' => $settings->periode_mulai, + 'periode_akhir' => $settings->periode_akhir, + 'kuota_tahunan' => $settings->kuota_maksimal, + 'jenis_reset' => 'massal', + 'reset_by' => $resetBy, + 'catatan' => $catatan ?? "Reset massal untuk {$totalSantri} santri", + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // Update semua kepulangan yang Disetujui menjadi Selesai + $jumlahDiupdate = self::whereIn('id_santri', $santriIds) + ->where('status', 'Disetujui') + ->whereBetween('tanggal_pulang', [$settings->periode_mulai, $settings->periode_akhir]) + ->update(['status' => 'Selesai']); + + // Update tanggal terakhir reset di settings + DB::table('kepulangan_settings') + ->where('id', $settings->id) + ->update([ + 'terakhir_reset' => now(), + 'reset_by' => $resetBy, + 'updated_at' => now(), + ]); + + return [ + 'success' => true, + 'total_santri' => $totalSantri, + 'total_hari_direset' => $totalHariDireset, + 'jumlah_izin_diupdate' => $jumlahDiupdate, + ]; + } + + /** + * Get history reset logs + */ + public static function getResetLogs($limit = 20) + { + return DB::table('kepulangan_reset_logs') + ->leftJoin('santris', 'kepulangan_reset_logs.id_santri', '=', 'santris.id_santri') + ->select( + 'kepulangan_reset_logs.*', + 'santris.nama_lengkap' + ) + ->orderBy('kepulangan_reset_logs.created_at', 'desc') + ->limit($limit) + ->get(); } } \ No newline at end of file diff --git a/sim-pkpps/app/Models/Santri.php b/sim-pkpps/app/Models/Santri.php index 53b6d81..f31d735 100644 --- a/sim-pkpps/app/Models/Santri.php +++ b/sim-pkpps/app/Models/Santri.php @@ -1,5 +1,5 @@ absensiKegiatans()->where('status', 'Hadir')->count(); } + /** + * Accessor: URL Foto Santri (BARU) + */ + public function getFotoUrlAttribute() + { + if ($this->foto && file_exists(storage_path('app/public/' . $this->foto))) { + return asset('storage/' . $this->foto); + } + + // Fallback ke gambar default berdasarkan jenis kelamin + if ($this->jenis_kelamin === 'Perempuan') { + return asset('images/default-female.png'); + } + + return asset('images/default-male.png'); + } + /** * Scope untuk filter santri aktif */ diff --git a/sim-pkpps/app/Models/User.php b/sim-pkpps/app/Models/User.php index d45d1da..b54bdda 100644 --- a/sim-pkpps/app/Models/User.php +++ b/sim-pkpps/app/Models/User.php @@ -3,7 +3,6 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -13,41 +12,42 @@ class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; - /** - * The attributes that are mass assignable. - * - * @var array - */ protected $fillable = [ 'name', 'email', - 'username', // Ditambahkan + 'username', 'password', - 'role', // Ditambahkan - 'role_id', // Ditambahkan + 'role', + 'role_id', ]; - /** - * The attributes that should be hidden for serialization. - * - * @var array - */ protected $hidden = [ 'password', 'remember_token', ]; - /** - * The attributes that should be cast. - * - * @var array - */ protected $casts = [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; - - // Helper method untuk cek role + + /** + * Relasi ke Santri + */ + public function santri() + { + return $this->belongsTo(Santri::class, 'role_id', 'id_santri'); + } + + /** + * Relasi ke Wali + */ + public function wali() + { + return $this->belongsTo(Wali::class, 'role_id', 'id_wali'); + } + + // Helper methods public function isAdmin() { return $this->role === 'admin'; diff --git a/sim-pkpps/database/migrations/2025_10_20_080216_create_kepulangan_table.php b/sim-pkpps/database/migrations/2025_10_20_080216_create_kepulangan_table.php deleted file mode 100644 index f3d76db..0000000 --- a/sim-pkpps/database/migrations/2025_10_20_080216_create_kepulangan_table.php +++ /dev/null @@ -1,45 +0,0 @@ -id(); - $table->string('id_kepulangan')->unique(); // KP001, KP002, dst - $table->string('id_santri'); // Relasi ke santri (S001, S002, dst) - $table->date('tanggal_izin'); // Tanggal pengajuan izin - $table->date('tanggal_pulang'); // Tanggal mulai pulang - $table->date('tanggal_kembali'); // Tanggal rencana kembali - $table->integer('durasi_izin'); // Durasi dalam hari - $table->text('alasan'); // Alasan kepulangan - $table->enum('status', ['Menunggu', 'Disetujui', 'Ditolak', 'Selesai'])->default('Menunggu'); - $table->string('approved_by')->nullable(); // Admin yang menyetujui - $table->timestamp('approved_at')->nullable(); // Waktu persetujuan - $table->text('catatan')->nullable(); // Catatan dari admin - $table->timestamps(); - - // Foreign key - $table->foreign('id_santri') - ->references('id_santri') - ->on('santris') - ->onDelete('cascade'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('kepulangan'); - } -}; \ No newline at end of file diff --git a/sim-pkpps/database/migrations/2025_11_24_064857_create_kepulangan_table.php b/sim-pkpps/database/migrations/2025_11_24_064857_create_kepulangan_table.php new file mode 100644 index 0000000..a7198fa --- /dev/null +++ b/sim-pkpps/database/migrations/2025_11_24_064857_create_kepulangan_table.php @@ -0,0 +1,54 @@ +id(); + $table->string('id_kepulangan', 20)->unique()->index(); + $table->string('id_santri', 20); + $table->date('tanggal_izin'); + $table->date('tanggal_pulang'); + $table->date('tanggal_kembali'); + $table->integer('durasi_izin')->default(0)->comment('Durasi dalam hari'); + $table->text('alasan'); + $table->enum('status', ['Menunggu', 'Disetujui', 'Ditolak', 'Selesai']) + ->default('Menunggu'); + $table->string('approved_by', 100)->nullable(); + $table->timestamp('approved_at')->nullable(); + $table->text('catatan')->nullable()->comment('Catatan approval atau penolakan'); + $table->timestamps(); + + // Foreign key + $table->foreign('id_santri') + ->references('id_santri') + ->on('santris') + ->onDelete('cascade') + ->onUpdate('cascade'); + + // Indexes untuk performa + $table->index('status'); + $table->index('tanggal_pulang'); + $table->index('tanggal_kembali'); + $table->index(['id_santri', 'status']); + $table->index(['tanggal_pulang', 'tanggal_kembali']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('kepulangan'); + } +}; \ No newline at end of file diff --git a/sim-pkpps/database/migrations/2025_11_24_064906_create_kepulangan_settings_table.php b/sim-pkpps/database/migrations/2025_11_24_064906_create_kepulangan_settings_table.php new file mode 100644 index 0000000..2fd9deb --- /dev/null +++ b/sim-pkpps/database/migrations/2025_11_24_064906_create_kepulangan_settings_table.php @@ -0,0 +1,42 @@ +id(); + $table->integer('kuota_maksimal')->default(12)->comment('Kuota hari izin per tahun'); + $table->date('periode_mulai')->comment('Awal periode kuota'); + $table->date('periode_akhir')->comment('Akhir periode kuota'); + $table->timestamp('terakhir_reset')->nullable(); + $table->string('reset_by', 100)->nullable(); + $table->timestamps(); + }); + + // Insert default settings + DB::table('kepulangan_settings')->insert([ + 'kuota_maksimal' => 12, + 'periode_mulai' => now()->startOfYear()->format('Y-m-d'), + 'periode_akhir' => now()->endOfYear()->format('Y-m-d'), + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('kepulangan_settings'); + } +}; \ No newline at end of file diff --git a/sim-pkpps/database/migrations/2025_11_24_064914_create_kepulangan_reset_logs_table.php b/sim-pkpps/database/migrations/2025_11_24_064914_create_kepulangan_reset_logs_table.php new file mode 100644 index 0000000..f8712c8 --- /dev/null +++ b/sim-pkpps/database/migrations/2025_11_24_064914_create_kepulangan_reset_logs_table.php @@ -0,0 +1,48 @@ +id(); + $table->string('id_santri', 20)->nullable()->comment('Null jika reset massal'); + $table->integer('total_hari_sebelum_reset')->default(0); + $table->date('periode_mulai'); + $table->date('periode_akhir'); + $table->integer('kuota_tahunan'); + $table->enum('jenis_reset', ['individual', 'massal'])->default('individual'); + $table->string('reset_by', 100); + $table->text('catatan')->nullable(); + $table->timestamps(); + + // Foreign key (nullable untuk reset massal) + $table->foreign('id_santri') + ->references('id_santri') + ->on('santris') + ->onDelete('set null') + ->onUpdate('cascade'); + + // Indexes + $table->index('id_santri'); + $table->index('jenis_reset'); + $table->index('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('kepulangan_reset_logs'); + } +}; \ No newline at end of file diff --git a/sim-pkpps/database/migrations/2025_12_19_131348_add_foto_to_santris_table.php b/sim-pkpps/database/migrations/2025_12_19_131348_add_foto_to_santris_table.php new file mode 100644 index 0000000..970e38b --- /dev/null +++ b/sim-pkpps/database/migrations/2025_12_19_131348_add_foto_to_santris_table.php @@ -0,0 +1,29 @@ +string('foto')->nullable()->after('rfid_uid'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('santris', function (Blueprint $table) { + $table->dropColumn('foto'); + }); + } +}; \ No newline at end of file diff --git a/sim-pkpps/resources/views/admin/kepulangan/create.blade.php b/sim-pkpps/resources/views/admin/kepulangan/create.blade.php index 95feb86..97b0dd6 100644 --- a/sim-pkpps/resources/views/admin/kepulangan/create.blade.php +++ b/sim-pkpps/resources/views/admin/kepulangan/create.blade.php @@ -9,6 +9,20 @@

Tambah Izin Kepulangan

+{{-- Info Periode Kuota --}} +
+
+
+ 📅 Periode Kuota:
+ {{ $settings->periode_mulai->format('d M Y') }} - {{ $settings->periode_akhir->format('d M Y') }} +
+
+ 📊 Kuota Maksimal:
+ {{ $settings->kuota_maksimal }} Hari / Tahun +
+
+
+ @if($errors->any())
    @@ -38,22 +52,34 @@
- {{-- Info Santri --}} + {{-- Info Santri & Kuota --}} - {{-- Info Durasi --}} -