From 019375906dae931504321f6375eadac2d353c39e Mon Sep 17 00:00:00 2001 From: zerobugteam-zekkk Date: Thu, 23 Jul 2026 18:33:05 +0700 Subject: [PATCH] Initial commit --- ESP32_OPTIMIZATION_REPORT.md | 183 +++ Iot_Nawasena/rfid_suara.ino | 282 ++++ Set-Content | 0 app.zip | Bin 0 -> 69829 bytes app/Console/Commands/MqttListen.php | 329 +++++ app/Console/Commands/SetupLanguages.php | 1 + app/Events/RfidScanned.php | 27 + app/Filament/Resources/BookResource.php | 1 + .../BookResource/Pages/CreateBook.php | 1 + .../Resources/BookResource/Pages/EditBook.php | 1 + .../BookResource/Pages/ListBooks.php | 1 + app/Filament/Resources/MemberResource.php | 1 + .../MemberResource/Pages/CreateMember.php | 1 + .../MemberResource/Pages/EditMember.php | 1 + .../MemberResource/Pages/ListMembers.php | 1 + .../Controllers/Admin/AnggotaController.php | 297 +++- .../Controllers/Admin/DashboardController.php | 94 +- .../Controllers/Admin/KoleksiController.php | 118 +- .../Admin/PeminjamanController.php | 320 +++-- .../Controllers/Admin/StatistikController.php | 182 ++- .../Controllers/Admin/SystemController.php | 148 +- app/Http/Controllers/AdminController.php | 1 + app/Http/Controllers/Api/IotController.php | 17 + .../Controllers/Auth/GoogleController.php | 1 + .../Auth/LoginWithEmailController.php | 1 + .../Auth/OtpVerificationController.php | 1 + .../Controllers/Auth/RegisterController.php | 1 + .../Auth/VerificationController.php | 1 + app/Http/Controllers/AuthController.php | 98 +- .../BorrowTransactionController.php | 1 + app/Http/Controllers/Controller.php | 1 + app/Http/Controllers/DashboardController.php | 1 + app/Http/Controllers/EspController.php | 107 ++ app/Http/Controllers/GoogleController.php | 54 + app/Http/Controllers/HomeController.php | 1 + app/Http/Controllers/IotModeController.php | 83 ++ .../Controllers/User/KoleksiController.php | 1 + .../Controllers/User/SecurityController.php | 1 + app/Http/Controllers/User/UserController.php | 1 + .../Controllers/User/UserMemberController.php | 204 ++- .../User/UserPeminjamanController.php | 262 ++-- .../User/UserProfileController.php | 8 +- .../Controllers/User/UserRatingController.php | 1 + app/Http/Controllers/UserController.php | 1 + .../Controllers/UserDashboardController.php | 1 + app/Http/Kernel.php | 23 +- app/Http/Middleware/AdminMiddleware.php | 1 + app/Http/Middleware/BypassNgro.php | 29 + app/Http/Middleware/CheckRole.php | 3 +- app/Http/Middleware/SetLocale.php | 1 + app/Http/Middleware/SkipNgrokWarning.php | 27 + app/Jobs/ProcessBorrowTransaction.php | 34 + app/Jobs/ProcessPeminjamanJob.php | 186 +++ app/Jobs/ProcessPengembalianJob.php | 72 + app/Livewire/Admin/NotificationBadges.php | 55 + app/Mail/AdminNotifikasiAnggotaBaru.php | 1 + app/Mail/AnggotaBaruMenungguPersetujuan.php | 1 + app/Mail/BukuDikembalikanMail.php | 1 + app/Mail/BukuSiapDiambilMail.php | 1 + app/Mail/JatuhTempoMail.php | 22 + app/Mail/PeminjamanDitolakMail.php | 1 + app/Mail/PeminjamanMenungguApproval.php | 1 + app/Mail/PengembalianMenungguApproval.php | 1 + app/Mail/PeringatanKeterlambatanMail.php | 1 + app/Mail/PerpanjanganDisetujuiMail.php | 1 + app/Mail/PerpanjanganDitolakMail.php | 1 + app/Mail/PerpanjanganMenungguApproval.php | 1 + app/Mail/SendOtpMail.php | 1 + app/Mail/StatusAnggotaDiperbarui.php | 1 + app/Models/Anggota.php | 8 +- app/Models/Book.php | 1 + app/Models/BorrowTransaction.php | 17 +- app/Models/Fasilitas.php | 1 + app/Models/IotMode.php | 16 + app/Models/Item.php | 1 + app/Models/Koleksi.php | 2 + app/Models/Member.php | 15 +- app/Models/Peminjaman.php | 1 + app/Models/PublishPlace.php | 1 + app/Models/Publisher.php | 3 +- app/Models/Rating.php | 1 + app/Models/Setting.php | 1 + app/Models/User.php | 33 +- app/Models/UserLogin.php | 1 + .../PerpanjanganPeminjamanNotification.php | 1 + app/Observers/IotModeObserver.php | 72 + app/Providers/AppServiceProvider.php | 39 +- app/Providers/Filament/AdminPanelProvider.php | 1 + app/Providers/RouteServiceProvider.php | 1 + bootstrap/app.php | 17 +- bootstrap/cache/pac5DA9.tmp | 196 +++ bootstrap/cache/packages.php | 11 +- bootstrap/cache/ser864F.tmp | 297 ++++ bootstrap/cache/services.php | 53 +- bootstrap/providers.php | 1 + composer.json | 6 +- composer.lock | 1225 ++++++++++++++++- config/app.php | 1 + config/auth.php | 1 + config/broadcasting.php | 83 ++ config/cache.php | 1 + config/database.php | 1 + config/filament.php | 5 +- config/filesystems.php | 1 + config/logging.php | 1 + config/mail.php | 1 + config/qrcode.php | 1 + config/queue.php | 1 + config/reverb.php | 103 ++ config/services.php | 1 + config/session.php | 1 + database/factories/UserFactory.php | 1 + .../0001_01_01_000000_create_users_table.php | 1 + .../0001_01_01_000001_create_cache_table.php | 1 + .../0001_01_01_000002_create_jobs_table.php | 1 + .../2025_10_13_041740_create_books_table.php | 1 + ...2025_10_13_041741_create_members_table.php | 1 + ...025_10_13_061335_create_sessions_table.php | 1 + ...025_10_13_115703_create_koleksis_table.php | 1 + ...5_10_13_131310_add_role_to_users_table.php | 1 + ...3_132558_add_admin_role_to_users_table.php | 1 + ...025_10_14_141805_create_anggotas_table.php | 1 + ...63805_create_borrow_transactions_table.php | 1 + ...update_koleksis_table_to_new_structure.php | 3 +- ...5_10_16_035039_create_publishers_table.php | 1 + ..._16_035040_create_publish_places_table.php | 1 + ...6_164735_add_qr_code_to_koleksis_table.php | 1 + ...025_10_17_171153_create_settings_table.php | 1 + ...oleksi_id_to_borrow_transactions_table.php | 1 + ...081328_clean_borrow_transactions_table.php | 1 + ..._18_093200_add_is_admin_to_users_table.php | 1 + ...19_091732_remove_role_from_users_table.php | 1 + ...5_10_19_092031_add_role_to_users_table.php | 1 + ..._025829_drop_is_admin_from_users_table.php | 1 + ...pengantar_to_borrow_transactions_table.php | 1 + ..._10_23_150118_create_user_logins_table.php | 1 + ...25_10_23_154111_create_fasilitas_table.php | 1 + ...ckup_date_to_borrow_transactions_table.php | 1 + ...ckup_date_to_borrow_transactions_table.php | 1 + ...ckup_date_in_borrow_transactions_table.php | 1 + ...6_add_email_login_token_to_users_table.php | 1 + ..._add_verification_token_to_users_table.php | 1 + ...dd_email_otp_expires_at_to_users_table.php | 1 + ...54_add_otp_last_sent_at_to_users_table.php | 1 + ...29_222551_add_status_to_anggotas_table.php | 1 + ...2025_10_29_232225_create_ratings_table.php | 1 + ...ckup_time_to_borrow_transactions_table.php | 1 + ...3_add_filter_columns_to_koleksis_table.php | 1 + ...panjangan_to_borrow_transactions_table.php | 1 + ...atus_enum_in_borrow_transactions_table.php | 1 + ...gan_count_to_borrow_transactions_table.php | 1 + ...n_columns_to_borrow_transactions_table.php | 1 + ...323_create_password_reset_tokens_table.php | 1 + ...d_ditolak_to_borrow_transactions_table.php | 1 + ...4_231251_add_barcode_to_koleksis_table.php | 1 + ...19_131904_add_nip_nim_to_anggota_table.php | 1 + ...5_11_19_175517_add_foto_to_users_table.php | 1 + ...2_231739_add_blocked_at_to_users_table.php | 1 + ...d_is_late_to_borrow_transactions_table.php | 1 + ...12_03_203552_add_foto_to_members_table.php | 1 + ...26_03_11_195541_create_iot_modes_table.php | 29 + ...05_211611_add_source_to_koleksis_table.php | 29 + ...6_04_19_192224_add_rfid_to_users_table.php | 29 + ...2026_04_19_192643_add_book_to_iot_mode.php | 20 + ..._add_transaction_id_to_iot_modes_table.php | 27 + ...6_04_27_001236_add_performance_indexes.php | 95 ++ ...ate_status_enum_in_borrow_transactions.php | 23 + ..._feedback_to_borrow_transactions_table.php | 27 + ...12_212919_add_google_id_to_users_table.php | 30 + ...08_modify_status_enum_add_perpanjangan.php | 36 + ..._04_185342_add_gender_to_anggota_table.php | 28 + database/seeders/AdminUserSeeder.php | 1 + database/seeders/DatabaseSeeder.php | 1 + database/seeders/FasilitasSeeder.php | 1 + database/seeders/KoleksiSeeder.php | 1 + database/seeders/LibrarySeeder.php | 1 + dump.rdb | Bin 0 -> 95 bytes package-lock.json | 151 +- package.json | 4 + public/.htaccess | 66 +- public/favicon.ico | Bin 0 -> 99637 bytes public/index.php | 1 + resources/views/admin/anggota/cetak.blade.php | 179 --- resources/views/admin/anggota/edit.blade.php | 334 ++++- resources/views/admin/anggota/index.blade.php | 557 +++++--- resources/views/admin/anggota/print.blade.php | 261 ++++ resources/views/admin/dashboard.blade.php | 685 +++++---- resources/views/admin/galeri.blade.php | 1 + .../admin/koleksi/barcode_multiple.blade.php | 101 ++ .../admin/koleksi/cetak_barcode.blade.php | 343 +++++ .../views/admin/koleksi/create.blade.php | 425 +++--- resources/views/admin/koleksi/edit.blade.php | 435 ++++-- resources/views/admin/koleksi/index.blade.php | 579 +++++--- .../koleksi/label_barcode_single.blade.php | 3 +- resources/views/admin/layout.blade.php | 3 +- resources/views/admin/peminjaman.blade.php | 593 +++++--- .../views/admin/peminjaman/nota.blade.php | 122 ++ resources/views/admin/profile.blade.php | 6 + resources/views/admin/security.blade.php | 364 +++-- resources/views/admin/statistik.blade.php | 517 ++++++- resources/views/admin/system.blade.php | 450 ++++-- .../views/auth/forgot-password.blade.php | 1 + resources/views/auth/login-email.blade.php | 1 + resources/views/auth/reset-password.blade.php | 1 + resources/views/auth/verify-notice.blade.php | 1 + .../emails/anggota_baru_menunggu.blade.php | 1 + .../views/emails/buku_dikembalikan.blade.php | 1 + .../views/emails/buku_siap_diambil.blade.php | 1 + resources/views/emails/jatuh_tempo.blade.php | 15 + .../views/emails/peminjaman_ditolak.blade.php | 1 + .../emails/peminjaman_menunggu.blade.php | 1 + .../emails/pengembalian-menunggu.blade.php | 1 + .../emails/peringatan_keterlambatan.blade.php | 1 + .../emails/perpanjangan_disetujui.blade.php | 1 + .../emails/perpanjangan_ditolak.blade.php | 1 + .../emails/perpanjangan_menunggu.blade.php | 1 + resources/views/emails/send-otp.blade.php | 1 + .../status_anggota_diperbarui.blade.php | 1 + resources/views/layouts/admin.blade.php | 1 + resources/views/layouts/anggota.blade.php | 1 + resources/views/layouts/app.blade.php | 1 + resources/views/layouts/dashboard.blade.php | 166 ++- resources/views/layouts/edit.blade.php | 1 + resources/views/layouts/guest.blade.php | 1 + resources/views/layouts/home.blade.php | 1 + resources/views/layouts/koleksi.blade.php | 1 + resources/views/layouts/kontak.blade.php | 1 + resources/views/layouts/layanan.blade.php | 1 + resources/views/layouts/pustaka.blade.php | 1 + resources/views/layouts/transaksi.blade.php | 117 ++ .../views/layouts/user-koleksi.blade.php | 7 +- resources/views/layouts/user.blade.php | 39 +- .../admin/notification-badges.blade.php | 130 ++ resources/views/login.blade.php | 367 ++--- resources/views/register.blade.php | 410 +++--- .../views/transaksi/detail-buku.blade.php | 413 ++++++ .../views/transaksi/hasil-transaksi.blade.php | 495 +++++++ .../transaksi/konfirmasi-kembali.blade.php | 378 +++++ .../transaksi/peminjaman-berhasil.blade.php | 203 +++ .../transaksi/pengembalian-berhasil.blade.php | 203 +++ .../views/transaksi/scan-barcode.blade.php | 361 +++++ resources/views/transaksi/scan-rfid.blade.php | 296 ++++ resources/views/user/dashboard.blade.php | 500 ++++--- resources/views/user/fasilitas.blade.php | 362 +++-- resources/views/user/koleksi.blade.php | 459 +++--- resources/views/user/koleksi/show.blade.php | 200 ++- resources/views/user/kontak-lokasi.blade.php | 269 +++- .../views/user/layanan/digital.blade.php | 558 ++++++-- resources/views/user/layanan/index.blade.php | 359 +++-- .../views/user/layanan/informasi.blade.php | 513 +++++-- .../views/user/layanan/peminjaman.blade.php | 587 +++++--- .../views/user/layanan/referensi.blade.php | 557 ++++++-- resources/views/user/member/create.blade.php | 789 +++++++++-- resources/views/user/member/index.blade.php | 1 + resources/views/user/member/pending.blade.php | 398 ++++++ .../views/user/peminjaman/create.blade.php | 454 +++--- .../views/user/peminjaman/index.blade.php | 587 +++++--- .../user/peminjaman/perpanjang.blade.php | 252 ++++ resources/views/user/profile.blade.php | 500 +++---- resources/views/user/rating/form.blade.php | 1 + resources/views/user/security.blade.php | 365 +++-- resources/views/verify-email.blade.php | 5 + resources/views/welcome.blade.php | 558 ++++---- routes/api.php | 373 +++++ routes/api/scan-member | 45 + routes/channels.php | 8 + routes/console.php | 1 + routes/web.php | 116 +- storage/app/public/.gitignore | 2 - tests/Feature/ExampleTest.php | 1 + tests/TestCase.php | 1 + tests/Unit/ExampleTest.php | 1 + } | 0 273 files changed, 19136 insertions(+), 5449 deletions(-) create mode 100644 ESP32_OPTIMIZATION_REPORT.md create mode 100644 Iot_Nawasena/rfid_suara.ino create mode 100644 Set-Content create mode 100644 app.zip create mode 100644 app/Console/Commands/MqttListen.php create mode 100644 app/Events/RfidScanned.php create mode 100644 app/Http/Controllers/Api/IotController.php create mode 100644 app/Http/Controllers/EspController.php create mode 100644 app/Http/Controllers/GoogleController.php create mode 100644 app/Http/Controllers/IotModeController.php create mode 100644 app/Http/Middleware/BypassNgro.php create mode 100644 app/Http/Middleware/SkipNgrokWarning.php create mode 100644 app/Jobs/ProcessBorrowTransaction.php create mode 100644 app/Jobs/ProcessPeminjamanJob.php create mode 100644 app/Jobs/ProcessPengembalianJob.php create mode 100644 app/Livewire/Admin/NotificationBadges.php create mode 100644 app/Mail/JatuhTempoMail.php create mode 100644 app/Models/IotMode.php create mode 100644 app/Observers/IotModeObserver.php create mode 100644 bootstrap/cache/pac5DA9.tmp create mode 100644 bootstrap/cache/ser864F.tmp create mode 100644 config/broadcasting.php create mode 100644 config/reverb.php create mode 100644 database/migrations/2026_03_11_195541_create_iot_modes_table.php create mode 100644 database/migrations/2026_04_05_211611_add_source_to_koleksis_table.php create mode 100644 database/migrations/2026_04_19_192224_add_rfid_to_users_table.php create mode 100644 database/migrations/2026_04_19_192643_add_book_to_iot_mode.php create mode 100644 database/migrations/2026_04_23_002401_add_transaction_id_to_iot_modes_table.php create mode 100644 database/migrations/2026_04_27_001236_add_performance_indexes.php create mode 100644 database/migrations/2026_04_28_214437_update_status_enum_in_borrow_transactions.php create mode 100644 database/migrations/2026_05_01_011758_add_admin_feedback_to_borrow_transactions_table.php create mode 100644 database/migrations/2026_05_12_212919_add_google_id_to_users_table.php create mode 100644 database/migrations/2026_05_27_235808_modify_status_enum_add_perpanjangan.php create mode 100644 database/migrations/2026_07_04_185342_add_gender_to_anggota_table.php create mode 100644 dump.rdb delete mode 100644 resources/views/admin/anggota/cetak.blade.php create mode 100644 resources/views/admin/anggota/print.blade.php create mode 100644 resources/views/admin/koleksi/barcode_multiple.blade.php create mode 100644 resources/views/admin/koleksi/cetak_barcode.blade.php create mode 100644 resources/views/admin/peminjaman/nota.blade.php create mode 100644 resources/views/emails/jatuh_tempo.blade.php create mode 100644 resources/views/layouts/transaksi.blade.php create mode 100644 resources/views/livewire/admin/notification-badges.blade.php create mode 100644 resources/views/transaksi/detail-buku.blade.php create mode 100644 resources/views/transaksi/hasil-transaksi.blade.php create mode 100644 resources/views/transaksi/konfirmasi-kembali.blade.php create mode 100644 resources/views/transaksi/peminjaman-berhasil.blade.php create mode 100644 resources/views/transaksi/pengembalian-berhasil.blade.php create mode 100644 resources/views/transaksi/scan-barcode.blade.php create mode 100644 resources/views/transaksi/scan-rfid.blade.php create mode 100644 resources/views/user/member/pending.blade.php create mode 100644 resources/views/user/peminjaman/perpanjang.blade.php create mode 100644 routes/api.php create mode 100644 routes/api/scan-member create mode 100644 routes/channels.php delete mode 100644 storage/app/public/.gitignore create mode 100644 } diff --git a/ESP32_OPTIMIZATION_REPORT.md b/ESP32_OPTIMIZATION_REPORT.md new file mode 100644 index 0000000..6734202 --- /dev/null +++ b/ESP32_OPTIMIZATION_REPORT.md @@ -0,0 +1,183 @@ +# πŸš€ ESP32 OPTIMIZATION REPORT + +## MASALAH YANG DITEMUKAN: + +### 1. ⚠️ HTTP TIMEOUT TERLALU LAMA +```cpp +SEBELUM: http.setTimeout(11000); // 11 DETIK! +SESUDAH: http.setTimeout(3000); // 3 DETIK (3.7x lebih cepat) +``` +**Dampak**: Jika timeout, ESP32 freeze 11 detik β†’ 3 detik + +--- + +### 2. ⚠️ BLOCKING POLLING DI MAIN LOOP +**SEBELUM:** +```cpp +void loop() { + updateModeFromServer(); // ← BLOCK 2-11 detik SETIAP kali + // RFID detection tidak berjalan saat polling +} +``` + +**SESUDAH:** +```cpp +// ⭐ Timer-based polling (NON-BLOCKING) +if (millis() - lastModeUpdate < MODE_UPDATE_INTERVAL) return; +updateModeFromServer(); // ← Hanya jika timer tercapai +// RFID tetap responsif! +``` + +--- + +### 3. ⚠️ WIFI RECONNECTION LOGIC LAMBAT +```cpp +SEBELUM: +WiFi.disconnect(); // Disconnect dulu (lambat) +WiFi.begin(ssid, password); + +SESUDAH: +WiFi.mode(WIFI_OFF); // Off total +delay(100); +WiFi.mode(WIFI_STA); // On ulang (lebih cepat) +WiFi.begin(ssid, password); +``` + +--- + +### 4. ⚠️ BLOCKING STATUS CHECKS +```cpp +SEBELUM: +if (WiFi.status() == WL_CONNECTED && !isAudioPlaying) { + updateModeFromServer(); // ← Walaupun cek tidak block, polling masih block +} + +SESUDAH: +// Tambah flag untuk prevent concurrent polling +if (isPollingInProgress) return; +isPollingInProgress = true; +// ... polling +isPollingInProgress = false; // Mark selesai +``` + +--- + +## βœ… PERUBAHAN OPTIMASI: + +| No | Perubahan | Sebelum | Sesudah | Dampak | +|----|-----------|---------|----------|--------| +| 1 | HTTP Timeout | 11000ms | 3000ms | ⚑ 3.7x lebih cepat | +| 2 | Connect Timeout | (none) | 3000ms | ⚑ Fail-fast jika koneksi gagal | +| 3 | Polling Pattern | Blocking loop | Timer-based | ⚑ Non-blocking, RFID responsif | +| 4 | Polling Interval | 2s (blocking) | 1.5s (non-blocking) | ⚑ 33% lebih sering cek | +| 5 | WiFi Reconnect | 10s interval | 5s interval | ⚑ 2x lebih cepat recover | +| 6 | JSON Parser | StaticJsonDocument | DynamicJsonDocument | ⚑ Lebih flexible | +| 7 | Loop Delay | (none) | 10ms | ⚑ Stabilitas & efficiency | +| 8 | RFID Timeout | 5000ms | 3000ms | ⚑ Fail-fast | + +--- + +## πŸ“Š HASIL YANG DIHARAPKAN: + +### Response Time: +- **Mode Update**: 2-11s β†’ **0.3-1.5s** (5-10x lebih cepat) +- **RFID Scan Response**: ~5s β†’ **2-3s** (2x lebih cepat) +- **Overall Responsiveness**: Signifikan improvement + +### Resource: +- **Memory**: Sedikit lebih banyak (polling flag) tapi tidak significant +- **WiFi**: Lebih efficient (connection reuse, proper timeout) +- **CPU**: Tidak akan freeze saat polling timeout + +--- + +## πŸ”§ IMPLEMENTASI: + +### Option 1: Copy-Paste Langsung +File sudah ada di: `ESP32_OPTIMIZED.ino` + +Ganti kode lama Anda dengan kode baru ini. + +### Option 2: Manual Apply Changes +Jika ingin manual, apply perubahan berikut di kode Anda: + +1. **Tambah di deklarasi state:** +```cpp +unsigned long lastModeUpdate = 0; +const unsigned long MODE_UPDATE_INTERVAL = 1500; +bool isPollingInProgress = false; +``` + +2. **Ganti updateModeFromServer():** +```cpp +void updateModeFromServer() { + if (isPollingInProgress || isAudioPlaying) return; + if (millis() - lastModeUpdate < MODE_UPDATE_INTERVAL) return; + + isPollingInProgress = true; + lastModeUpdate = millis(); + + HTTPClient http; + http.setReuse(true); + http.setTimeout(3000); // ← UTAMA: Kurangi dari 11000 + http.setConnectTimeout(3000); + + // ... rest of code + + isPollingInProgress = false; +} +``` + +3. **Update loop():** +```cpp +void loop() { + keepWiFiAlive(); + checkAudioStatus(); + + if (WiFi.status() == WL_CONNECTED && !isAudioPlaying) { + updateModeFromServer(); // Sekarang non-blocking! + } + + if (!isAudioPlaying && (millis() - lastSuccessfulScan > scanDelay)) { + if (rfid.PICC_IsNewCardPresent() && rfid.PICC_ReadCardSerial()) { + handleRFID(); + } + } + + // ... display logic + + delay(10); // ← Tambah delay kecil untuk stability +} +``` + +--- + +## ⚠️ TESTING CHECKLIST: + +- [ ] Upload kode baru ke ESP32 +- [ ] Monitor Serial output (buka Serial Monitor 115200 baud) +- [ ] Cek response time RFID scan (seharusnya ~2-3 detik) +- [ ] Cek mode update responsiveness +- [ ] Cek WiFi reconnection behavior +- [ ] Test dengan multiple RFID scans berturut-turut + +--- + +## πŸ“ CATATAN PENTING: + +- Polling interval 1.5s berarti worst-case mode update delay adalah ~1.5 detik +- Jika perlu lebih realtime, bisa kurangi ke 1 detik +- HTTP timeout 3s sudah optimal untuk LAN - jangan lebih rendah +- Jika masih lambat, kemungkinan WiFi signal lemah atau server overload + +--- + +## 🎯 NEXT STEPS: + +Jika masih lambat setelah ini: +1. Check WiFi signal strength (RSSI) +2. Monitor server CPU/RAM usage +3. Enable Serial debug untuk lihat timing detil +4. Bisa gunakan WebSocket instead of polling (lebih advanced) + +**Update saya setelah test!** πŸ‘ diff --git a/Iot_Nawasena/rfid_suara.ino b/Iot_Nawasena/rfid_suara.ino new file mode 100644 index 0000000..058cfe4 --- /dev/null +++ b/Iot_Nawasena/rfid_suara.ino @@ -0,0 +1,282 @@ +#include +#include +#include "DFRobotDFPlayerMini.h" +#include +#include +#include +#include +#include + +/* ================= CONFIGURATION ================= */ +const char* ssid = "OPPO A57"; +const char* password = "11223344"; +const char* mqtt_server = "192.168.254.121"; +const int mqtt_port = 1883; + +/* ================= OBJECTS & PINS ================= */ +#define SS_PIN 5 +#define RST_PIN 4 +#define BUZZER_PIN 13 +MFRC522 rfid(SS_PIN, RST_PIN); +HardwareSerial mySerial(2); +DFRobotDFPlayerMini dfplayer; +LiquidCrystal_I2C lcd(0x27, 16, 2); + +WiFiClient espClient; +PubSubClient client(espClient); + +int merah = 23, hijau = 22, biru = 21; + +/* ================= STATE & TIMERS ================= */ +String mode = "standby"; +unsigned long lastWifiCheck = 0; +unsigned long lastHeartbeat = 0; +bool isAudioPlaying = false; +unsigned long audioStartTime = 0; +const unsigned long AUDIO_DURATION = 3500; // jeda 3 detik + +String lastUID = ""; +unsigned long lastScanTime = 0; +const unsigned long SCAN_DELAY = 3000; // jeda 3 detik + +String scrollMessage = " NAWASENA SMART LIBRARY SYSTEM - BAKORWIL III MALANG "; +unsigned long lastScroll = 0; +int scrollCursor = 0; + +/* ================= HELPERS ================= */ +void setRGB(bool r, bool g, bool b) { + digitalWrite(merah, r); + digitalWrite(hijau, g); + digitalWrite(biru, b); +} + +void beep(int duration) { + digitalWrite(BUZZER_PIN, HIGH); + delay(duration); + digitalWrite(BUZZER_PIN, LOW); +} + +void playTrack(int track) { + if (isAudioPlaying) return; + Serial.print("πŸ”Š Memutar Track: "); Serial.println(track); + dfplayer.play(track); + isAudioPlaying = true; + audioStartTime = millis(); +} + +void applyModeVisual() { + if (mode == "admin") setRGB(1, 1, 0); + else if (mode == "scan_member") setRGB(0, 1, 0); + else if (mode == "wait") setRGB(1, 0, 0); + else if (mode == "identified") setRGB(0, 1, 1); + else setRGB(0, 0, 1); +} + + +/* ================= MQTT CALLBACK ================= */ +void callback(char* topic, byte* payload, unsigned int length) { + String message = ""; + for (int i = 0; i < length; i++) message += (char)payload[i]; + + Serial.println("πŸ“₯ MQTT Diterima [" + String(topic) + "]: " + message); + + if (String(topic) == "rfid/mode/set") { + mode = message; + lcd.clear(); + if (mode == "scan_member") playTrack(6); + else if (mode == "admin") playTrack(5); + applyModeVisual(); + } + + if (String(topic) == "rfid/status") { + lcd.clear(); + if (message == "valid") { + mode = "identified"; + lcd.setCursor(0, 0); lcd.print(" KARTU TERBACA "); + lcd.setCursor(0, 1); lcd.print(" SILAHKAN... "); + playTrack(4); + } + else if (message == "success" || message == "finish") { + setRGB(0, 1, 0); + lcd.setCursor(0, 0); lcd.print(" TRANSAKSI OK "); + lcd.setCursor(0, 1); lcd.print(" TERIMA KASIH "); + playTrack(8); + delay(2500); + mode = "standby"; + } + else if (message == "registered") { + setRGB(0, 1, 0); + lcd.setCursor(0, 0); lcd.print(" REGISTRASI OK "); + lcd.setCursor(0, 1); lcd.print(" KARTU TERTAUT "); + playTrack(7); + delay(2500); + mode = "standby"; + } + else if (message == "error" || message == "failed") { + setRGB(1, 0, 0); + lcd.setCursor(0, 0); lcd.print(" AKSES DITOLAK "); + lcd.setCursor(0, 1); lcd.print(" COBA LAGI... "); + playTrack(3); + delay(2500); + } + lcd.clear(); + applyModeVisual(); + } +} +void reconnectMQTT() { + while (!client.connected()) { + Serial.print("πŸ”„ Menghubungkan MQTT..."); + String clientId = "ESP32_Nawasena_" + String(random(0xffff), HEX); + + if (client.connect(clientId.c_str(), NULL, NULL, "rfid/device/status", 1, true, "offline")) { + Serial.println("TERHUBUNG!"); + client.subscribe("rfid/mode/set"); + client.subscribe("rfid/status"); + client.subscribe("rfid/scan/admin"); + client.publish("rfid/device/status", "online", true); + } else { + Serial.print("Gagal, rc="); Serial.print(client.state()); + delay(5000); + } + } +} + +/* ================= CORE FUNCTIONS ================= */ + +void handleRFID() { + String uid = ""; + for (byte i = 0; i < rfid.uid.size; i++) { + uid += (rfid.uid.uidByte[i] < 0x10 ? "0" : ""); + uid += String(rfid.uid.uidByte[i], HEX); + } + uid.toUpperCase(); + + if (uid == lastUID && (millis() - lastScanTime < SCAN_DELAY)) { + // Sesi dihentikan dengan aman jika kartu yang sama di-tap berulang kali + rfid.PICC_HaltA(); + rfid.PCD_StopCrypto1(); + return; + } + + // Perbarui waktu scan terakhir yang sukses + lastUID = uid; + lastScanTime = millis(); + + Serial.println("πŸ†” UID Terdeteksi: " + uid); + + beep(150); + setRGB(1, 0, 1); + + lcd.clear(); + lcd.setCursor(0, 0); lcd.print(" MEMPROSES "); + lcd.setCursor(0, 1); lcd.print(" MOHON TUNGGU.. "); + + // Publikasikan data murni berdasarkan mode via MQTT saja (API Dihapus) + if (mode == "admin") { + client.publish("rfid/scan/admin", uid.c_str()); + } + else if (mode == "scan_member") { + client.publish("rfid/scan/member", uid.c_str()); + } + else { + // Default fallback jika standby atau mode lainnya ke topic umum + client.publish("rfid/scan/general", uid.c_str()); + } + + delay(500); + applyModeVisual(); + + rfid.PICC_HaltA(); + rfid.PCD_StopCrypto1(); +} + +void updateScrollText() { + if (millis() - lastScroll > 350) { + lastScroll = millis(); + lcd.setCursor(0, 1); + String displayMsg = scrollMessage.substring(scrollCursor, scrollCursor + 16); + if (displayMsg.length() < 16) { + displayMsg += scrollMessage.substring(0, 16 - displayMsg.length()); + } + lcd.print(displayMsg); + scrollCursor++; + if (scrollCursor >= scrollMessage.length()) scrollCursor = 0; + } +} + +/* ================= MAIN PROGRAM ================= */ +void setup() { + Serial.begin(115200); + pinMode(merah, OUTPUT); pinMode(hijau, OUTPUT); pinMode(biru, OUTPUT); + pinMode(BUZZER_PIN, OUTPUT); + + digitalWrite(BUZZER_PIN, LOW); + setRGB(1, 1, 1); + + Wire.begin(32, 33); + lcd.init(); lcd.backlight(); + lcd.setCursor(0, 0); lcd.print(" NAWASENA SMART "); + lcd.setCursor(0, 1); lcd.print(" BOOTING... "); + + mySerial.begin(9600, SERIAL_8N1, 25, 26); + if (dfplayer.begin(mySerial)) { + dfplayer.volume(30); + } + + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } + + client.setServer(mqtt_server, mqtt_port); + client.setCallback(callback); + + SPI.begin(18, 15, 19, 5); + rfid.PCD_Init(); + + delay(1000); + playTrack(1); + lcd.clear(); + applyModeVisual(); +} + +void loop() { + if (!client.connected()) { + reconnectMQTT(); + } + client.loop(); + + if (millis() - lastHeartbeat > 30000) { + lastHeartbeat = millis(); + if(client.connected()) { + client.publish("rfid/device/status", "online", true); + } + } + + if (isAudioPlaying && (millis() - audioStartTime >= AUDIO_DURATION)) { + isAudioPlaying = false; + } + + // Logika Tampilan LCD + if (mode != "identified" && mode != "wait") { + lcd.setCursor(0, 0); + if (mode == "scan_member") { + lcd.print(" TAP KARTU ANDA "); + lcd.setCursor(0, 1); + lcd.print(" ( ANGGOTA ) "); + } else if (mode == "admin") { + lcd.print(" MODE REGISTRASI"); + lcd.setCursor(0, 1); + lcd.print(" SCAN KARTU... "); + } else { + lcd.print(" NAWASENA READY "); + updateScrollText(); + } + } + + if (!isAudioPlaying) { + if (rfid.PICC_IsNewCardPresent()) { + if (rfid.PICC_ReadCardSerial()) { + handleRFID(); + } + } + } +} diff --git a/Set-Content b/Set-Content new file mode 100644 index 0000000..e69de29 diff --git a/app.zip b/app.zip new file mode 100644 index 0000000000000000000000000000000000000000..68c301e581a8179524f56ce042ef0ac44060e703 GIT binary patch literal 69829 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, false, 60); + $this->info("πŸš€ NAWASENA MQTT BACKBONE STARTED"); + + // Subscribe ke semua topik rfid + $mqtt->subscribe('rfid/#', function ($topic, $message) use ($mqtt) { + $this->info("πŸ“₯ Inbound [$topic]: $message"); + + // --- TAMBAHAN: LOGIKA STATUS HARDWARE --- + if ($topic === 'rfid/device/status') { + $this->handleDeviceStatus($message); + return; // Keluar dari callback agar tidak masuk ke pemrosesan JSON di bawah + } + + $data = json_decode($message, true); + if (!is_array($data)) { + $data = ['raw' => $message, 'uid' => $message, 'barcode' => $message]; + } + + // 1. ALUR SCAN BARCODE BUKU + if ($topic === 'rfid/book') { + $this->handleBookScan($mqtt, $data); + } + + // 2. ALUR SCAN KARTU ANGGOTA (RFID) + if ($topic === 'rfid/scan' || $topic === 'rfid/scan/member') { + $this->handleUserScan($mqtt, $data); + } + + // 3. ALUR PENDAFTARAN KARTU BARU (ADMIN MODE) + if ($topic === 'rfid/scan/admin') { + $this->handleAdminScan($mqtt, $data); + } + + // 4. EKSEKUSI FINAL (DARI TOMBOL UI ATAU HARDWARE) + if ($topic === 'rfid/confirm' && $message === 'execute') { + $this->executeFinalTransaction($mqtt); + } + + if ($topic === 'rfid/confirm/return' && $message === 'execute') { + $this->executeReturnTransaction($mqtt); + } + + // 5. BATALKAN TRANSAKSI (DARI TOMBOL UI) + if ($topic === 'rfid/cancel' && $message === 'reset') { + $this->handleCancelTransaction($mqtt); + } + + }, 0); + + $mqtt->loop(true); + + } catch (\Exception $e) { + $this->error("❌ MQTT ERROR: " . $e->getMessage()); + $this->info("Mencoba menyambung ulang dalam 5 detik..."); + sleep(5); + $this->handle(); + } + } + + /* ================= HANDLER STATUS DEVICE ================= */ + private function handleDeviceStatus($message) + { + if ($message === 'online') { + $this->info("🟒 DEVICE STATUS: Hardware is Online"); + } else { + $this->warn("πŸ”΄ DEVICE STATUS: Hardware is Offline"); + } + } + + /* ================= HANDLER BATALKAN TRANSAKSI ================= */ + private function handleCancelTransaction($mqtt) + { + DB::table('iot_modes')->where('id', 1)->update([ + 'mode' => 'standby', + 'user_id' => null, + 'book_id' => null + ]); + + $this->publish($mqtt, 'rfid/mode', 'standby'); + $this->publish($mqtt, 'rfid/status', 'idle'); + $this->info("πŸ”„ System Reset to Standby by User Request"); + } + + /* ================= HANDLER PENDAFTARAN ANGGOTA (BARU) ================= */ + private function handleAdminScan($mqtt, $data) + { + $uid = $this->cleanData($data['uid'] ?? $data['raw'] ?? ''); + if (!$uid) return; + + $iot = DB::table('iot_modes')->where('id', 1)->first(); + + if ($iot && $iot->mode === 'admin' && $iot->user_id) { + try { + DB::table('anggota')->where('id', $iot->user_id)->update([ + 'uid' => $uid, + 'uid_clean' => $uid, + 'status' => 'disetujui', + 'updated_at' => now() + ]); + + DB::table('iot_modes')->where('id', 1)->update([ + 'mode' => 'standby', + 'user_id' => null, + 'book_id' => null + ]); + + $this->publish($mqtt, 'rfid/status', 'registered'); + $this->publish($mqtt, 'rfid/mode', 'standby'); + + $this->publish($mqtt, 'rfid/ui/admin', [ + 'status' => 'registered', + 'uid' => $uid, + 'message' => 'Kartu Berhasil Ditautkan' + ]); + + $this->info("βœ… SUCCESS: UID $uid registered to Member ID {$iot->user_id}"); + } catch (\Exception $e) { + $this->error("❌ Admin Scan Failed: " . $e->getMessage()); + $this->publish($mqtt, 'rfid/status', 'error'); + } + } + } + + /* ================= HANDLER PEMINJAMAN / PENGEMBALIAN ================= */ + private function handleBookScan($mqtt, $data) + { + $barcode = $this->cleanData($data['barcode'] ?? $data['raw'] ?? ''); + if (!$barcode) return; + + $buku = DB::table('koleksis') + ->where('barcode', $barcode) + ->orWhere('kode_eksemplar', $barcode) + ->first(); + + if (!$buku) { + $this->publish($mqtt, 'rfid/status', 'error'); + $this->publish($mqtt, 'rfid/ui/detail', ['status' => 'tidak_ditemukan']); + return; + } + + $trx = DB::table('borrow_transactions') + ->where('koleksi_id', $buku->biblio_id) + ->where('status', 'dipinjam') + ->first(); + + if ($trx) { + $anggota = DB::table('anggota')->where('id', $trx->anggota_id)->first(); + IotMode::query()->where('id', 1)->update([ + 'mode' => 'return_confirm', + 'book_id' => $buku->biblio_id, + 'user_id' => $trx->anggota_id + ]); + + $this->publish($mqtt, 'rfid/ui/redirect_return', [ + 'status' => 'konfirmasi_kembali', + 'judul' => $buku->judul_koleksi ?? $buku->title, + 'nama' => $anggota->nama ?? 'Anggota', + 'barcode' => $barcode, + 'tanggal_pinjam' => $trx->tanggal_pinjam + ]); + } else { + IotMode::query()->where('id', 1)->update([ + 'mode' => 'wait', + 'book_id' => $buku->biblio_id, + 'user_id' => null + ]); + + $this->publish($mqtt, 'rfid/mode', 'wait'); + $this->publish($mqtt, 'rfid/ui/detail', [ + 'status' => 'ok', + 'judul' => $buku->judul_koleksi ?? $buku->title, + 'barcode' => $barcode + ]); + } + } + + private function handleUserScan($mqtt, $data) + { + $uid = $this->cleanData($data['uid'] ?? $data['raw'] ?? ''); + if (!$uid) return; + + $iot = DB::table('iot_modes')->where('id', 1)->first(); + + if ($iot && $iot->mode === 'admin') { + $this->info("πŸ”„ Redirecting scan to Admin Handler..."); + $this->handleAdminScan($mqtt, $data); + return; + } + + if (!$iot || !in_array($iot->mode, ['wait', 'user', 'identified'])) { + $this->warn("⚠️ Scan ignored: Mode is {$iot->mode}"); + return; + } + + if ($iot->book_id) { + $anggota = DB::table('anggota')->where('uid', $uid)->orWhere('uid_clean', $uid)->first(); + + if ($anggota) { + DB::table('iot_modes')->where('id', 1)->update([ + 'mode' => 'identified', + 'user_id' => $anggota->id + ]); + + $this->publish($mqtt, 'rfid/status', 'valid'); + $this->publish($mqtt, 'rfid/ui/member', [ + 'status' => 'valid', + 'nama' => $anggota->nama, + 'uid' => $uid + ]); + } else { + + // Mode tetap wait, tidak perlu diubah ke failed + $this->publish($mqtt, 'rfid/status', 'failed'); + + $this->publish($mqtt, 'rfid/ui/member', [ + 'status' => 'invalid' + ]); + + $this->info("❌ Invalid Card: $uid"); + } + } + } + + private function executeReturnTransaction($mqtt) + { + $iot = DB::table('iot_modes')->where('id', 1)->first(); + + if ($iot && $iot->mode === 'return_confirm' && $iot->book_id) { + try { + // Ambil data buku untuk keperluan pesan UI web user + $book = DB::table('koleksis')->where('biblio_id', $iot->book_id)->first(); + $judulBuku = $book ? ($book->judul_koleksi ?? $book->title) : 'Buku'; + $targetUserId = $iot->user_id; + + $affected = DB::table('borrow_transactions') + ->where('koleksi_id', $iot->book_id) + ->where('status', 'dipinjam') + ->update([ + 'status' => 'kembali', + 'updated_at' => now() + ]); + + if ($affected) { + DB::table('iot_modes')->where('id', 1)->update(['mode' => 'success_return']); + $this->publish($mqtt, 'rfid/status', 'finish'); + $this->info("βœ… SUCCESS: Book returned"); + + // πŸ”₯ BROADCAST AUTO-REFRESH USER WEB (PENGEMBALIAN SUKSES) + if ($targetUserId) { + $this->publish($mqtt, 'peminjaman/user/' . $targetUserId, [ + 'status' => 'dikembalikan', + 'message' => "Pengembalian Sukses! Buku [{$judulBuku}] telah terdata kembali." + ]); + } + } + } catch (\Exception $e) { + $this->error("❌ Return Update Failed: " . $e->getMessage()); + $this->publish($mqtt, 'rfid/status', 'error'); + } + } + } + + private function executeFinalTransaction($mqtt) + { + $iot = DB::table('iot_modes')->where('id', 1)->first(); + + if ($iot && $iot->mode === 'identified' && $iot->book_id && $iot->user_id) { + try { + // Ambil data buku untuk keperluan pesan UI web user + $book = DB::table('koleksis')->where('biblio_id', $iot->book_id)->first(); + $judulBuku = $book ? ($book->judul_koleksi ?? $book->title) : 'Buku'; + + DB::table('borrow_transactions')->insert([ + 'anggota_id' => $iot->user_id, + 'koleksi_id' => $iot->book_id, + 'tanggal_pinjam' => now(), + 'tanggal_kembali' => now()->addDays(7), + 'status' => 'dipinjam', + 'created_at' => now(), + 'updated_at' => now() + ]); + + DB::table('iot_modes')->where('id', 1)->update(['mode' => 'success_loan']); + $this->publish($mqtt, 'rfid/status', 'finish'); + $this->info("βœ… SUCCESS: Loan completed"); + + // πŸ”₯ BROADCAST AUTO-REFRESH USER WEB (PEMINJAMAN SUKSES) + $this->publish($mqtt, 'peminjaman/user/' . $iot->user_id, [ + 'status' => 'dipinjam', + 'message' => "Peminjaman Sukses! Buku [{$judulBuku}] masuk ke riwayat akun Anda." + ]); + + } catch (\Exception $e) { + $this->error("❌ DB Insert Failed: " . $e->getMessage()); + $this->publish($mqtt, 'rfid/status', 'error'); + } + } + } + + private function publish($mqtt, $topic, $data) { + $payload = is_array($data) ? json_encode($data) : (string)$data; + $mqtt->publish($topic, $payload, 0); + } + + private function cleanData($data) { + return strtoupper(str_replace(' ', '', trim($data))); + } +} + diff --git a/app/Console/Commands/SetupLanguages.php b/app/Console/Commands/SetupLanguages.php index ee97b99..b0cbe6e 100644 --- a/app/Console/Commands/SetupLanguages.php +++ b/app/Console/Commands/SetupLanguages.php @@ -53,3 +53,4 @@ public function handle() $this->info('βœ… File bahasa "id.json" dan "en.json" berhasil dibuat dan diisi.'); } } + diff --git a/app/Events/RfidScanned.php b/app/Events/RfidScanned.php new file mode 100644 index 0000000..19b7567 --- /dev/null +++ b/app/Events/RfidScanned.php @@ -0,0 +1,27 @@ +data = $data; + } + + public function broadcastOn() + { + return new Channel('rfid-channel'); + } + +} +{ + \Log::info('EVENT DIKIRIM', $this->data); + return new Channel('rfid-channel'); +} + diff --git a/app/Filament/Resources/BookResource.php b/app/Filament/Resources/BookResource.php index 9c8b58d..7af16d8 100644 --- a/app/Filament/Resources/BookResource.php +++ b/app/Filament/Resources/BookResource.php @@ -93,3 +93,4 @@ public static function getPages(): array ]; } } + diff --git a/app/Filament/Resources/BookResource/Pages/CreateBook.php b/app/Filament/Resources/BookResource/Pages/CreateBook.php index 228b9fe..3ebe2f2 100644 --- a/app/Filament/Resources/BookResource/Pages/CreateBook.php +++ b/app/Filament/Resources/BookResource/Pages/CreateBook.php @@ -10,3 +10,4 @@ class CreateBook extends CreateRecord { protected static string $resource = BookResource::class; } + diff --git a/app/Filament/Resources/BookResource/Pages/EditBook.php b/app/Filament/Resources/BookResource/Pages/EditBook.php index a0fb317..17cf905 100644 --- a/app/Filament/Resources/BookResource/Pages/EditBook.php +++ b/app/Filament/Resources/BookResource/Pages/EditBook.php @@ -17,3 +17,4 @@ protected function getHeaderActions(): array ]; } } + diff --git a/app/Filament/Resources/BookResource/Pages/ListBooks.php b/app/Filament/Resources/BookResource/Pages/ListBooks.php index 786a983..dcf78b3 100644 --- a/app/Filament/Resources/BookResource/Pages/ListBooks.php +++ b/app/Filament/Resources/BookResource/Pages/ListBooks.php @@ -17,3 +17,4 @@ protected function getHeaderActions(): array ]; } } + diff --git a/app/Filament/Resources/MemberResource.php b/app/Filament/Resources/MemberResource.php index 2c56a3e..8addc7a 100644 --- a/app/Filament/Resources/MemberResource.php +++ b/app/Filament/Resources/MemberResource.php @@ -62,3 +62,4 @@ public static function getPages(): array ]; } } + diff --git a/app/Filament/Resources/MemberResource/Pages/CreateMember.php b/app/Filament/Resources/MemberResource/Pages/CreateMember.php index 258fe60..da5f36c 100644 --- a/app/Filament/Resources/MemberResource/Pages/CreateMember.php +++ b/app/Filament/Resources/MemberResource/Pages/CreateMember.php @@ -10,3 +10,4 @@ class CreateMember extends CreateRecord { protected static string $resource = MemberResource::class; } + diff --git a/app/Filament/Resources/MemberResource/Pages/EditMember.php b/app/Filament/Resources/MemberResource/Pages/EditMember.php index 0be6367..77c508b 100644 --- a/app/Filament/Resources/MemberResource/Pages/EditMember.php +++ b/app/Filament/Resources/MemberResource/Pages/EditMember.php @@ -17,3 +17,4 @@ protected function getHeaderActions(): array ]; } } + diff --git a/app/Filament/Resources/MemberResource/Pages/ListMembers.php b/app/Filament/Resources/MemberResource/Pages/ListMembers.php index b177e4e..4e0b782 100644 --- a/app/Filament/Resources/MemberResource/Pages/ListMembers.php +++ b/app/Filament/Resources/MemberResource/Pages/ListMembers.php @@ -17,3 +17,4 @@ protected function getHeaderActions(): array ]; } } + diff --git a/app/Http/Controllers/Admin/AnggotaController.php b/app/Http/Controllers/Admin/AnggotaController.php index fb935d8..2aa6504 100644 --- a/app/Http/Controllers/Admin/AnggotaController.php +++ b/app/Http/Controllers/Admin/AnggotaController.php @@ -6,92 +6,191 @@ use Illuminate\Http\Request; use App\Models\Anggota; use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\DB; use SimpleSoftwareIO\QrCode\Facades\QrCode; use App\Mail\StatusAnggotaDiperbarui; +use PhpMqtt\Client\MqttClient; +use Barryvdh\DomPDF\Facade\Pdf; class AnggotaController extends Controller { - + /* ================= MQTT HELPER ================= */ + private function mqttPublish($topic, $message) + { + try { + // Menggunakan broker lokal (127.0.0.1) bawaan Laragon / Mosquitto + $mqtt = new MqttClient('127.0.0.1', 1883, 'laravel-admin-' . uniqid()); + $mqtt->connect(); + + $payload = is_array($message) ? json_encode($message) : (string)$message; + $mqtt->publish($topic, $payload, 0); + + $mqtt->disconnect(); + } catch (\Exception $e) { + \Log::error("MQTT Error: " . $e->getMessage()); + } + } + + /* ================= MANAJEMEN ANGGOTA ================= */ + public function index(Request $request) { - $query = $request->input('search'); - $status = $request->input('status'); + // Ambil input dengan nama variabel yang jelas agar tidak bentrok dengan scope Eloquent ($query) + $searchKeyword = $request->input('search'); + $statusFilter = $request->input('status'); - $members = Anggota::when($query, function ($q) use ($query) { - $q->where('nama', 'like', "%{$query}%") - ->orWhere('email', 'like', "%{$query}%"); - }) - ->when($status, function ($q) use ($status) { - $q->where('status', $status); - }) - ->orderByDesc('created_at') - ->paginate(10) - ->withQueryString(); + // Bangun query dasar + $query = Anggota::query(); + + // Kondisi jika ada pencarian (Bungkus dalam sub-query OR agar tidak merusak filter status) + if (!empty($searchKeyword)) { + $query->where(function ($q) use ($searchKeyword) { + $q->where('nama', 'like', "%{$searchKeyword}%") + ->orWhere('email', 'like', "%{$searchKeyword}%") + ->orWhere('nip_nim', 'like', "%{$searchKeyword}%") // Sinkronisasi pencarian NIP/NIM + ->orWhere('rfid_uid', 'like', "%{$searchKeyword}%"); // Sinkronisasi pencarian RFID UID (dari database: rfid_uid) + }); + } + + // Kondisi jika ada filter status + if (!empty($statusFilter)) { + $query->where('status', $statusFilter); + } + + // Eksekusi pagination dengan aman + $members = $query->orderByDesc('created_at')->paginate(10)->withQueryString(); + + // PROTEKSI EXTRA: Jika hasil kosong, pastikan tetap mengirim object kosong yang aman, bukan null + if (!$members) { + $members = new \Illuminate\Pagination\LengthAwarePaginator([], 0, 10); + } return view('admin.anggota.index', compact('members')); } - public function create() { return view('admin.anggota.create'); } - public function store(Request $request) { + // 🌟 SINKRONISASI: Menambahkan nip_nim dan gender ke formulir tambah manual oleh admin $validatedData = $request->validate([ - 'nama' => 'required|string|max:100', - 'email' => 'required|email|unique:anggota,email', - 'alamat' => 'required|string|max:255', - 'no_hp' => 'nullable|string|max:20', + 'nama' => 'required|string|max:100', + 'gender' => 'required|in:Laki-laki,Perempuan', + 'nip_nim' => 'required|string|max:255|unique:anggota,nip_nim', + 'email' => 'required|email|unique:anggota,email', + 'alamat' => 'required|string|max:255', + 'no_hp' => 'nullable|string|max:20', ]); - $validatedData['status'] = 'disetujui'; + $validatedData['status'] = 'disetujui'; + $member = Anggota::create($validatedData); - Anggota::create($validatedData); + // MQTT: Memicu reload halaman index admin karena ada entri data fisik baru + $this->mqttPublish('pendaftaran/refresh/admin', [ + 'action' => 'created', + 'message' => 'Anggota baru telah ditambahkan oleh Admin' + ]); return redirect()->route('admin.anggota.index') ->with('success', 'Anggota baru berhasil didaftarkan dan disetujui!'); } - public function edit($id) { $member = Anggota::findOrFail($id); return view('admin.anggota.edit', compact('member')); } - - public function update(Request $request, $id) - { - $member = Anggota::findOrFail($id); + public function update(Request $request, $id) +{ + $anggota = Anggota::findOrFail($id); - $validatedData = $request->validate([ - 'nama' => 'required|string|max:100', - 'email' => 'required|email|unique:anggota,email,' . $member->id, - 'alamat' => 'required|string|max:255', - 'no_hp' => 'nullable|string|max:20', - 'status' => 'required|in:menunggu,disetujui,ditolak', - ]); + // 1. Validasi Input Form + $validatedData = $request->validate([ + 'nama' => 'required|string|max:100', + 'gender' => 'required|in:Laki-laki,Perempuan', + 'nik_nip' => 'required|string|max:255|unique:anggota,nik_nip,' . $anggota->id, + 'email' => 'required|email|unique:anggota,email,' . $anggota->id, + 'no_hp' => 'nullable|string|max:20', + 'alamat' => 'required|string|max:255', // Alamat lengkap gabungan dari JS Blade + 'provinsi' => 'nullable|string', + 'kota' => 'nullable|string', + 'kecamatan' => 'nullable|string', + 'desa' => 'nullable|string', + ]); - $member->update($validatedData); + try { + // 2. Simpan Perubahan Utama + $anggota->nama = $validatedData['nama']; + $anggota->gender = $validatedData['gender']; + $anggota->nik_nip = $validatedData['nik_nip']; + $anggota->email = $validatedData['email']; + $anggota->no_hp = $validatedData['no_hp']; + $anggota->alamat = $validatedData['alamat']; // 🌟 Ini sudah mencakup data wilayah lengkap + // 🌟 FIXED: 4 baris pengisian wilayah dihapus karena kolomnya tidak ada di tabel database Anda + // Data wilayah Anda sudah aman dan rapi di dalam kolom $anggota->alamat + + $anggota->save(); + + // 3. MQTT Terintegrasi + try { + if (method_exists($this, 'mqttPublish')) { + $this->mqttPublish('pendaftaran/refresh/admin', [ + 'action' => 'updated', + 'member_id' => $anggota->id + ]); + + if ($anggota->user_id) { + $this->mqttPublish('pendaftaran/status/' . $anggota->user_id, [ + 'status' => $anggota->status, + 'message' => 'Data pendaftaran Anda telah diperbarui oleh Admin menjadi: ' . $anggota->status + ]); + } + } + } catch (\Exception $mqttError) { + \Log::warning('MQTT Gagal: ' . $mqttError->getMessage()); + } + + // 4. Kembali Ke Index Sembari Membawa Toast/Alert Sukses return redirect()->route('admin.anggota.index') - ->with('success', 'Data anggota berhasil diperbarui!'); - } + ->with('success', 'Data anggota berhasil diperbarui! πŸŽ‰'); + + } catch (\Exception $e) { + return redirect()->back() + ->withInput() + ->withErrors(['error_server' => 'Terjadi kesalahan pada server: ' . $e->getMessage()]); + } +} + - public function destroy($id) { $member = Anggota::findOrFail($id); + $savedUserId = $member->user_id; + $member->delete(); + // MQTT: Sinyal refresh untuk admin karena ada baris data yang hilang + $this->mqttPublish('pendaftaran/refresh/admin', [ + 'action' => 'deleted' + ]); + + // MQTT: Paksa pengguna logout atau refresh jika akunnya dihapus oleh admin + if ($savedUserId) { + $this->mqttPublish('pendaftaran/status/' . $savedUserId, [ + 'status' => 'deleted', + 'message' => 'Data pendaftaran Anda telah dihapus oleh pihak admin.' + ]); + } + return redirect()->route('admin.anggota.index') ->with('success', 'Anggota berhasil dihapus!'); } - public function cetak($id) { $member = Anggota::findOrFail($id); @@ -100,15 +199,58 @@ public function cetak($id) return view('admin.anggota.cetak', compact('member', 'qrCode')); } - + /* ================= INTEGRASI IOT & STATE FLOW ================= */ + public function approve($id) { $member = Anggota::findOrFail($id); + + // 1. Cek kesiapan IoT (Jangan timpa jika sedang ada transaksi berjalan) + $current = DB::table('iot_modes')->where('id', 1)->first(); + if ($current && $current->mode !== 'standby') { + return back()->with('warning', 'Sistem sedang sibuk. Pastikan alat dalam posisi standby.'); + } + + // 2. Update status anggota di database menjadi disetujui $member->update(['status' => 'disetujui']); - Mail::to($member->email)->send(new StatusAnggotaDiperbarui($member)); + // 3. Kunci State IoT ke mode 'admin' untuk User ID target + DB::table('iot_modes')->where('id', 1)->update([ + 'mode' => 'admin', + 'user_id' => $member->id, + 'updated_at' => now() + ]); - return back()->with('success', "Anggota {$member->nama} telah disetujui."); + // 4. Perintah utama ke perangkat keras ESP32 + $this->mqttPublish('rfid/mode/set', 'admin'); + $this->mqttPublish('rfid/admin/request_scan', [ + 'status' => 'waiting_scan', + 'nama' => $member->nama, + 'id' => $member->id + ]); + + // 5. SINKRONISASI STATE ASINKRON: Kirim aksi 'approved' ke JavaScript panel admin. + $this->mqttPublish('pendaftaran/refresh/admin', [ + 'action' => 'approved', + 'member_id' => $member->id + ]); + + // 6. Notifikasi real-time ke aplikasi/halaman milik user + if ($member->user_id) { + $this->mqttPublish('pendaftaran/status/' . $member->user_id, [ + 'status' => 'disetujui', + 'message' => "Pendaftaran Anda disetujui! Alat RFID sekarang menunggu scan kartu Anda." + ]); + } + + // 7. Pengiriman Notifikasi via Email + try { + Mail::to($member->email)->send(new StatusAnggotaDiperbarui($member)); + } catch (\Exception $e) { + \Log::error("Email Error: " . $e->getMessage()); + } + + return back()->with('success', "Anggota {$member->nama} disetujui. Perangkat RFID Nawasena siap menerima pemindaian kartu."); } public function reject($id) @@ -116,8 +258,77 @@ public function reject($id) $member = Anggota::findOrFail($id); $member->update(['status' => 'ditolak']); - Mail::to($member->email)->send(new StatusAnggotaDiperbarui($member)); + // MQTT: Beritahu halaman admin untuk memperbarui status penolakan + $this->mqttPublish('pendaftaran/refresh/admin', [ + 'action' => 'rejected', + 'member_id' => $member->id + ]); + + // 🌟 SINKRONISASI: Sinyal 'ditolak' dikirim ke user agar di view user langsung muncul tombol pendaftaran ulang (sinkron dengan UserMemberController) + if ($member->user_id) { + $this->mqttPublish('pendaftaran/status/' . $member->user_id, [ + 'status' => 'ditolak', + 'message' => "Pendaftaran Anda ditolak oleh admin. Silakan periksa kembali data Anda dan lakukan daftar ulang jika diperlukan." + ]); + } + + try { + Mail::to($member->email)->send(new StatusAnggotaDiperbarui($member)); + } catch (\Exception $e) { + \Log::error("Email Error: " . $e->getMessage()); + } return back()->with('warning', "Anggota {$member->nama} telah ditolak."); } -} + + public function receiveRfidUid(Request $request) + { + $uid = $request->input('uid'); + $anggotaId = $request->input('anggota_id'); + + $member = Anggota::find($anggotaId); + if ($member) { + // 1. Simpan string UID fisik kartu ke data anggota + $member->update(['rfid_uid' => $uid]); + + // 2. Kembalikan state mesin IoT ke mode 'standby' agar siap digunakan kembali + DB::table('iot_modes')->where('id', 1)->update([ + 'mode' => 'standby', + 'user_id' => null, + 'updated_at' => now() + ]); + + $this->mqttPublish('pendaftaran/refresh/admin', [ + 'action' => 'uid_saved', + 'member_id' => $member->id, + 'rfid_uid' => $uid + ]); + + // 4. Perintahkan sistem user untuk mengalihkan view ke halaman dashboard utama library + if ($member->user_id) { + $this->mqttPublish('pendaftaran/status/' . $member->user_id, [ + 'status' => 'uid_ready', + 'message' => 'Kartu RFID Anda berhasil diregistrasikan!' + ]); + } + + return response()->json(['status' => 'success', 'message' => 'UID Berhasil disimpan dan disinkronkan ke Web-Admin via MQTT.']); + } + + return response()->json(['status' => 'error', 'message' => 'Data Anggota tidak ditemukan.'], 404); + } + + public function print($id) + { + $member = Anggota::findOrFail($id); + + // Load view kartu yang sudah rapi + $pdf = Pdf::loadView('admin.anggota.print', compact('member')); + + // Atur ukuran kertas kustom atau default (misal A4 atau ukuran kartu) + $pdf->setPaper('a4', 'portrait'); + + // Menggunakan STREAM agar PDF terbuka langsung di PDF Viewer browser (persis seperti contoh gambar) + return $pdf->stream('Kartu Anggota ' . $member->nama . '.pdf'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index dfbf7ac..a1478ee 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -15,25 +15,42 @@ class DashboardController extends Controller { public function index() { - - + // 1. Menghitung Data Dasar Koleksi $jumlahJudul = Koleksi::distinct('title')->count('title'); - $jumlahEksemplar = Koleksi::whereNotNull('kode_eksemplar')->count(); - $jumlahUser = User::count(); $totalBuku = $jumlahJudul; - $totalAnggota = $jumlahUser; + $totalAnggota = Anggota::count(); + // 2. Statistik Anggota & Sirkulasi $anggotaBulanIni = Anggota::whereMonth('created_at', date('m'))->count(); - + // Menghitung buku yang sedang aktif dipinjam $totalPeminjaman = BorrowTransaction::where('status', 'dipinjam')->count(); - $totalPengembalian = BorrowTransaction::where('status', 'dikembalikan')->count(); + /** + * UPDATE: Menghitung total pengembalian. + * Menggunakan status 'kembali' atau 'dikembalikan' agar sinkron dengan database + */ + $totalPengembalian = BorrowTransaction::whereIn('status', ['kembali', 'dikembalikan'])->count(); + // 3. Menghitung Notifikasi (PENGECEKAN KETAT) + $pendingMembers = Anggota::whereIn('status', ['pending', 'menunggu'])->count(); + /** + * UPDATE: Notifikasi Peminjaman/Pengembalian Baru + * Menghitung transaksi yang statusnya 'menunggu' (pinjam baru), + * 'pengembalian_menunggu' (user lapor sudah mengembalikan), + * dan 'perpanjangan_menunggu' (user minta perpanjang). + */ + $pendingLoans = BorrowTransaction::whereIn('status', [ + 'menunggu', + 'pengembalian_menunggu', + 'perpanjangan_menunggu' + ])->count(); + + // 4. Data Chart (Sirkulasi Peminjaman Bulanan) $chartData = BorrowTransaction::selectRaw('MONTH(tanggal_pinjam) as bulan, COUNT(*) as total') ->whereYear('tanggal_pinjam', date('Y')) ->groupBy('bulan') @@ -48,17 +65,13 @@ public function index() $bulanLabel = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']; - - $ratings = Rating::with('user', 'transaction.koleksi')->orderBy('created_at', 'desc')->get(); + // 5. Rating & Feedback + $ratings = Rating::with(['user', 'transaction.koleksi']) + ->orderBy('created_at', 'desc') + ->take(10) + ->get(); - $ratingCounts = []; - $ratingComments = []; - for ($i = 1; $i <= 5; $i++) { - $ratingCounts[$i] = $ratings->where('rating', $i)->count(); - $ratingComments[$i] = $ratings->where('rating', $i)->pluck('feedback')->implode(', '); - } - - + // 6. Buku Terpopuler $mostBorrowedBook = BorrowTransaction::select('koleksi_id') ->selectRaw('COUNT(*) as total') ->groupBy('koleksi_id') @@ -66,7 +79,7 @@ public function index() ->with('koleksi') ->first(); - + // 7. Return ke View return view('admin.dashboard', compact( 'jumlahJudul', 'jumlahEksemplar', @@ -76,12 +89,53 @@ public function index() 'anggotaBulanIni', 'totalPeminjaman', 'totalPengembalian', + 'pendingMembers', + 'pendingLoans', 'chartDataFull', 'bulanLabel', 'ratings', - 'ratingCounts', - 'ratingComments', 'mostBorrowedBook' )); } + + // TURUNKAN ADMIN MENJADI USER + public function turunkanAdmin($id) + { + $auth = Auth::user(); + + if (!$auth || $auth->role !== 'super_admin') { + abort(403, 'Akses ditolak!'); + } + + $user = User::findOrFail($id); + + $user->role = 'user'; + $user->save(); + + return back()->with( + 'success', + 'Admin berhasil dijadikan user.' + ); + } + + // BLOKIR USER + public function blokirUser($id) + { + $auth = Auth::user(); + + if (!$auth || $auth->role !== 'super_admin') { + abort(403, 'Akses ditolak!'); + } + + $user = User::findOrFail($id); + + $user->blocked_at = now(); + $user->save(); + + return back()->with( + 'success', + 'User berhasil diblokir.' + ); + } } + diff --git a/app/Http/Controllers/Admin/KoleksiController.php b/app/Http/Controllers/Admin/KoleksiController.php index 3aebbac..796c3e1 100644 --- a/app/Http/Controllers/Admin/KoleksiController.php +++ b/app/Http/Controllers/Admin/KoleksiController.php @@ -11,7 +11,6 @@ use Illuminate\Support\Facades\Storage; use Barryvdh\DomPDF\Facade\Pdf; use Milon\Barcode\DNS1D; - class KoleksiController extends Controller { public function __construct() @@ -19,7 +18,7 @@ public function __construct() } - public function index(Request $request) + public function index(Request $request) { $query = Koleksi::query(); @@ -40,7 +39,7 @@ public function index(Request $request) return view('admin.koleksi.index', compact('results')); } - public function create() + public function create() { $publishers = Publisher::all(); $publishPlaces = PublishPlace::all(); @@ -48,7 +47,7 @@ public function create() return view('admin.koleksi.create', compact('publishers', 'publishPlaces')); } - public function store(Request $request) + public function store(Request $request) { $request->validate([ 'title' => 'required|string|max:255', @@ -79,6 +78,10 @@ public function store(Request $request) 'publisher_name' => $request->publisher_name, 'publish_place_name' => $request->publish_place_name, 'publish_year' => $request->publish_year, + + // πŸ”₯ FIX WAJIB + 'tahun_terbit' => $request->publish_year, + 'isbn_issn' => $request->isbn_issn, 'edition' => $request->edition, 'classification' => $request->classification, @@ -98,7 +101,7 @@ public function store(Request $request) ->with('success', 'βœ… Koleksi berhasil ditambahkan dengan Barcode.'); } - protected function generateKodeEksemplar() + protected function generateKodeEksemplar() { do { $random = str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); @@ -108,7 +111,7 @@ protected function generateKodeEksemplar() return $kode; } - protected function generateBarcode($kodeEksemplar) + protected function generateBarcode($kodeEksemplar) { $barcode = new DNS1D(); $barcode->setStorPath(storage_path('app/public/barcodes')); @@ -125,7 +128,7 @@ protected function generateBarcode($kodeEksemplar) return $barcodePath; } - public function printBarcodeSingle($kode_eksemplar) + public function printBarcodeSingle($kode_eksemplar) { $koleksi = Koleksi::where('kode_eksemplar', $kode_eksemplar)->firstOrFail(); $setting = Setting::first(); @@ -136,7 +139,7 @@ public function printBarcodeSingle($kode_eksemplar) return $pdf->stream("barcode_{$koleksi->kode_eksemplar}.pdf"); } - public function printBarcodeMultiple(Request $request) + public function printBarcodeMultiple(Request $request) { $selected = $request->input('selected', []); @@ -157,7 +160,6 @@ public function printBarcodeMultiple(Request $request) return $pdf->stream("barcode_massal.pdf"); } - public function edit($kode_eksemplar) { $koleksi = Koleksi::where('kode_eksemplar', $kode_eksemplar)->firstOrFail(); @@ -167,7 +169,6 @@ public function edit($kode_eksemplar) return view('admin.koleksi.edit', compact('koleksi', 'publishers', 'publishPlaces')); } - public function update(Request $request, $kode_eksemplar) { $koleksi = Koleksi::where('kode_eksemplar', $kode_eksemplar)->firstOrFail(); @@ -200,6 +201,10 @@ public function update(Request $request, $kode_eksemplar) 'publisher_name' => $request->publisher_name, 'publish_place_name' => $request->publish_place_name, 'publish_year' => $request->publish_year, + + // πŸ”₯ FIX WAJIB + 'tahun_terbit' => $request->publish_year, + 'isbn_issn' => $request->isbn_issn, 'edition' => $request->edition, 'classification' => $request->classification, @@ -214,21 +219,98 @@ public function update(Request $request, $kode_eksemplar) ->with('success', 'βœ… Data koleksi berhasil diperbarui.'); } - - public function destroy($kode_eksemplar) - { - $koleksi = Koleksi::where('kode_eksemplar', $kode_eksemplar)->firstOrFail(); + public function destroy($kode_eksemplar) +{ + $koleksi = Koleksi::where('kode_eksemplar', $kode_eksemplar)->firstOrFail(); - if ($koleksi->image) { + if ($koleksi->image) { + // Cek apakah ada eksemplar LAIN yang masih menggunakan gambar ini + $fotoMasihDipakai = Koleksi::where('image', $koleksi->image) + ->where('kode_eksemplar', '!=', $kode_eksemplar) + ->exists(); + + // Jika tidak ada eksemplar lain yang pakai, baru hapus file fisiknya + if (!$fotoMasihDipakai) { Storage::disk('public')->delete($koleksi->image); } + } + + // Barcode tetap langsung dihapus karena setiap eksemplar punya barcode unik + if ($koleksi->barcode) { + Storage::disk('public')->delete($koleksi->barcode); + } + + $koleksi->delete(); + + return redirect()->route('admin.koleksi.index') + ->with('success', 'βœ… Koleksi berhasil dihapus.'); +} + // ... (di dalam KoleksiController) + +public function printMultipleBarcode(Request $request) +{ + // 1. Validasi input + if (!$request->has('kode_eksemplar') || !is_array($request->kode_eksemplar)) { + return redirect()->back()->with('error', 'Silakan centang minimal satu koleksi terlebih dahulu.'); + } + + $kodeEksemplarArray = $request->kode_eksemplar; + + // --- BAGIAN INI DIPERBAIKI --- + // Mengurutkan data agar sama persis dengan urutan centang (array input) + $orderList = "'" . implode("','", $kodeEksemplarArray) . "'"; + + $items = Koleksi::whereIn('kode_eksemplar', $kodeEksemplarArray) + ->orderByRaw("FIELD(kode_eksemplar, $orderList)") + ->get(); + // ----------------------------- + + if ($items->isEmpty()) { + return redirect()->back()->with('error', 'Data koleksi yang Anda pilih tidak ditemukan.'); + } + + $setting = Setting::first(); + + // 2. Render PDF + $pdf = Pdf::loadView('admin.koleksi.barcode_multiple', compact('items', 'setting')); + + // 3. Atur kertas + $pdf->setPaper('a4', 'portrait'); + + // 4. Stream PDF + return $pdf->stream('Label_Barcode_Massal.pdf'); +} +public function destroyMultiple(Request $request) +{ + $kodese = $request->input('kode_eksemplar', []); + + if (empty($kodese)) { + return redirect()->back()->with('error', 'Tidak ada item yang dipilih.'); + } + + // Ambil semua data koleksi yang kodenya sesuai array + $koleksis = Koleksi::whereIn('kode_eksemplar', $kodese)->get(); + + foreach ($koleksis as $koleksi) { + // Contoh logika hapus file cover jika tidak dipakai eksemplar lain + if ($koleksi->image) { + $fotoMasihDipakai = Koleksi::where('image', $koleksi->image) + ->whereNotIn('kode_eksemplar', $kodese) + ->exists(); + if (!$fotoMasihDipakai) { + Storage::disk('public')->delete($koleksi->image); + } + } + + // Hapus file barcode asli jika disimpan fisik if ($koleksi->barcode) { Storage::disk('public')->delete($koleksi->barcode); } $koleksi->delete(); - - return redirect()->route('admin.koleksi.index') - ->with('success', 'βœ… Koleksi berhasil dihapus.'); } + + return redirect()->route('admin.koleksi.index')->with('success', count($kodese) . ' data koleksi berhasil dihapus sekaligus.'); } +} + diff --git a/app/Http/Controllers/Admin/PeminjamanController.php b/app/Http/Controllers/Admin/PeminjamanController.php index 2c2d01b..46d238d 100644 --- a/app/Http/Controllers/Admin/PeminjamanController.php +++ b/app/Http/Controllers/Admin/PeminjamanController.php @@ -7,36 +7,42 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; // Tambahkan ini use App\Mail\BukuSiapDiambilMail; use App\Mail\PeminjamanDitolakMail; use App\Mail\BukuDikembalikanMail; use App\Mail\PerpanjanganDisetujuiMail; use App\Mail\PerpanjanganDitolakMail; use Carbon\Carbon; +use App\Models\IotMode; class PeminjamanController extends Controller { - public function index() { - $transactions = BorrowTransaction::with(['anggota', 'koleksi']) + // Gunakan eager loading 'anggota.user' untuk mendapatkan email dari tabel users + $transactions = BorrowTransaction::with(['anggota.user', 'koleksi']) ->orderBy('created_at', 'desc') ->get(); return view('admin.peminjaman', compact('transactions')); } - + public function cetakNota($id) + { + $transaction = BorrowTransaction::with(['anggota', 'koleksi'])->findOrFail($id); + return view('admin.peminjaman.nota', compact('transaction')); + } + public function downloadSuratPengantar($id) { $transaction = BorrowTransaction::findOrFail($id); - if (!$transaction->surat_pengantar) { return back()->with('error', 'File surat pengantar tidak tersedia.'); } $filePath = 'public/surat_pengantar/' . $transaction->surat_pengantar; - if (!Storage::exists($filePath)) { return back()->with('error', 'File surat pengantar tidak ditemukan.'); } @@ -44,41 +50,26 @@ public function downloadSuratPengantar($id) return Storage::download($filePath, $transaction->surat_pengantar); } - - public function kembalikan($id) - { - $transaction = BorrowTransaction::findOrFail($id); - - if (!in_array($transaction->status, ['dipinjam', 'pengembalian_menunggu'])) { - return redirect()->route('admin.peminjaman') - ->with('error', 'Transaksi ini belum dipinjam atau sudah dikembalikan.'); - } - - $transaction->markAsDikembalikan(); - - if ($transaction->anggota && $transaction->anggota->email) { - try { - Mail::to($transaction->anggota->email)->send(new BukuDikembalikanMail($transaction)); - } catch (\Exception $e) { - } - } - - return redirect()->route('admin.peminjaman') - ->with('success', 'Buku berhasil ditandai sebagai dikembalikan.'); - } - - public function setujui(Request $request, $id) { - $transaction = BorrowTransaction::findOrFail($id); + $transaction = BorrowTransaction::with('anggota.user')->findOrFail($id); if (!in_array($transaction->status, ['menunggu', 'pending'])) { return redirect()->back()->with('error', 'Transaksi ini sudah diproses.'); } + // πŸ”₯ CEK LIMIT PINJAMAN + $jumlahPinjaman = BorrowTransaction::where('anggota_id', $transaction->anggota_id) + ->whereIn('status', ['dipinjam', 'disetujui', 'pengembalian_menunggu']) + ->count(); + + if ($jumlahPinjaman >= 2) { + return redirect()->back()->with('error', 'Anggota sudah mencapai batas maksimal peminjaman (2 buku).'); + } + $request->validate([ 'pickup_date' => 'required|date', - 'pickup_time' => 'required|date_format:H:i', + 'pickup_time' => 'required', 'tanggal_kembali' => 'nullable|date|after_or_equal:pickup_date', ]); @@ -86,44 +77,56 @@ public function setujui(Request $request, $id) ? Carbon::parse($request->tanggal_kembali) : Carbon::parse($request->pickup_date)->addDays(7); - $transaction->pickup_date = $request->pickup_date; - $transaction->pickup_time = $request->pickup_time; - $transaction->tanggal_kembali = $tanggalKembali; - $transaction->status = 'disetujui'; - $transaction->save(); + // Update Data + $transaction->update([ + 'pickup_date' => $request->pickup_date, + 'pickup_time' => $request->pickup_time, + 'tanggal_kembali' => $tanggalKembali, + 'status' => 'disetujui' + ]); - if ($transaction->anggota && $transaction->anggota->email) { + IotMode::query()->update([ + 'mode' => 'user', + 'updated_at' => now() + ]); + + // πŸ“§ PROSES KIRIM EMAIL + // Pastikan relasi ke user benar untuk mengambil email + $recipientEmail = $transaction->anggota->user->email ?? $transaction->anggota->email; + + if ($recipientEmail) { try { - Mail::to($transaction->anggota->email) - ->send(new BukuSiapDiambilMail($transaction)); + Mail::to($recipientEmail)->send(new BukuSiapDiambilMail($transaction)); } catch (\Exception $e) { + Log::error('Gagal kirim email BukuSiapDiambil: ' . $e->getMessage()); + // Tetap lanjut meskipun email gagal agar status di DB tersimpan } } - return redirect()->route('admin.peminjaman') - ->with('success', 'Peminjaman disetujui. Email pemberitahuan telah dikirim ke anggota.'); + return redirect()->route('admin.peminjaman')->with('success', 'Peminjaman disetujui dan email notifikasi dikirim.'); } - public function ambilBuku($id) { $transaction = BorrowTransaction::findOrFail($id); if ($transaction->status !== 'disetujui') { - return redirect()->route('admin.peminjaman') - ->with('error', 'Buku belum disetujui atau sudah diambil.'); + return redirect()->route('admin.peminjaman')->with('error', 'Buku belum disetujui atau sudah diambil.'); } $transaction->markAsDipinjam(); - return redirect()->route('admin.peminjaman') - ->with('success', 'Buku berhasil diambil. Status transaksi: Dipinjam.'); + IotMode::query()->update([ + 'mode' => 'standby', + 'updated_at' => now() + ]); + + return redirect()->route('admin.peminjaman')->with('success', 'Buku berhasil diambil.'); } - public function tolak($id) { - $transaction = BorrowTransaction::findOrFail($id); + $transaction = BorrowTransaction::with('anggota.user')->findOrFail($id); if (!in_array($transaction->status, ['menunggu', 'pending'])) { return redirect()->back()->with('error', 'Transaksi ini sudah diproses.'); @@ -131,110 +134,185 @@ public function tolak($id) $transaction->markAsDitolak(); - if ($transaction->anggota && $transaction->anggota->email) { + $recipientEmail = $transaction->anggota->user->email ?? $transaction->anggota->email; + if ($recipientEmail) { try { - Mail::to($transaction->anggota->email) - ->send(new PeminjamanDitolakMail($transaction)); + Mail::to($recipientEmail)->send(new PeminjamanDitolakMail($transaction)); } catch (\Exception $e) { + Log::error('Gagal kirim email PeminjamanDitolak: ' . $e->getMessage()); } } - return redirect()->route('admin.peminjaman') - ->with('success', 'Peminjaman ditolak dan email pemberitahuan dikirim.'); + return redirect()->route('admin.peminjaman')->with('success', 'Peminjaman ditolak.'); } - - public function setujuiPengembalian($id) + public function setujuiPerpanjangan(Request $request, $id) { - $transaction = BorrowTransaction::findOrFail($id); - - if ($transaction->status !== 'pengembalian_menunggu') { - return back()->with('error', 'Transaksi ini tidak menunggu pengembalian.'); - } - - $transaction->markAsDikembalikan(); - - if ($transaction->anggota && $transaction->anggota->email) { - try { - Mail::to($transaction->anggota->email)->send(new BukuDikembalikanMail($transaction)); - } catch (\Exception $e) { - } - } - - return redirect()->route('admin.peminjaman') - ->with('success', 'Pengembalian buku telah disetujui oleh admin.'); - } - - - public function store(Request $request) - { - $request->validate([ - 'anggota_id' => 'required|exists:anggota,id', - 'koleksi_id' => 'required|exists:koleksis,biblio_id', - 'tanggal_pinjam' => 'required|date', - 'surat_pengantar' => 'nullable|file|mimes:pdf|max:2048', - ]); - - $data = $request->only('anggota_id', 'koleksi_id', 'tanggal_pinjam'); - - if ($request->hasFile('surat_pengantar')) { - $file = $request->file('surat_pengantar'); - $filename = time() . '_' . $file->getClientOriginalName(); - $file->storeAs('public/surat_pengantar', $filename); - $data['surat_pengantar'] = $filename; - } - - $data['status'] = 'menunggu'; - - BorrowTransaction::create($data); - - return redirect()->route('admin.peminjaman') - ->with('success', 'Transaksi peminjaman berhasil dibuat dan menunggu persetujuan.'); - } - - - public function setujuiPerpanjangan($id) - { - $transaction = BorrowTransaction::findOrFail($id); + $transaction = BorrowTransaction::with('anggota.user')->findOrFail($id); if ($transaction->status !== 'perpanjangan_menunggu') { - return back()->with('error', 'Transaksi ini tidak menunggu perpanjangan.'); + return back()->with('error', 'Permohonan perpanjangan tidak ditemukan.'); } - $transaction->tanggal_kembali = Carbon::parse($transaction->tanggal_kembali)->addDays(7); - $transaction->status = 'dipinjam'; - $transaction->save(); + // Jika Admin input tanggal di modal, gunakan itu. Jika tidak, tambah 7 hari dari tanggal kembali lama. + $newTanggalKembali = $request->tanggal_kembali + ? Carbon::parse($request->tanggal_kembali) + : Carbon::parse($transaction->tanggal_kembali)->addDays(7); - if ($transaction->anggota && $transaction->anggota->email) { + $transaction->update([ + 'tanggal_kembali' => $newTanggalKembali, + 'status' => 'dipinjam' + ]); + + $recipientEmail = $transaction->anggota->user->email ?? $transaction->anggota->email; + if ($recipientEmail) { try { - Mail::to($transaction->anggota->email) - ->send(new PerpanjanganDisetujuiMail($transaction)); + Mail::to($recipientEmail)->send(new PerpanjanganDisetujuiMail($transaction)); } catch (\Exception $e) { + Log::error('Gagal kirim email PerpanjanganDisetujui: ' . $e->getMessage()); } } - return back()->with('success', 'Perpanjangan disetujui dan email telah dikirim ke anggota.'); + return back()->with('success', 'Perpanjangan disetujui.'); } public function tolakPerpanjangan($id) { - $transaction = BorrowTransaction::findOrFail($id); + $transaction = BorrowTransaction::with('anggota.user')->findOrFail($id); if ($transaction->status !== 'perpanjangan_menunggu') { - return back()->with('error', 'Transaksi ini tidak menunggu perpanjangan.'); + return back()->with('error', 'Permohonan tidak valid.'); } - $transaction->status = 'dipinjam'; - $transaction->save(); + $transaction->update(['status' => 'dipinjam']); - if ($transaction->anggota && $transaction->anggota->email) { + $recipientEmail = $transaction->anggota->user->email ?? $transaction->anggota->email; + if ($recipientEmail) { try { - Mail::to($transaction->anggota->email) - ->send(new PerpanjanganDitolakMail($transaction)); + Mail::to($recipientEmail)->send(new PerpanjanganDitolakMail($transaction)); } catch (\Exception $e) { + Log::error('Gagal kirim email PerpanjanganDitolak: ' . $e->getMessage()); } } - return back()->with('success', 'Perpanjangan ditolak dan email pemberitahuan telah dikirim.'); + return back()->with('success', 'Perpanjangan ditolak.'); + } + + public function kembalikan($id) + { + $transaction = BorrowTransaction::with('anggota.user')->findOrFail($id); + + if (!in_array($transaction->status, ['dipinjam', 'pengembalian_menunggu'])) { + return redirect()->route('admin.peminjaman')->with('error', 'Status tidak valid untuk dikembalikan.'); + } + + $transaction->markAsDikembalikan(); + + $recipientEmail = $transaction->anggota->user->email ?? $transaction->anggota->email; + if ($recipientEmail) { + try { + Mail::to($recipientEmail)->send(new BukuDikembalikanMail($transaction)); + } catch (\Exception $e) { + Log::error('Gagal kirim email BukuDikembalikan: ' . $e->getMessage()); + } + } + + return redirect()->route('admin.peminjaman')->with('success', 'Buku berhasil dikembalikan.'); + } + + public function espScan(Request $request) + { + $uid = strtoupper($request->query('uid') ?? ''); + $mode = $request->query('mode') ?? ''; + + if (!$uid) return response()->json(['status' => 'uid_kosong']); + if ($mode !== 'user') return response()->json(['status' => 'mode_salah']); + + $anggota = DB::table('anggota')->where('uid', $uid)->first(); + if (!$anggota) return response()->json(['status' => 'tidak_valid']); + + $jumlahPinjaman = BorrowTransaction::where('anggota_id', $anggota->id) + ->whereIn('status', ['dipinjam', 'pengembalian_menunggu']) + ->count(); + + if ($jumlahPinjaman >= 2) return response()->json(['status' => 'limit_tercapai']); + + $transaction = BorrowTransaction::where('anggota_id', $anggota->id) + ->where('status', 'disetujui') + ->first(); + + if (!$transaction) return response()->json(['status' => 'tidak_valid']); + + $transaction->markAsDipinjam(); + + IotMode::query()->update([ + 'mode' => 'standby', + 'updated_at' => now() + ]); + + return response()->json(['status' => 'valid']); + } + // Tambahkan ini di PeminjamanController.php + +public function konfirmasiKembali() +{ + try { + // 1. Ambil data terakhir dari IotMode untuk tahu buku apa yang baru di-scan + $iot = IotMode::first(); + + if (!$iot || !$iot->book_id) { + return response()->json([ + 'status' => 'error', + 'message' => 'Data buku tidak ditemukan di sistem IoT' + ], 404); + } + + // 2. Cari transaksi yang statusnya 'dipinjam' atau 'pengembalian_menunggu' berdasarkan buku tersebut + $transaction = BorrowTransaction::with('anggota.user') + ->where('koleksi_id', $iot->book_id) // Pastikan kolom ini sesuai dengan di DB Anda + ->whereIn('status', ['dipinjam', 'pengembalian_menunggu']) + ->first(); + + if (!$transaction) { + return response()->json([ + 'status' => 'error', + 'message' => 'Transaksi aktif untuk buku ini tidak ditemukan' + ], 404); + } + + // 3. Proses Pengembalian (Memakai fungsi yang sudah Anda punya di model) + $transaction->markAsDikembalikan(); + + // 4. Reset IotMode ke standby + IotMode::query()->update([ + 'mode' => 'standby', + 'book_id' => null, // Bersihkan ID buku agar tidak terbaca lagi + 'updated_at' => now() + ]); + + // 5. Kirim Email (Opsional, memakai logika yang sama dengan fungsi kembalikan manual) + $recipientEmail = $transaction->anggota->user->email ?? $transaction->anggota->email; + if ($recipientEmail) { + try { + Mail::to($recipientEmail)->send(new BukuDikembalikanMail($transaction)); + } catch (\Exception $e) { + Log::error('Gagal kirim email: ' . $e->getMessage()); + } + } + + // 6. WAJIB: Kirim balasan JSON agar JS tidak error + return response()->json([ + 'status' => 'berhasil_kembali', + 'message' => 'Buku ' . $transaction->koleksi->judul . ' berhasil dikembalikan' + ]); + + } catch (\Exception $e) { + Log::error('Error Konfirmasi Kembali: ' . $e->getMessage()); + return response()->json([ + 'status' => 'error', + 'message' => 'Terjadi kesalahan internal: ' . $e->getMessage() + ], 500); } } +} + diff --git a/app/Http/Controllers/Admin/StatistikController.php b/app/Http/Controllers/Admin/StatistikController.php index eb8a9ae..c7a7909 100644 --- a/app/Http/Controllers/Admin/StatistikController.php +++ b/app/Http/Controllers/Admin/StatistikController.php @@ -4,38 +4,196 @@ use App\Http\Controllers\Controller; use App\Models\Koleksi; -use App\Models\Member; +use App\Models\Anggota; use App\Models\BorrowTransaction; +use App\Models\User; // Menggunakan model User untuk data pendaftaran akun use Illuminate\Http\Request; +use Carbon\Carbon; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Facades\Auth; class StatistikController extends Controller { public function index() { + // 1. RINGKASAN UTAMA $totalBuku = Koleksi::count('kode_eksemplar'); - $totalAnggota = Member::count(); + $totalAnggota = Anggota::count(); + + $anggotaBaruBulanIni = Anggota::whereMonth('created_at', Carbon::now()->month) + ->whereYear('created_at', Carbon::now()->year) + ->count(); + + // 2. STATUS SIRKULASI $totalPeminjaman = BorrowTransaction::where('status', 'dipinjam')->count(); $totalPengembalian = BorrowTransaction::where('status', 'dikembalikan')->count(); + // --- PENGAMAN KOLOM JATUH TEMPO / TANGGAL KEMBALI --- + $totalTerlambat = 0; + if (Schema::hasColumn('borrow_transactions', 'tanggal_kembali')) { + $totalTerlambat = BorrowTransaction::where('status', 'dipinjam') + ->where('tanggal_kembali', '<', Carbon::today()) + ->count(); + } elseif (Schema::hasColumn('borrow_transactions', 'tanggal_harus_kembali')) { + $totalTerlambat = BorrowTransaction::where('status', 'dipinjam') + ->where('tanggal_harus_kembali', '<', Carbon::today()) + ->count(); + } + + // --- PENGAMAN KOLOM DENDA --- + $totalDenda = 0; + if (Schema::hasColumn('borrow_transactions', 'denda')) { + $totalDenda = BorrowTransaction::sum('denda'); + } + + $rasioKeterpakaian = $totalBuku > 0 + ? round(($totalPeminjaman / $totalBuku) * 100) . '%' + : '0%'; + + // 3. PROSES DATA ASAL WILAYAH (Berdasarkan Tabel Anggota Kolom Alamat) + $wilayahCounts = [ + 'Kota Surabaya' => 0, + 'Kabupaten Sidoarjo' => 0, + 'Kabupaten Malang' => 0, + 'Kota Malang' => 0, + 'Kota Batu' => 0, + 'Kabupaten Pasuruan' => 0, + 'Kota Pasuruan' => 0, + 'Kabupaten Blitar' => 0, + 'Kota Blitar' => 0, + 'Luar Wilayah' => 0, + ]; + + if (Schema::hasColumn('anggota', 'alamat')) { + // Ambil semua string alamat dari database + $daftarAlamat = Anggota::pluck('alamat'); + + foreach ($daftarAlamat as $alamat) { + if (empty($alamat)) { + $wilayahCounts['Luar Wilayah']++; + continue; + } + + $alamatLower = strtolower($alamat); + + // Klasifikasi berdasarkan kata kunci spesifik + if (str_contains($alamatLower, 'surabaya')) { + $wilayahCounts['Kota Surabaya']++; + } elseif (str_contains($alamatLower, 'sidoarjo')) { + $wilayahCounts['Kabupaten Sidoarjo']++; + } elseif (str_contains($alamatLower, 'batu')) { + $wilayahCounts['Kota Batu']++; + } + // Cek Kota Malang vs Kabupaten Malang + elseif (str_contains($alamatLower, 'kota malang')) { + $wilayahCounts['Kota Malang']++; + } elseif (str_contains($alamatLower, 'malang')) { + $wilayahCounts['Kabupaten Malang']++; + } + // Cek Kota Pasuruan vs Kabupaten Pasuruan + elseif (str_contains($alamatLower, 'kota pasuruan')) { + $wilayahCounts['Kota Pasuruan']++; + } elseif (str_contains($alamatLower, 'pasuruan')) { + $wilayahCounts['Kabupaten Pasuruan']++; + } + // Cek Kota Blitar vs Kabupaten Blitar + elseif (str_contains($alamatLower, 'kota blitar')) { + $wilayahCounts['Kota Blitar']++; + } elseif (str_contains($alamatLower, 'blitar')) { + $wilayahCounts['Kabupaten Blitar']++; + } + // Jika tidak mampir ke kriteria manapun + else { + $wilayahCounts['Luar Wilayah']++; + } + } + } + + // Pecah array asosiatif menjadi index array polos untuk konsumsi Chart.js + $wilayahLabels = array_keys($wilayahCounts); + $wilayahData = array_values($wilayahCounts); + + + // 4. METRIK RINGKASAN DATA USER (Tabel Users) + $userHariIni = User::whereDate('created_at', Carbon::today())->count(); + $userBulanIni = User::whereMonth('created_at', Carbon::now()->month)->whereYear('created_at', Carbon::now()->year)->count(); + $userTahunIni = User::whereYear('created_at', Carbon::now()->year)->count(); + + // 5. DATA MASTER BULAN & PEMINJAMAN (Untuk Sumbu X Semua Grafik) + $bulanLabels = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']; + $tahunIni = Carbon::now()->year; + + // A. Data Volume Peminjaman $peminjamanPerBulan = BorrowTransaction::selectRaw('MONTH(tanggal_pinjam) as bulan, COUNT(*) as total') + ->whereYear('tanggal_pinjam', $tahunIni) ->groupBy('bulan') - ->orderBy('bulan') ->pluck('total', 'bulan'); - $bulanLabels = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']; $dataPeminjaman = []; - for ($i = 1; $i <= 12; $i++) { $dataPeminjaman[] = $peminjamanPerBulan[$i] ?? 0; } + // B. Data Grafik Aktivitas Sistem (Berdasarkan users.created_at) + $userPerBulan = User::selectRaw('MONTH(created_at) as bulan, COUNT(*) as total') + ->whereYear('created_at', $tahunIni) + ->groupBy('bulan') + ->pluck('total', 'bulan'); + + $chartUsersCreatedAt = []; + for ($i = 1; $i <= 12; $i++) { + $chartUsersCreatedAt[] = $userPerBulan[$i] ?? 0; + } + + // C. Data Grafik Anggota (Berdasarkan anggota.created_at) + $anggotaPerBulan = Anggota::selectRaw('MONTH(created_at) as bulan, COUNT(*) as total') + ->whereYear('created_at', $tahunIni) + ->groupBy('bulan') + ->pluck('total', 'bulan'); + + $chartAnggotaCreatedAt = []; + for ($i = 1; $i <= 12; $i++) { + $chartAnggotaCreatedAt[] = $anggotaPerBulan[$i] ?? 0; + } + + + // 6. PENGAMAN KONDISI BUKU + $bukuReferensi = 0; $bukuRusak = 0; $bukuHilang = 0; + if (Schema::hasColumn('koleksis', 'kondisi_buku')) { + $bukuRusak = Koleksi::where('kondisi_buku', 'rusak')->count(); + $bukuHilang = Koleksi::where('kondisi_buku', 'hilang')->count(); + } elseif (Schema::hasColumn('koleksis', 'kondisi')) { + $bukuRusak = Koleksi::where('kondisi', 'rusak')->count(); + $bukuHilang = Koleksi::where('kondisi', 'hilang')->count(); + } + + $totalUsulanBuku = Schema::hasTable('usulan_buku') ? DB::table('usulan_buku')->count() : 0; + + // 7. GENDER MEMBER + $genderLaki = 0; $genderPerempuan = 0; + if (Schema::hasColumn('anggota', 'gender')) { + $genderLaki = Anggota::whereIn('gender', ['Laki-laki', 'L'])->count(); + $genderPerempuan = Anggota::whereIn('gender', ['Perempuan', 'P'])->count(); + } elseif (Schema::hasColumn('anggota', 'jenis_kelamin')) { + $genderLaki = Anggota::whereIn('jenis_kelamin', ['Laki-laki', 'L'])->count(); + $genderPerempuan = Anggota::whereIn('jenis_kelamin', ['Perempuan', 'P'])->count(); + } + + if ($genderLaki === 0 && $genderPerempuan === 0) { + $genderLaki = 50; + $genderPerempuan = 50; + } + + // Kirim semua variabel ke view admin.statistik return view('admin.statistik', compact( - 'totalBuku', - 'totalAnggota', - 'totalPeminjaman', - 'totalPengembalian', - 'bulanLabels', - 'dataPeminjaman' + 'totalBuku', 'totalAnggota', 'anggotaBaruBulanIni', + 'totalPeminjaman', 'totalPengembalian', 'totalTerlambat', 'totalDenda', 'rasioKeterpakaian', + 'wilayahLabels', 'wilayahData', + 'userHariIni', 'userBulanIni', 'userTahunIni', + 'bukuReferensi', 'bukuRusak', 'bukuHilang', 'totalUsulanBuku', + 'genderLaki', 'genderPerempuan', 'bulanLabels', 'dataPeminjaman', + 'chartUsersCreatedAt', 'chartAnggotaCreatedAt' )); } -} +} \ No newline at end of file diff --git a/app/Http/Controllers/Admin/SystemController.php b/app/Http/Controllers/Admin/SystemController.php index 9f79baf..30cb1b5 100644 --- a/app/Http/Controllers/Admin/SystemController.php +++ b/app/Http/Controllers/Admin/SystemController.php @@ -9,94 +9,198 @@ class SystemController extends Controller { - + /* + |-------------------------------------------------------------------------- + | HALAMAN KELOLA SISTEM + |-------------------------------------------------------------------------- + */ + public function index() { - if (!Auth::check() || Auth::user()->role !== 'admin') { - abort(403, 'Akses ditolak. Hanya admin yang dapat membuka halaman ini.'); + // HANYA SUPER ADMIN + if ( + !Auth::check() || + Auth::user()->role !== 'super_admin' + ) { + abort(403, 'Akses ditolak. Hanya super admin.'); } $users = User::orderBy('name')->get(); + return view('admin.system', compact('users')); } + /* + |-------------------------------------------------------------------------- + | JADIKAN USER MENJADI ADMIN + |-------------------------------------------------------------------------- + */ + public function makeAdmin(Request $request) { - if (!Auth::check() || Auth::user()->role !== 'admin') { + // HANYA SUPER ADMIN + if ( + !Auth::check() || + Auth::user()->role !== 'super_admin' + ) { abort(403, 'Akses ditolak.'); } - $request->validate(['user_id' => 'required|exists:users,id']); + $request->validate([ + 'user_id' => 'required|exists:users,id' + ]); + $user = User::findOrFail($request->user_id); + // Tidak bisa ubah super admin + if ($user->role === 'super_admin') { + return back()->with( + 'error', + 'Tidak dapat mengubah super admin.' + ); + } + $user->role = 'admin'; $user->save(); - return back()->with('success', "{$user->name} sekarang menjadi admin."); + return back()->with( + 'success', + "{$user->name} sekarang menjadi admin." + ); } - + /* + |-------------------------------------------------------------------------- + | CABUT ADMIN MENJADI USER + |-------------------------------------------------------------------------- + */ + public function removeAdmin(Request $request) { - if (!Auth::check() || Auth::user()->role !== 'admin') { + // HANYA SUPER ADMIN + if ( + !Auth::check() || + Auth::user()->role !== 'super_admin' + ) { abort(403, 'Akses ditolak.'); } - $request->validate(['user_id' => 'required|exists:users,id']); + $request->validate([ + 'user_id' => 'required|exists:users,id' + ]); + $user = User::findOrFail($request->user_id); + // Tidak bisa ubah diri sendiri if ($user->id === Auth::id()) { - return back()->with('error', 'Anda tidak dapat mencabut status admin Anda sendiri.'); + return back()->with( + 'error', + 'Anda tidak dapat mencabut status Anda sendiri.' + ); + } + + // Tidak bisa ubah super admin lain + if ($user->role === 'super_admin') { + return back()->with( + 'error', + 'Tidak dapat mengubah super admin.' + ); } $user->role = 'user'; $user->save(); - return back()->with('success', "Status admin untuk {$user->name} telah dicabut."); + return back()->with( + 'success', + "Status admin untuk {$user->name} telah dicabut." + ); } + /* + |-------------------------------------------------------------------------- + | BLOKIR USER + |-------------------------------------------------------------------------- + */ + public function block(Request $request) { - if (!Auth::check() || Auth::user()->role !== 'admin') { + // HANYA SUPER ADMIN + if ( + !Auth::check() || + Auth::user()->role !== 'super_admin' + ) { abort(403, 'Akses ditolak.'); } - $request->validate(['user_id' => 'required|exists:users,id']); + $request->validate([ + 'user_id' => 'required|exists:users,id' + ]); $user = User::find($request->user_id); if (!$user) { - return back()->with('error', 'User tidak ditemukan.'); + return back()->with( + 'error', + 'User tidak ditemukan.' + ); } - if ($user->role === 'admin') { - return back()->with('error', 'Tidak dapat memblokir admin.'); + // Tidak bisa blokir admin + if ( + $user->role === 'admin' || + $user->role === 'super_admin' + ) { + return back()->with( + 'error', + 'Tidak dapat memblokir admin atau super admin.' + ); } $user->blocked_at = now(); $user->save(); - return back()->with('success', "User {$user->name} telah diblokir."); + return back()->with( + 'success', + "User {$user->name} telah diblokir." + ); } - + /* + |-------------------------------------------------------------------------- + | BUKA BLOKIR USER + |-------------------------------------------------------------------------- + */ + public function unblock(Request $request) { - if (!Auth::check() || Auth::user()->role !== 'admin') { + // HANYA SUPER ADMIN + if ( + !Auth::check() || + Auth::user()->role !== 'super_admin' + ) { abort(403, 'Akses ditolak.'); } - $request->validate(['user_id' => 'required|exists:users,id']); + $request->validate([ + 'user_id' => 'required|exists:users,id' + ]); $user = User::find($request->user_id); if (!$user) { - return back()->with('error', 'User tidak ditemukan.'); + return back()->with( + 'error', + 'User tidak ditemukan.' + ); } $user->blocked_at = null; $user->save(); - return back()->with('success', "Blokir untuk {$user->name} telah dibuka."); + return back()->with( + 'success', + "Blokir untuk {$user->name} telah dibuka." + ); } } + diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index fef8075..cec554e 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -17,3 +17,4 @@ public function anggota() { return view('admin.anggota'); } public function sirkulasi() { return view('admin.sirkulasi'); } public function settings() { return view('admin.settings'); } } + diff --git a/app/Http/Controllers/Api/IotController.php b/app/Http/Controllers/Api/IotController.php new file mode 100644 index 0000000..4f74cc4 --- /dev/null +++ b/app/Http/Controllers/Api/IotController.php @@ -0,0 +1,17 @@ +mode); + } +} + diff --git a/app/Http/Controllers/Auth/GoogleController.php b/app/Http/Controllers/Auth/GoogleController.php index 3df2dbe..bccbab2 100644 --- a/app/Http/Controllers/Auth/GoogleController.php +++ b/app/Http/Controllers/Auth/GoogleController.php @@ -37,3 +37,4 @@ public function handleGoogleCallback() } } } + diff --git a/app/Http/Controllers/Auth/LoginWithEmailController.php b/app/Http/Controllers/Auth/LoginWithEmailController.php index 1e1450e..e50dc0c 100644 --- a/app/Http/Controllers/Auth/LoginWithEmailController.php +++ b/app/Http/Controllers/Auth/LoginWithEmailController.php @@ -49,3 +49,4 @@ public function login($token) { } } + diff --git a/app/Http/Controllers/Auth/OtpVerificationController.php b/app/Http/Controllers/Auth/OtpVerificationController.php index c20b09f..b27beda 100644 --- a/app/Http/Controllers/Auth/OtpVerificationController.php +++ b/app/Http/Controllers/Auth/OtpVerificationController.php @@ -42,3 +42,4 @@ public function verify(Request $request) } + diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index a5c6840..df399fd 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -39,3 +39,4 @@ public function register(Request $request) return redirect()->route('verification.notice'); } } + diff --git a/app/Http/Controllers/Auth/VerificationController.php b/app/Http/Controllers/Auth/VerificationController.php index 886999b..5da97e4 100644 --- a/app/Http/Controllers/Auth/VerificationController.php +++ b/app/Http/Controllers/Auth/VerificationController.php @@ -22,3 +22,4 @@ public function verify($token) return redirect()->route('login')->with('success', 'Email berhasil diverifikasi. Silakan login.'); } } + diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 866cdff..b7bed36 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -51,7 +51,7 @@ public function register(Request $request) ]); $otp = rand(100000, 999999); - $expiresAt = now()->addMinute(); + $now = Carbon::now(); $user = User::create([ 'name' => $request->name, @@ -59,8 +59,8 @@ public function register(Request $request) 'password' => Hash::make($request->password), 'role' => 'user', 'email_otp' => $otp, - 'email_otp_expires_at' => $expiresAt, - 'otp_last_sent_at' => now(), + 'email_otp_expires_at' => $now, + 'otp_last_sent_at' => $now, ]); $this->sendOtpEmail($user, $otp); @@ -90,7 +90,10 @@ public function login(Request $request) } if (Auth::attempt($credentials)) { + // Mengamankan session ID baru sebelum melakukan pengalihan rute $request->session()->regenerate(); + + // Dipastikan langsung mengeksekusi return redirect bypass role return $this->redirectBasedOnRole(); } @@ -102,30 +105,30 @@ public function logout(Request $request) Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); - return redirect()->route('login'); + return redirect('/login'); } public function dashboard() { - $user = Auth::user(); - if (!$user) return redirect()->route('login'); - - if ($user->role === 'admin') return redirect()->route('admin.dashboard'); - if ($user->role === 'user') return redirect()->route('user.dashboard'); - abort(403, 'Akses ditolak!'); + // Jembatan utama penentu rute setelah melewati guard auth bawaan + return $this->redirectBasedOnRole(); } public function adminDashboard() { $user = Auth::user(); - if (!$user || $user->role !== 'admin') return redirect()->route('login'); + if (!$user || ($user->role !== 'admin' && $user->role !== 'super_admin')) { + return redirect()->route('login'); + } return view('admin.dashboard', compact('user')); } public function userDashboard() { $user = Auth::user(); - if (!$user || $user->role !== 'user') return redirect()->route('login'); + if (!$user || $user->role !== 'user') { + return redirect()->route('login'); + } return view('user.dashboard', compact('user')); } @@ -133,7 +136,8 @@ public function profile() { $user = Auth::user(); if (!$user) return redirect()->route('login'); - return $user->role === 'admin' + + return ($user->role === 'admin' || $user->role === 'super_admin') ? view('admin.profile', compact('user')) : view('user.profile', compact('user')); } @@ -144,13 +148,15 @@ public function security() if (!$user || $user->role !== 'user') return redirect()->route('login'); return view('user.security', compact('user')); } - public function adminSecurity() -{ - $user = Auth::user(); - if (!$user || $user->role !== 'admin') return redirect()->route('login'); - return view('admin.security', compact('user')); -} + public function adminSecurity() + { + $user = Auth::user(); + if (!$user || ($user->role !== 'admin' && $user->role !== 'super_admin')) { + return redirect()->route('login'); + } + return view('admin.security', compact('user')); + } public function updateProfile(Request $request) { @@ -186,7 +192,9 @@ public function updateProfile(Request $request) } $user->save(); - return redirect()->route('user.security')->with('success', 'Profil & password berhasil diperbarui!'); + + $route = ($user->role === 'admin' || $user->role === 'super_admin') ? 'admin.security' : 'user.security'; + return redirect()->route($route)->with('success', 'Profil & password berhasil diperbarui!'); } public function userKoleksi() @@ -201,10 +209,20 @@ public function userKoleksi() private function redirectBasedOnRole() { $user = Auth::user(); - if (!$user) return redirect()->route('login'); - return $user->role === 'admin' - ? redirect()->route('admin.dashboard') - : redirect()->route('user.dashboard'); + if (!$user) { + return redirect()->route('login'); + } + + if ($user->role === 'admin' || $user->role === 'super_admin') { + return redirect()->route('admin.dashboard'); + } + + if ($user->role === 'user') { + return redirect()->route('user.dashboard'); + } + + Auth::logout(); + return redirect()->route('login')->withErrors(['email' => 'Hak akses tidak dikenali.']); } public function showVerifyEmailForm(Request $request) @@ -228,11 +246,18 @@ public function verifyEmail(Request $request) return back()->withErrors(['otp' => 'Kode verifikasi salah.']); } - if ($user->email_otp_expires_at && now()->greaterThan($user->email_otp_expires_at)) { - $user->otp_last_sent_at = null; - $user->save(); + if ($user->email_otp_expires_at) { + $otpCreatedAt = Carbon::parse($user->email_otp_expires_at); + $secondsElapsed = Carbon::now()->diffInSeconds($otpCreatedAt); - return back()->withErrors(['otp' => 'Kode OTP sudah kadaluarsa. Silakan kirim ulang.']); + if ($secondsElapsed > 60) { + $user->update([ + 'email_otp' => null, + 'email_otp_expires_at' => null, + 'otp_last_sent_at' => null, + ]); + return back()->withErrors(['otp' => 'Kode OTP sudah kadaluarsa (lebih dari 1 menit). Silakan klik kirim ulang.']); + } } $user->update([ @@ -252,17 +277,12 @@ public function resendOtp(Request $request) if (!$user) return back()->withErrors(['email' => 'Email tidak ditemukan.']); - if ($user->email_otp_expires_at && now()->greaterThan($user->email_otp_expires_at)) { - $user->otp_last_sent_at = null; - $user->save(); - } - if (empty($user->otp_last_sent_at)) { return $this->sendNewOtp($user); } $lastSent = Carbon::parse($user->otp_last_sent_at); - $diff = now()->diffInSeconds($lastSent); + $diff = Carbon::now()->diffInSeconds($lastSent); if ($diff < 60) { $remaining = 60 - $diff; @@ -275,11 +295,12 @@ public function resendOtp(Request $request) private function sendNewOtp($user) { $otp = rand(100000, 999999); + $now = Carbon::now(); $user->update([ - 'email_otp' => $otp, - 'email_otp_expires_at' => now()->addMinute(), - 'otp_last_sent_at' => now(), + 'email_otp' => $otp, + 'email_otp_expires_at' => $now, + 'otp_last_sent_at' => $now, ]); $this->sendOtpEmail($user, $otp); @@ -357,7 +378,6 @@ public function updateFoto(Request $request) $user = Auth::user(); if ($request->hasFile('foto')) { - if ($user->foto && file_exists(public_path('storage/foto/'.$user->foto))) { unlink(public_path('storage/foto/'.$user->foto)); } @@ -386,5 +406,5 @@ public function deleteFoto() return back()->with('success', 'Foto profil berhasil dihapus!'); } - } + diff --git a/app/Http/Controllers/BorrowTransactionController.php b/app/Http/Controllers/BorrowTransactionController.php index 806ff26..554e8a0 100644 --- a/app/Http/Controllers/BorrowTransactionController.php +++ b/app/Http/Controllers/BorrowTransactionController.php @@ -82,3 +82,4 @@ public function kembalikan($id) ->with('success', 'Buku berhasil dikembalikan!'); } } + diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index a0a2a8a..8019344 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -11,3 +11,4 @@ class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; } + diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index a2c61a1..ac2e279 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -37,3 +37,4 @@ public function index() )); } } + diff --git a/app/Http/Controllers/EspController.php b/app/Http/Controllers/EspController.php new file mode 100644 index 0000000..f970c25 --- /dev/null +++ b/app/Http/Controllers/EspController.php @@ -0,0 +1,107 @@ +query('uid') ?? '')); + $eventId = time() . rand(100,999); + + if (!$uid) { + return response()->json(['status' => 'error', 'message' => 'Kartu tidak terdeteksi'], 400); + } + + // 2. Cek Mode IOT (Hanya boleh scan jika mode 'user' atau 'scan_member') + $iot = IotMode::first(); + if (!$iot) { + return response()->json(['status' => 'error', 'message' => 'Sistem Belum Siap'], 500); + } + + // 3. Cari data Anggota (Sesuai struktur DB Anda: kolom 'uid') + $anggota = DB::table('anggota') + ->whereRaw("UPPER(TRIM(uid)) = ?", [$uid]) + ->first(); + + if (!$anggota) { + // Kita kirim 404 agar JavaScript masuk ke blok catch "TIDAK TERDAFTAR" + return response()->json([ + 'status' => 'gagal', + 'message' => 'Anggota tidak ditemukan' + ], 404); + } + + // 4. Proses Transaksi (Jika buku sudah di-scan sebelumnya) + $buku = null; + if ($iot->book_id) { + $buku = DB::table('koleksis')->where('biblio_id', $iot->book_id)->first(); + + if ($buku) { + // Simpan ke database transaksi asli + $trx = BorrowTransaction::create([ + 'anggota_id' => $anggota->id, + 'koleksi_id' => $buku->biblio_id, + 'tanggal_pinjam' => now(), + 'tanggal_kembali' => now()->addDays(7), + 'status' => 'dipinjam' + ]); + + // Simpan ke Cache untuk keperluan Polling/Halaman Success + Cache::put('rfid_event', [ + 'event_id' => $eventId, + 'status' => 'berhasil', + 'nama' => $anggota->nama, + 'judul' => $buku->title ?? $buku->judul_koleksi, + 'trx_id' => $trx->id + ], now()->addSeconds(60)); + } + } + + // 5. Reset Mode IOT ke Standby setelah sukses scan + $iot->update([ + 'mode' => 'standby', + 'book_id' => null + ]); + + // 6. Return Response yang diharapkan JavaScript Web Anda + return response()->json([ + 'status' => 'berhasil', + 'id' => $anggota->id, + 'nama' => $anggota->nama, + 'judul' => $buku ? ($buku->title ?? $buku->judul_koleksi) : 'Buku tidak terdeteksi' + ], 200); + } + + /* ================= POLLING & MODE (TETAP SAMA) ================= */ + + public function mode() + { + $iot = IotMode::first(); + if (!$iot) return response()->json(['status' => 'empty']); + + $judul = '-'; + if ($iot->book_id) { + $buku = DB::table('koleksis')->where('biblio_id', $iot->book_id)->first(); + $judul = $buku->title ?? $buku->judul_koleksi ?? 'Judul Tidak Ditemukan'; + } + + return response()->json([ + 'status' => 'ok', + 'mode' => $iot->mode, + 'judul' => $judul, + 'barcode' => $iot->book_id ?? '-' + ]); + } +} + diff --git a/app/Http/Controllers/GoogleController.php b/app/Http/Controllers/GoogleController.php new file mode 100644 index 0000000..f7a3b94 --- /dev/null +++ b/app/Http/Controllers/GoogleController.php @@ -0,0 +1,54 @@ +redirect(); + } + + public function handleGoogleCallback() +{ + try { + $user = Socialite::driver('google')->user(); + + $finduser = User::where('google_id', $user->id) + ->orWhere('email', $user->email) + ->first(); + + if($finduser){ + Auth::login($finduser); + } else { + // Saat membuat user baru, tentukan role defaultnya (misal: 'user') + $finduser = User::create([ + 'name' => $user->name, + 'email' => $user->email, + 'google_id'=> $user->id, + 'role' => 'user', // Sesuaikan dengan kolom role di database Anda + 'password' => encrypt('pustakanawasena123') + ]); + Auth::login($finduser); + } + + // --- LOGIKA REDIRECT BERDASARKAN ROLE --- + // Sesuaikan 'admin' atau '1' dengan cara Anda menentukan role + if ($finduser->role == 'admin') { + return redirect()->intended('/admin/dashboard'); + } + + return redirect()->intended('/home'); // Sesuai const HOME di Provider Anda + + } catch (Exception $e) { + return redirect('login')->with('error', 'Gagal login!'); + } +} +} + diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 075436f..405ffa6 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -14,3 +14,4 @@ public function index() return view('home'); // Akan memanggil resources/views/home.blade.php } } + diff --git a/app/Http/Controllers/IotModeController.php b/app/Http/Controllers/IotModeController.php new file mode 100644 index 0000000..2848a9f --- /dev/null +++ b/app/Http/Controllers/IotModeController.php @@ -0,0 +1,83 @@ +mode; // misal: 'user' + $bookId = $request->book_id ?? null; + + // 1. Simpan ke Database sebagai acuan utama + IotMode::query()->update([ + 'mode' => $mode, + 'book_id' => $bookId, + 'updated_at' => now() + ]); + + Cache::forget('iot_mode_data'); + + // 2. Kirim sinyal ke MQTT agar hardware merespon saat itu juga + $this->publishMqtt([ + 'mode' => $mode, + 'book_id' => $bookId, + 'ts' => now()->timestamp + ]); + + return response()->json([ + 'status' => 'ok', + 'mode' => $mode, + 'message' => 'Mode berhasil diperbarui' + ]); + } + + /** + * Reset sistem ke kondisi awal (Standby) + */ + public function reset() + { + IotMode::query()->update([ + 'mode' => 'standby', + 'book_id' => null + ]); + + Cache::forget('iot_mode_data'); + + $this->publishMqtt([ + 'mode' => 'standby', + 'book_id' => null, + 'ts' => now()->timestamp + ]); + + return response()->json(['status' => 'reset_ok']); + } + + /** + * Fungsi Helper untuk koneksi MQTT + */ + private function publishMqtt($data) + { + try { + // Sesuaikan IP Broker MQTT Anda (misal: 127.0.0.1 atau IP Local Laptop) + $mqtt = new MqttClient('127.0.0.1', 1883, 'laravel-publisher-' . uniqid()); + $mqtt->connect(); + + // Kirim JSON ke topik rfid/mode + $mqtt->publish('rfid/mode', json_encode($data), 0); + + $mqtt->disconnect(); + } catch (\Exception $e) { + \Log::error("Gagal mengirim MQTT: " . $e->getMessage()); + } + } +} + diff --git a/app/Http/Controllers/User/KoleksiController.php b/app/Http/Controllers/User/KoleksiController.php index e6e567f..a1c3cad 100644 --- a/app/Http/Controllers/User/KoleksiController.php +++ b/app/Http/Controllers/User/KoleksiController.php @@ -215,3 +215,4 @@ public function getPengarang(Request $request) ]); } } + diff --git a/app/Http/Controllers/User/SecurityController.php b/app/Http/Controllers/User/SecurityController.php index e84661f..c8c25fa 100644 --- a/app/Http/Controllers/User/SecurityController.php +++ b/app/Http/Controllers/User/SecurityController.php @@ -12,3 +12,4 @@ public function index() return view('user.keamanan.security'); // Pastikan view ini dibuat } } + diff --git a/app/Http/Controllers/User/UserController.php b/app/Http/Controllers/User/UserController.php index ae9ea55..69c6477 100644 --- a/app/Http/Controllers/User/UserController.php +++ b/app/Http/Controllers/User/UserController.php @@ -33,3 +33,4 @@ public function security(Request $request) return view('user.keamanan.security', compact('logins', 'totalLogins', 'lastLogin')); } } + diff --git a/app/Http/Controllers/User/UserMemberController.php b/app/Http/Controllers/User/UserMemberController.php index 06a6348..65f0952 100644 --- a/app/Http/Controllers/User/UserMemberController.php +++ b/app/Http/Controllers/User/UserMemberController.php @@ -9,93 +9,209 @@ use Illuminate\Support\Facades\Mail; use App\Mail\AnggotaBaruMenungguPersetujuan; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\Log; +use PhpMqtt\Client\MqttClient; // πŸ”₯ Import library MQTT class UserMemberController extends Controller { + /** + * πŸ”₯ Helper fungsi privat untuk mengirimkan sinyal real-time ke MQTT Broker + */ + private function triggerAdminRefresh($actionMessage) + { + try { + // Membuka koneksi ke MQTT Broker lokal pada port default TCP 1883 + $mqtt = new MqttClient('127.0.0.1', 1883, 'laravel_user_trigger_' . uniqid()); + $mqtt->connect(); + + // Sesuai dengan konfigurasi JS Admin Anda, action diatur sebagai 'created' agar memicu reload + $payload = json_encode([ + 'action' => 'created', + 'message' => $actionMessage + ]); + + $mqtt->publish('pendaftaran/refresh/admin', $payload, 0); + $mqtt->disconnect(); + } catch (\Exception $e) { + // Mencatat error ke log jika broker MQTT mati agar aplikasi user tidak ikut crash/error 500 + Log::error('Gagal mengirim sinyal MQTT ke Admin: ' . $e->getMessage()); + } + } public function create() { + // πŸ”₯ CEK VERIFIKASI EMAIL + if (!Auth::user()->hasVerifiedEmail()) { + return redirect()->route('verification.notice') + ->with('error', 'Silakan verifikasi email terlebih dahulu sebelum mendaftar anggota.'); + } + + // πŸ”₯ LOGIKA REDIRECT JIKA SUDAH DAFTAR + if (Auth::user()->anggota) { + // Hanya lempar ke halaman pending jika statusnya memang 'menunggu' + if (Auth::user()->anggota->status === 'menunggu') { + return redirect()->route('user.member.pending'); + } + + // Jika disetujui, langsung arahkan ke halaman peminjaman/dashboard utama + if (Auth::user()->anggota->status === 'disetujui') { + return redirect()->route('user.peminjaman.index') + ->with('info', 'Kamu sudah terdaftar sebagai anggota aktif.'); + } + + // πŸ’‘ Catatan: Jika statusnya 'ditolak', logika akan lolos ke bawah + // sehingga pengguna dapat masuk kembali ke formulir pendaftaran. + } + return view('user.member.create'); } - - public function store(Request $request) + /** + * Tampilan Halaman Menunggu Persetujuan & Status Anggota + */ + public function pending() { + $anggota = Auth::user()->anggota; + + // Jika belum mendaftar, kembalikan ke form pendaftaran + if (!$anggota) { + return redirect()->route('user.member.create'); + } + + // Jika sudah disetujui, arahkan langsung ke peminjaman + if ($anggota->status === 'disetujui') { + return redirect()->route('user.peminjaman.index'); + } + + // Mengembalikan view status dengan membawa data keanggotaan ($anggota) + return view('user.member.pending', compact('anggota')); + } + +public function store(Request $request) + { + // πŸ”₯ CEK VERIFIKASI EMAIL + if (!Auth::user()->hasVerifiedEmail()) { + return back()->with('error', 'Verifikasi email terlebih dahulu.'); + } + + $anggotaLama = Auth::user()->anggota; + + // πŸ’‘ DISESUAIKAN: Menggunakan kolom 'nik_nip' di database untuk aturan unique + $nikNipRule = 'required|string|max:255|unique:anggota,nik_nip'; + if ($anggotaLama && $anggotaLama->status === 'ditolak') { + $nikNipRule = 'required|string|max:255|unique:anggota,nik_nip,' . $anggotaLama->id; + } + + // Validasi data input form $request->validate([ - 'nama' => 'required|string|max:255', - 'nip_nim' => 'required|string|max:255|unique:anggota,nip_nim', - 'email' => 'required|email|unique:anggota,email', - 'alamat' => 'required|string', - 'no_hp' => 'nullable|string|max:20', - 'foto' => 'nullable|image|mimes:jpg,jpeg,png|max:2048', + 'nama' => 'required|string|max:255', + 'gender' => 'required|in:Laki-laki,Perempuan', + 'nik_nip' => $nikNipRule, // πŸ’‘ SUDAH DISESUAIKAN + 'provinsi' => 'required|string|max:255', + 'kota' => 'required|string|max:255', + 'kecamatan' => 'required|string|max:255', + 'desa' => 'required|string|max:255', + 'kode_pos' => 'required|string|max:5', + 'alamat' => 'required|string|min:10', + 'no_hp' => 'nullable|string|max:20', + 'foto' => 'nullable|image|mimes:jpg,jpeg,png|max:5120', ]); - if (Auth::user()->anggota) { - return redirect()->route('user.peminjaman.create') - ->with('info', 'Kamu sudah terdaftar sebagai anggota.'); + // Proteksi ganda status pendaftaran + if ($anggotaLama) { + if ($anggotaLama->status === 'menunggu' || $anggotaLama->status === 'disetujui') { + return redirect()->route('user.member.pending') + ->with('info', 'Pendaftaran Anda sedang diproses atau sudah aktif.'); + } } - $namaFoto = null; + $namaFoto = $anggotaLama ? $anggotaLama->foto : null; + // Proses unggah foto baru jika ada file yang dikirim if ($request->hasFile('foto')) { + if ($anggotaLama && $anggotaLama->foto) { + if (Storage::disk('public')->exists('foto_anggota/' . $anggotaLama->foto)) { + Storage::disk('public')->delete('foto_anggota/' . $anggotaLama->foto); + } + } + $file = $request->file('foto'); - $namaFoto = time() . '_' . $file->getClientOriginalName(); - - $folderPath = storage_path('app/public/foto_anggota'); - if (!file_exists($folderPath)) { - mkdir($folderPath, 0777, true); - } - - $file->move($folderPath, $namaFoto); - - if (!file_exists($folderPath . '/' . $namaFoto)) { - return back()->with('error', 'Foto gagal diupload.'); - } + $namaFoto = time() . '_' . uniqid() . '.' . $file->getClientOriginalExtension(); + $file->storeAs('foto_anggota', $namaFoto, 'public'); } - $anggota = Anggota::create([ - 'nama' => $request->nama, - 'nip_nim' => $request->nip_nim, - 'email' => $request->email, - 'alamat' => $request->alamat, - 'no_hp' => $request->no_hp ?? null, - 'user_id' => Auth::id(), - 'status' => 'menunggu', - 'foto' => $namaFoto, - ]); + // Gabungkan teks wilayah administratif ke dalam kolom alamat + $alamatLengkap = $request->alamat; + if (!str_contains($alamatLengkap, $request->desa)) { + $alamatLengkap = "Desa/Kel. {$request->desa}, Kec. {$request->kecamatan}, {$request->kota}, Provinsi {$request->provinsi}. Kode Pos: {$request->kode_pos}. Detail: " . $request->alamat; + } + $notifMessage = 'Ada pendaftar baru mandiri!'; + + if ($anggotaLama && $anggotaLama->status === 'ditolak') { + // πŸ”₯ UPDATE DATA JIKA USER DAFTAR ULANG + $anggotaLama->update([ + 'nama' => $request->nama, + 'gender' => $request->gender, + 'nik_nip' => $request->nik_nip, // πŸ’‘ DISESUAIKAN: Menyimpan ke kolom 'nik_nip' + 'alamat' => $alamatLengkap, + 'no_hp' => $request->no_hp ?? null, + 'status' => 'menunggu', + 'foto' => $namaFoto, + ]); + $anggota = $anggotaLama; + $notifMessage = 'Seorang anggota mengajukan pendaftaran ulang!'; + } else { + // πŸ”₯ BUAT BARU JIKA BELUM PERNAH DAFTAR SAMA SEKALI + if (Anggota::where('email', Auth::user()->email)->exists()) { + return back()->with('error', 'Email ini sudah terdaftar sebagai anggota.'); + } + + $anggota = Anggota::create([ + 'nama' => $request->nama, + 'gender' => $request->gender, + 'nik_nip' => $request->nik_nip, // πŸ’‘ DISESUAIKAN: Menyimpan ke kolom 'nik_nip' + 'email' => Auth::user()->email, + 'alamat' => $alamatLengkap, + 'no_hp' => $request->no_hp ?? null, + 'user_id' => Auth::id(), + 'status' => 'menunggu', + 'foto' => $namaFoto, + ]); + } + + // πŸ”₯ TAMBAHAN MQTT + $this->triggerAdminRefresh($notifMessage); + + // Kirim email notifikasi ke admin try { Mail::to('admin@bakorwil.com')->send(new AnggotaBaruMenungguPersetujuan($anggota)); } catch (\Exception $e) { - \Log::error('Gagal mengirim email ke admin: ' . $e->getMessage()); + Log::error('Gagal mengirim email ke admin: ' . $e->getMessage()); } Auth::user()->setRelation('anggota', $anggota); - return redirect()->route('user.peminjaman.create') - ->with('success', 'Data anggota berhasil dikirim. Mohon tunggu persetujuan admin.'); + return redirect()->route('user.member.pending') + ->with('success', 'Data pendaftaran berhasil diperbarui/dikirim. Mohon tunggu persetujuan admin.'); } - - public function index() { $anggota = Anggota::latest()->take(5)->get(); return view('user.member.index', compact('anggota')); } - public function all() { $anggota = Anggota::latest()->get(); return view('user.member.all', compact('anggota')); } - public static function getFotoUrl($anggota) { - if ($anggota->foto && file_exists(storage_path('app/public/foto_anggota/'.$anggota->foto))) { - return asset('storage/foto_anggota/'.$anggota->foto); + if ($anggota && $anggota->foto && Storage::disk('public')->exists('foto_anggota/' . $anggota->foto)) { + return asset('storage/foto_anggota/' . $anggota->foto); } return asset('images/default-user.png'); } -} +} \ No newline at end of file diff --git a/app/Http/Controllers/User/UserPeminjamanController.php b/app/Http/Controllers/User/UserPeminjamanController.php index c6ad518..caa3f55 100644 --- a/app/Http/Controllers/User/UserPeminjamanController.php +++ b/app/Http/Controllers/User/UserPeminjamanController.php @@ -17,96 +17,146 @@ class UserPeminjamanController extends Controller { - - public function index() + public function index() { $anggota = Auth::user()->anggota; + // πŸ”₯ 1. PROTEKSI UTAMA: Cek pendaftaran dan status persetujuan admin if (!$anggota) { return redirect()->route('user.member.create') ->with('error', 'Silakan daftar anggota terlebih dahulu sebelum meminjam buku.'); } - + // Jika statusnya bukan 'disetujui' (misal: 'menunggu' atau 'ditolak'), tendang ke halaman pending + if ($anggota->status !== 'disetujui') { + return redirect()->route('user.member.pending') + ->with('error', 'Akses ditolak! Anda harus berstatus sebagai Anggota Aktif (Disetujui) untuk mengakses halaman transaksi.'); + } + + + // 2. Ambil transaksi dipinjam untuk cek keterlambatan (Kode Asli Anda) $dipinjam = BorrowTransaction::where('anggota_id', $anggota->id) ->where('status', 'dipinjam') ->get(); + $blokir = false; foreach ($dipinjam as $trx) { if ($trx->tanggal_kembali && Carbon::now()->gt(Carbon::parse($trx->tanggal_kembali))) { - if ($trx->is_late == 0) { $trx->is_late = 1; } + $blokir = true; if ($trx->late_notified == 0 && env('MAIL_USERNAME')) { try { Mail::to($trx->anggota->email) ->send(new PeringatanKeterlambatanMail($trx)); - $trx->late_notified = 1; } catch (\Exception $e) { \Log::error('Gagal mengirim email keterlambatan: ' . $e->getMessage()); } } - $trx->save(); } } - + // 3. Ambil semua transaksi termasuk kolom admin_feedback (Kode Asli Anda) $peminjaman = BorrowTransaction::with(['koleksi']) ->where('anggota_id', $anggota->id) - ->orderBy('tanggal_pinjam', 'desc') + ->latest() ->get() ->map(function ($item) { - if ($item->pickup_date) { - $item->pickup_date = Carbon::parse($item->pickup_date)->setTimezone('Asia/Jakarta'); - } - if ($item->tanggal_pinjam) { - $item->tanggal_pinjam = Carbon::parse($item->tanggal_pinjam)->setTimezone('Asia/Jakarta'); - } - if ($item->tanggal_kembali) { - $item->tanggal_kembali = Carbon::parse($item->tanggal_kembali)->setTimezone('Asia/Jakarta'); + // Konversi timezone untuk view + $fields = ['pickup_date', 'tanggal_pinjam', 'tanggal_kembali']; + foreach ($fields as $field) { + if ($item->$field) { + $item->$field = Carbon::parse($item->$field)->setTimezone('Asia/Jakarta'); + } } return $item; }); - return view('user.peminjaman.index', compact('peminjaman', 'anggota')); + return view('user.peminjaman.index', compact('peminjaman', 'anggota', 'blokir')); } - - public function create() + /** + * MENAMPILKAN FORM PERPANJANGAN (GET) + */ + public function showPerpanjangForm($id) { - $anggota = Auth::user()->anggota; + $peminjaman = BorrowTransaction::with(['koleksi', 'anggota'])->findOrFail($id); - if (!$anggota) { - return redirect()->route('user.member.create') - ->with('error', 'Silakan daftar anggota terlebih dahulu sebelum meminjam buku.'); + // Proteksi: Cek apakah benar milik user yang login + if ($peminjaman->anggota_id !== Auth::user()->anggota->id) { + abort(403); } - $koleksi = Koleksi::whereDoesntHave('borrowTransactions', function ($q) { - $q->whereIn('status', ['dipinjam', 'menunggu', 'disetujui']); - })->get(); + // Proteksi: Cek status & keterlambatan + if ($peminjaman->status !== 'dipinjam') { + return redirect()->route('user.peminjaman.index')->with('error', 'Status buku tidak valid untuk diperpanjang.'); + } - $lastTransaction = $anggota->borrowTransactions()->latest()->first(); + if ($peminjaman->tanggal_kembali && Carbon::now()->gt(Carbon::parse($peminjaman->tanggal_kembali))) { + return redirect()->route('user.peminjaman.index')->with('error', 'Buku terlambat tidak bisa diperpanjang secara mandiri.'); + } - return view('user.peminjaman.create', compact('anggota', 'koleksi', 'lastTransaction')); + if ($peminjaman->perpanjangan_diajukan) { + return redirect()->route('user.peminjaman.index')->with('error', 'Perpanjangan sudah pernah diajukan.'); + } + + return view('user.peminjaman.perpanjang', compact('peminjaman')); + } + + /** + * MEMPROSES PENGAJUAN PERPANJANGAN (POST) + */ + public function processPerpanjang(Request $request, $id) + { + $transaction = BorrowTransaction::findOrFail($id); + + // Validasi input file + $request->validate([ + 'surat_pengantar' => 'required|file|mimes:pdf|max:2048', + ]); + + // Cek keterlambatan lagi (security check) + if ($transaction->tanggal_kembali && Carbon::now()->gt(Carbon::parse($transaction->tanggal_kembali))) { + return redirect()->route('user.peminjaman.index')->with('error', 'Buku ini sudah terlambat.'); + } + + // Simpan File PDF baru + if ($request->hasFile('surat_pengantar')) { + $file = $request->file('surat_pengantar'); + $pdfFilename = 'perpanjang_' . time() . '_' . $file->getClientOriginalName(); + $file->storeAs('public/surat_pengantar', $pdfFilename); + + // Update transaksi + $transaction->status = 'perpanjangan_menunggu'; + $transaction->perpanjangan_diajukan = true; + $transaction->surat_pengantar = $pdfFilename; // Update dengan surat baru + $transaction->save(); + } + + // Kirim Email ke Admin + if (env('MAIL_USERNAME')) { + try { + Mail::to(config('mail.from.address')) + ->send(new PerpanjanganMenungguApproval($transaction)); + } catch (\Exception $e) { + \Log::error('Gagal kirim email perpanjangan: ' . $e->getMessage()); + } + } + + return redirect()->route('user.peminjaman.index') + ->with('success', 'Permintaan perpanjangan dan dokumen berhasil dikirim!'); } - public function store(Request $request) { $anggota = Auth::user()->anggota; - if (!$anggota) { - return redirect()->route('user.member.create') - ->with('error', 'Silakan daftar anggota terlebih dahulu sebelum meminjam buku.'); - } - - if ($anggota->status !== 'disetujui') { - return redirect()->route('user.peminjaman.create') - ->with('error', 'Anda belum dapat meminjam buku sebelum disetujui oleh admin.'); + if (!$anggota || $anggota->status !== 'disetujui') { + return back()->with('error', 'Status keanggotaan Anda belum diizinkan meminjam.'); } $request->validate([ @@ -115,20 +165,9 @@ public function store(Request $request) 'surat_pengantar' => 'nullable|file|mimes:pdf|max:2048', ]); - $alreadyBorrowed = BorrowTransaction::where('koleksi_id', $request->koleksi_id) - ->whereIn('status', ['menunggu', 'disetujui', 'dipinjam']) - ->exists(); - - if ($alreadyBorrowed) { - return back()->withErrors(['koleksi_id' => 'Buku ini sedang dalam proses peminjaman.']); - } - $pdfFilename = null; if ($request->hasFile('surat_pengantar')) { $file = $request->file('surat_pengantar'); - if (!Storage::exists('public/surat_pengantar')) { - Storage::makeDirectory('public/surat_pengantar'); - } $pdfFilename = time() . '_' . $file->getClientOriginalName(); $file->storeAs('public/surat_pengantar', $pdfFilename); } @@ -141,157 +180,52 @@ public function store(Request $request) 'surat_pengantar' => $pdfFilename, ]); - if (env('MAIL_USERNAME')) { - try { - Mail::send('emails.peminjaman_baru', [ - 'anggota' => $anggota, - 'transaction' => $transaction, - ], function ($message) { - $message->to(config('mail.from.address')) - ->subject('πŸ“š Pengajuan Peminjaman Buku Baru'); - }); - } catch (\Exception $e) { - \Log::error('Gagal mengirim email peminjaman ke admin: ' . $e->getMessage()); - } - } - - return redirect()->route('user.peminjaman.index') - ->with('success', 'Pengajuan peminjaman berhasil dikirim! Menunggu persetujuan admin.'); + return redirect()->route('user.peminjaman.index')->with('success', 'Pengajuan berhasil dikirim!'); } - public function kembalikan($id) { $transaction = BorrowTransaction::findOrFail($id); - if ($transaction->status != 'dipinjam') { - return redirect()->route('user.peminjaman.index') - ->with('error', 'Buku belum dapat dikembalikan atau sudah dikembalikan.'); + return back()->with('error', 'Buku belum dapat dikembalikan.'); } $transaction->markAsPengembalianMenunggu(); if (env('MAIL_USERNAME')) { try { - Mail::to(config('mail.from.address')) - ->send(new PengembalianMenungguApproval($transaction)); - } catch (\Exception $e) { - return back()->with('error', 'Gagal mengirim email ke admin: ' . $e->getMessage()); - } + Mail::to(config('mail.from.address'))->send(new PengembalianMenungguApproval($transaction)); + } catch (\Exception $e) {} } - return redirect()->route('user.peminjaman.index') - ->with('success', 'Permintaan pengembalian dikirim! Menunggu persetujuan admin.'); - } - - - public function ajukanPerpanjangan($id) - { - $transaction = BorrowTransaction::findOrFail($id); - - if ($transaction->status !== 'dipinjam') { - return redirect()->route('user.peminjaman.index') - ->with('error', 'Hanya buku yang sedang dipinjam yang bisa diperpanjang.'); - } - - if ($transaction->perpanjangan_diajukan) { - return back()->with('error', 'Anda sudah pernah mengajukan perpanjangan untuk buku ini.'); - } - - $transaction->status = 'perpanjangan_menunggu'; - $transaction->perpanjangan_diajukan = true; - $transaction->save(); - - if (env('MAIL_USERNAME')) { - try { - Mail::to(config('mail.from.address')) - ->send(new PerpanjanganMenungguApproval($transaction)); - } catch (\Exception $e) { - return back()->with('error', 'Gagal mengirim email perpanjangan: ' . $e->getMessage()); - } - } - - return redirect()->route('user.peminjaman.index') - ->with('success', 'Permintaan perpanjangan telah dikirim! Menunggu persetujuan admin.'); - } - - public function setujuiPerpanjangan($id) - { - $transaction = BorrowTransaction::findOrFail($id); - - if ($transaction->status !== 'perpanjangan_menunggu') { - return back()->with('error', 'Status transaksi tidak valid.'); - } - - $transaction->tanggal_kembali = Carbon::parse($transaction->tanggal_kembali)->addDays(7); - $transaction->status = 'dipinjam'; - $transaction->perpanjangan_disetujui = true; - $transaction->save(); - - return back()->with('success', 'Perpanjangan berhasil disetujui (7 hari).'); - } - - public function tolakPerpanjangan($id) - { - $transaction = BorrowTransaction::findOrFail($id); - - if ($transaction->status !== 'perpanjangan_menunggu') { - return back()->with('error', 'Status transaksi tidak valid.'); - } - - $transaction->status = 'dipinjam'; - $transaction->perpanjangan_disetujui = false; - $transaction->save(); - - return back()->with('success', 'Permintaan perpanjangan ditolak.'); + return redirect()->route('user.peminjaman.index')->with('success', 'Permintaan pengembalian dikirim!'); } + // Fungsi Rating & Penilaian public function penilaian($id) { $transaction = BorrowTransaction::findOrFail($id); - - if ($transaction->status !== 'dikembalikan') { - return redirect()->route('user.peminjaman.index') - ->with('error', 'Pengembalian belum disetujui admin.'); - } - return view('user.rating.form', compact('transaction')); } public function simpanPenilaian(Request $request, $id) { - $transaction = BorrowTransaction::findOrFail($id); - - $request->validate([ - 'rating' => 'required|integer|min:1|max:5', - 'feedback' => 'nullable|string|max:500', - ]); - + $request->validate(['rating' => 'required|integer|min:1|max:5']); Rating::create([ 'user_id' => Auth::id(), - 'borrow_transaction_id' => $transaction->id, + 'borrow_transaction_id' => $id, 'rating' => $request->rating, 'feedback' => $request->feedback, ]); - - return redirect()->route('user.peminjaman.index') - ->with('success', 'Terima kasih atas penilaian Anda!'); + return redirect()->route('user.peminjaman.index')->with('success', 'Terima kasih!'); } public function downloadSuratPengantar($id) { $transaction = BorrowTransaction::findOrFail($id); - - if (!$transaction->surat_pengantar) { - return back()->with('error', 'Surat pengantar tidak ditemukan.'); - } - $path = 'public/surat_pengantar/' . $transaction->surat_pengantar; - - if (!Storage::exists($path)) { - return back()->with('error', 'File surat pengantar tidak ditemukan di server.'); - } - + if (!Storage::exists($path)) return back()->with('error', 'File tidak ditemukan.'); return Storage::download($path); } } + diff --git a/app/Http/Controllers/User/UserProfileController.php b/app/Http/Controllers/User/UserProfileController.php index e4d5a5b..a7f4fb3 100644 --- a/app/Http/Controllers/User/UserProfileController.php +++ b/app/Http/Controllers/User/UserProfileController.php @@ -1,15 +1,18 @@ find(Auth::id()); return view('profile', compact('user')); } @@ -43,4 +46,5 @@ public function updatePassword(Request $request) { return redirect()->route('user.profile')->with('success', 'Password berhasil diperbarui!'); } -} \ No newline at end of file +} + diff --git a/app/Http/Controllers/User/UserRatingController.php b/app/Http/Controllers/User/UserRatingController.php index 22bc1cd..35ea857 100644 --- a/app/Http/Controllers/User/UserRatingController.php +++ b/app/Http/Controllers/User/UserRatingController.php @@ -55,3 +55,4 @@ public function store(Request $request, $id) ->with('success', 'Terima kasih! Penilaian Anda telah kami terima πŸ™'); } } + diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 2ddf7cd..8ed702e 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -30,3 +30,4 @@ public function anggota() return view('user.anggota'); } } + diff --git a/app/Http/Controllers/UserDashboardController.php b/app/Http/Controllers/UserDashboardController.php index cb0bba1..dc05181 100644 --- a/app/Http/Controllers/UserDashboardController.php +++ b/app/Http/Controllers/UserDashboardController.php @@ -11,3 +11,4 @@ public function index() return view('user.dashboard'); } } + diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index a5213da..b34673f 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -6,8 +6,13 @@ class Kernel extends HttpKernel { - + /** + * The application's global HTTP middleware stack. + * + * These middleware are run during every request to your application. + */ protected $middleware = [ + // TrustProxies sangat penting agar Ngrok bisa mengirimkan header HTTPS dengan benar \App\Http\Middleware\TrustProxies::class, \Illuminate\Http\Middleware\HandleCors::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class, @@ -16,6 +21,9 @@ class Kernel extends HttpKernel \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, ]; + /** + * The application's middleware groups. + */ protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, @@ -28,15 +36,24 @@ class Kernel extends HttpKernel ], 'api' => [ - 'throttle:api', + // 'throttle:api', // DINONAKTIFKAN: Agar scan barcode & polling via Ngrok tidak terkena limit (Error 429) \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; - + /** + * The application's route middleware. + * + * These middleware may be assigned to groups or used individually. + */ protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ]; } + diff --git a/app/Http/Middleware/AdminMiddleware.php b/app/Http/Middleware/AdminMiddleware.php index 218a559..3a4bab3 100644 --- a/app/Http/Middleware/AdminMiddleware.php +++ b/app/Http/Middleware/AdminMiddleware.php @@ -16,3 +16,4 @@ public function handle(Request $request, Closure $next) abort(403, 'Akses ditolak!'); } } + diff --git a/app/Http/Middleware/BypassNgro.php b/app/Http/Middleware/BypassNgro.php new file mode 100644 index 0000000..870bb53 --- /dev/null +++ b/app/Http/Middleware/BypassNgro.php @@ -0,0 +1,29 @@ +header('ngrok-skip-browser-warning', 'true'); + } else { + // Backup jika response adalah instance Symfony Response biasa + $response->headers->set('ngrok-skip-browser-warning', 'true'); + } + + return $response; + } +} + diff --git a/app/Http/Middleware/CheckRole.php b/app/Http/Middleware/CheckRole.php index 5de8af4..09ed7e6 100644 --- a/app/Http/Middleware/CheckRole.php +++ b/app/Http/Middleware/CheckRole.php @@ -25,4 +25,5 @@ public function handle(Request $request, Closure $next, string $role): Response return $next($request); } -} \ No newline at end of file +} + diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php index 997a7e7..d235ebd 100644 --- a/app/Http/Middleware/SetLocale.php +++ b/app/Http/Middleware/SetLocale.php @@ -15,3 +15,4 @@ public function handle(Request $request, Closure $next): Response return $next($request); } } + diff --git a/app/Http/Middleware/SkipNgrokWarning.php b/app/Http/Middleware/SkipNgrokWarning.php new file mode 100644 index 0000000..ad188b5 --- /dev/null +++ b/app/Http/Middleware/SkipNgrokWarning.php @@ -0,0 +1,27 @@ +header('ngrok-skip-browser-warning', 'any_value'); + } + + return $response; + } +} + diff --git a/app/Jobs/ProcessBorrowTransaction.php b/app/Jobs/ProcessBorrowTransaction.php new file mode 100644 index 0000000..5b0deac --- /dev/null +++ b/app/Jobs/ProcessBorrowTransaction.php @@ -0,0 +1,34 @@ +data = $data; + } + + public function handle(): void + { + BorrowTransaction::create([ + 'anggota_id' => $this->data['anggota_id'], + 'koleksi_id' => $this->data['koleksi_id'], + 'tanggal_pinjam' => now(), + 'tanggal_kembali' => now()->addDays(7), + 'status' => 'dipinjam' + ]); + } +} + diff --git a/app/Jobs/ProcessPeminjamanJob.php b/app/Jobs/ProcessPeminjamanJob.php new file mode 100644 index 0000000..ad17f01 --- /dev/null +++ b/app/Jobs/ProcessPeminjamanJob.php @@ -0,0 +1,186 @@ +bookId = $bookId; + $this->userId = $userId; + } + + public function handle(): void + { + try { + + /* + |-------------------------------------------------------------------------- + | Ambil Data Buku + |-------------------------------------------------------------------------- + */ + + $book = DB::table('koleksis') + ->where('biblio_id', $this->bookId) + ->first(); + + if (!$book) { + Log::error('Buku tidak ditemukan'); + return; + } + + $judulBuku = $book->judul_koleksi + ?? $book->title + ?? 'Buku'; + + /* + |-------------------------------------------------------------------------- + | Cegah Double Peminjaman + |-------------------------------------------------------------------------- + */ + + $existing = DB::table('borrow_transactions') + ->where('koleksi_id', $this->bookId) + ->where('status', 'dipinjam') + ->first(); + + if ($existing) { + + Log::warning('Buku sudah dipinjam'); + + $this->publishMqtt('rfid/status', 'failed'); + + return; + } + + /* + |-------------------------------------------------------------------------- + | Insert Transaksi + |-------------------------------------------------------------------------- + */ + + DB::table('borrow_transactions')->insert([ + 'anggota_id' => $this->userId, + 'koleksi_id' => $this->bookId, + 'tanggal_pinjam' => now(), + 'tanggal_kembali' => now()->addDays(7), + 'status' => 'dipinjam', + 'created_at' => now(), + 'updated_at' => now() + ]); + + /* + |-------------------------------------------------------------------------- + | Update State IoT + |-------------------------------------------------------------------------- + */ + + IotMode::where('id', 1)->update([ + 'mode' => 'success_loan' + ]); + + /* + |-------------------------------------------------------------------------- + | MQTT Publish + |-------------------------------------------------------------------------- + */ + + $this->publishMqtt('rfid/status', 'finish'); + + $this->publishMqtt( + 'peminjaman/user/' . $this->userId, + [ + 'status' => 'dipinjam', + 'message' => "Buku [$judulBuku] berhasil dipinjam" + ] + ); + + /* + |-------------------------------------------------------------------------- + | Reset ke Standby + |-------------------------------------------------------------------------- + */ + + sleep(2); + + IotMode::where('id', 1)->update([ + 'mode' => 'standby', + 'book_id' => null, + 'user_id' => null + ]); + + $this->publishMqtt('rfid/mode', 'standby'); + $this->publishMqtt('rfid/status', 'idle'); + + Log::info('Peminjaman berhasil diproses via queue'); + + } catch (\Exception $e) { + + Log::error('Queue Peminjaman Error: ' . $e->getMessage()); + + IotMode::where('id', 1)->update([ + 'mode' => 'wait' + ]); + + $this->publishMqtt('rfid/status', 'failed'); + } + } + + /* + |-------------------------------------------------------------------------- + | Helper MQTT + |-------------------------------------------------------------------------- + */ + + private function publishMqtt($topic, $message): void + { + try { + + $mqtt = new MqttClient( + '127.0.0.1', + 1883, + 'laravel-job-' . uniqid() + ); + + $mqtt->connect(); + + $payload = is_array($message) + ? json_encode($message) + : (string) $message; + + $mqtt->publish($topic, $payload, 0); + + $mqtt->disconnect(); + + } catch (\Exception $e) { + + Log::error('MQTT Publish Error: ' . $e->getMessage()); + } + } +} + diff --git a/app/Jobs/ProcessPengembalianJob.php b/app/Jobs/ProcessPengembalianJob.php new file mode 100644 index 0000000..9b985f2 --- /dev/null +++ b/app/Jobs/ProcessPengembalianJob.php @@ -0,0 +1,72 @@ +data = $data; + } + + public function handle(): void + { + try { + + $bookId = $this->data['book_id']; + $userId = $this->data['user_id']; + + DB::table('borrow_transactions') + ->where('koleksi_id', $bookId) + ->where('status', 'dipinjam') + ->update([ + 'status' => 'dikembalikan', + 'updated_at' => now() + ]); + + // reset mode + IotMode::where('id', 1)->update([ + 'mode' => 'standby', + 'book_id' => null, + 'user_id' => null + ]); + + $mqtt = new MqttClient( + '127.0.0.1', + 1883, + 'job-kembali-' . uniqid() + ); + + $mqtt->connect(); + + $mqtt->publish('rfid/status', 'finish', 0); + $mqtt->publish('rfid/mode', 'standby', 0); + + $mqtt->disconnect(); + + Log::info('Pengembalian berhasil'); + + } catch (\Throwable $e) { + + Log::error( + 'Queue Pengembalian Error: ' . + $e->getMessage() + ); + } + } +} + diff --git a/app/Livewire/Admin/NotificationBadges.php b/app/Livewire/Admin/NotificationBadges.php new file mode 100644 index 0000000..e404b38 --- /dev/null +++ b/app/Livewire/Admin/NotificationBadges.php @@ -0,0 +1,55 @@ +loadData(); + } + + public function loadData() + { + // ========================= + // CACHE MEMBERS + // ========================= + $this->pendingMembers = Cache::remember('notif_pending_members', 10, function () { + return Anggota::whereIn('status', ['pending', 'menunggu'])->count(); + }); + + // ========================= + // CACHE LOANS + // ========================= + $this->pendingLoans = Cache::remember('notif_pending_loans', 10, function () { + return BorrowTransaction::whereIn('status', [ + 'menunggu', + 'pengembalian_menunggu', + 'perpanjangan_menunggu' + ])->count(); + }); + } + + public function refreshData() + { + $this->loadData(); + } + + public function render() + { + return view('livewire.admin.notification-badges', [ + 'pendingMembers' => $this->pendingMembers, + 'pendingLoans' => $this->pendingLoans, + ]); + } +} + diff --git a/app/Mail/AdminNotifikasiAnggotaBaru.php b/app/Mail/AdminNotifikasiAnggotaBaru.php index be36e55..60dd7d6 100644 --- a/app/Mail/AdminNotifikasiAnggotaBaru.php +++ b/app/Mail/AdminNotifikasiAnggotaBaru.php @@ -24,3 +24,4 @@ public function build() ->markdown('emails.admin.anggota_baru'); } } + diff --git a/app/Mail/AnggotaBaruMenungguPersetujuan.php b/app/Mail/AnggotaBaruMenungguPersetujuan.php index bbc98bf..9833322 100644 --- a/app/Mail/AnggotaBaruMenungguPersetujuan.php +++ b/app/Mail/AnggotaBaruMenungguPersetujuan.php @@ -30,3 +30,4 @@ public function build() ->view('emails.anggota_baru_menunggu'); } } + diff --git a/app/Mail/BukuDikembalikanMail.php b/app/Mail/BukuDikembalikanMail.php index 5ac7aad..c67a4a1 100644 --- a/app/Mail/BukuDikembalikanMail.php +++ b/app/Mail/BukuDikembalikanMail.php @@ -27,3 +27,4 @@ public function build() ]); } } + diff --git a/app/Mail/BukuSiapDiambilMail.php b/app/Mail/BukuSiapDiambilMail.php index a81a0a9..4c95091 100644 --- a/app/Mail/BukuSiapDiambilMail.php +++ b/app/Mail/BukuSiapDiambilMail.php @@ -23,3 +23,4 @@ public function build() ->view('emails.buku_siap_diambil'); } } + diff --git a/app/Mail/JatuhTempoMail.php b/app/Mail/JatuhTempoMail.php new file mode 100644 index 0000000..ef3ae69 --- /dev/null +++ b/app/Mail/JatuhTempoMail.php @@ -0,0 +1,22 @@ +transaction = $transaction; + } + + public function build() + { + return $this->subject('Pengingat Jatuh Tempo Buku') + ->view('emails.jatuh_tempo'); + } +} + diff --git a/app/Mail/PeminjamanDitolakMail.php b/app/Mail/PeminjamanDitolakMail.php index d0b1055..f4f280b 100644 --- a/app/Mail/PeminjamanDitolakMail.php +++ b/app/Mail/PeminjamanDitolakMail.php @@ -23,3 +23,4 @@ public function build() ->view('emails.peminjaman_ditolak'); } } + diff --git a/app/Mail/PeminjamanMenungguApproval.php b/app/Mail/PeminjamanMenungguApproval.php index 769353b..fe92fb9 100644 --- a/app/Mail/PeminjamanMenungguApproval.php +++ b/app/Mail/PeminjamanMenungguApproval.php @@ -26,3 +26,4 @@ public function build() ->view('emails.peminjaman_menunggu'); } } + diff --git a/app/Mail/PengembalianMenungguApproval.php b/app/Mail/PengembalianMenungguApproval.php index 9157092..3157195 100644 --- a/app/Mail/PengembalianMenungguApproval.php +++ b/app/Mail/PengembalianMenungguApproval.php @@ -24,3 +24,4 @@ public function build() ->view('emails.pengembalian-menunggu'); } } + diff --git a/app/Mail/PeringatanKeterlambatanMail.php b/app/Mail/PeringatanKeterlambatanMail.php index 37c8be2..a9b3b57 100644 --- a/app/Mail/PeringatanKeterlambatanMail.php +++ b/app/Mail/PeringatanKeterlambatanMail.php @@ -23,3 +23,4 @@ public function build() ->markdown('emails.peringatan'); } } + diff --git a/app/Mail/PerpanjanganDisetujuiMail.php b/app/Mail/PerpanjanganDisetujuiMail.php index e265ce5..2414350 100644 --- a/app/Mail/PerpanjanganDisetujuiMail.php +++ b/app/Mail/PerpanjanganDisetujuiMail.php @@ -24,3 +24,4 @@ public function build() ->view('emails.perpanjangan_disetujui'); } } + diff --git a/app/Mail/PerpanjanganDitolakMail.php b/app/Mail/PerpanjanganDitolakMail.php index fe54f97..9af4c16 100644 --- a/app/Mail/PerpanjanganDitolakMail.php +++ b/app/Mail/PerpanjanganDitolakMail.php @@ -24,3 +24,4 @@ public function build() ->view('emails.perpanjangan_ditolak'); } } + diff --git a/app/Mail/PerpanjanganMenungguApproval.php b/app/Mail/PerpanjanganMenungguApproval.php index 3e1c57b..ed264c8 100644 --- a/app/Mail/PerpanjanganMenungguApproval.php +++ b/app/Mail/PerpanjanganMenungguApproval.php @@ -24,3 +24,4 @@ public function build() ->view('emails.perpanjangan_menunggu'); } } + diff --git a/app/Mail/SendOtpMail.php b/app/Mail/SendOtpMail.php index 9dd4cf6..e90f522 100644 --- a/app/Mail/SendOtpMail.php +++ b/app/Mail/SendOtpMail.php @@ -31,3 +31,4 @@ public function build() ]); } } + diff --git a/app/Mail/StatusAnggotaDiperbarui.php b/app/Mail/StatusAnggotaDiperbarui.php index c16355c..b4a8f09 100644 --- a/app/Mail/StatusAnggotaDiperbarui.php +++ b/app/Mail/StatusAnggotaDiperbarui.php @@ -24,3 +24,4 @@ public function build() ->view('emails.status_anggota_diperbarui'); } } + diff --git a/app/Models/Anggota.php b/app/Models/Anggota.php index 9f67d0c..26df9f2 100644 --- a/app/Models/Anggota.php +++ b/app/Models/Anggota.php @@ -15,12 +15,13 @@ class Anggota extends Model protected $fillable = [ 'nama', + 'gender', 'email', 'alamat', 'no_hp', 'user_id', 'status', - 'nip_nim', + 'nik_nip', 'foto', ]; @@ -43,9 +44,9 @@ public function getFormattedNameAttribute() } - public function getFormattedNipNimAttribute() + public function getFormattedNikNipAttribute() { - return $this->nip_nim ? strtoupper($this->nip_nim) : null; + return $this->nik_nip ? strtoupper($this->nik_nip) : null; } @@ -56,3 +57,4 @@ public function hasActiveBorrow() ->exists(); } } + diff --git a/app/Models/Book.php b/app/Models/Book.php index 5178a88..a6999d4 100644 --- a/app/Models/Book.php +++ b/app/Models/Book.php @@ -37,3 +37,4 @@ public function getFormattedTitleAttribute() return ucwords(strtolower($this->judul)); } } + diff --git a/app/Models/BorrowTransaction.php b/app/Models/BorrowTransaction.php index 2c3a3cb..56ff92a 100644 --- a/app/Models/BorrowTransaction.php +++ b/app/Models/BorrowTransaction.php @@ -108,11 +108,17 @@ public function markAsDitolak() } public function markAsDikembalikan() - { - $this->status = 'dikembalikan'; - $this->tanggal_kembali = Carbon::today(); - $this->save(); - } +{ + // Menggunakan update langsung ke database berdasarkan ID target + $this->where('id', $this->id)->update([ + 'status' => 'dikembalikan', + 'tanggal_kembali' => \Carbon\Carbon::today() + ]); + + // Sinkronisasi data object yang sedang aktif di memory + $this->status = 'dikembalikan'; + $this->tanggal_kembali = \Carbon\Carbon::today(); +} public function markAsPengembalianMenunggu() { @@ -231,3 +237,4 @@ public static function boot() }); } } + diff --git a/app/Models/Fasilitas.php b/app/Models/Fasilitas.php index 095413f..c16617c 100644 --- a/app/Models/Fasilitas.php +++ b/app/Models/Fasilitas.php @@ -15,3 +15,4 @@ class Fasilitas extends Model 'deskripsi', ]; } + diff --git a/app/Models/IotMode.php b/app/Models/IotMode.php new file mode 100644 index 0000000..f01472b --- /dev/null +++ b/app/Models/IotMode.php @@ -0,0 +1,16 @@ +barcode && Storage::disk('public')->exists($this->barcode); } } + diff --git a/app/Models/Member.php b/app/Models/Member.php index 9919c99..5ca218f 100644 --- a/app/Models/Member.php +++ b/app/Models/Member.php @@ -9,16 +9,9 @@ class Member extends Model { use HasFactory; - protected $fillable = [ - 'nama', - 'email', - 'alamat', - 'user_id', - ]; + // TAMBAHKAN BARIS INI AGAR LARAVEL MEMBACA TABEL ANGGOTA + protected $table = 'anggota'; - public $timestamps = true; - - public function user() { - return $this->belongsTo(User::class); - } + // Kode isi model kamu yang lain tetap biarkan di bawah ini... } + diff --git a/app/Models/Peminjaman.php b/app/Models/Peminjaman.php index 1598bff..66f068e 100644 --- a/app/Models/Peminjaman.php +++ b/app/Models/Peminjaman.php @@ -29,3 +29,4 @@ public function koleksi() return $this->belongsTo(Koleksi::class); } } + diff --git a/app/Models/PublishPlace.php b/app/Models/PublishPlace.php index 83c8efd..5ab69c5 100644 --- a/app/Models/PublishPlace.php +++ b/app/Models/PublishPlace.php @@ -8,3 +8,4 @@ class PublishPlace extends Model { // } + diff --git a/app/Models/Publisher.php b/app/Models/Publisher.php index 1e4a0fd..655e98c 100644 --- a/app/Models/Publisher.php +++ b/app/Models/Publisher.php @@ -8,4 +8,5 @@ class Publisher extends Model { protected $guarded = ['id']; -} \ No newline at end of file +} + diff --git a/app/Models/Rating.php b/app/Models/Rating.php index d0de93d..e3bab15 100644 --- a/app/Models/Rating.php +++ b/app/Models/Rating.php @@ -26,3 +26,4 @@ public function user() return $this->belongsTo(User::class); } } + diff --git a/app/Models/Setting.php b/app/Models/Setting.php index bc6d2b4..cfb1a5e 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -12,3 +12,4 @@ class Setting extends Model 'logo', ]; } + diff --git a/app/Models/User.php b/app/Models/User.php index 1e72036..66a3838 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,12 +9,12 @@ use Filament\Models\Contracts\FilamentUser; use Filament\Panel; use App\Models\UserLogin; +use Illuminate\Support\Carbon; class User extends Authenticatable implements FilamentUser, MustVerifyEmail { use HasFactory, Notifiable; - protected $fillable = [ 'name', 'email', @@ -28,14 +28,12 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail 'email_verified_at', ]; - protected $hidden = [ 'password', 'remember_token', 'email_otp', ]; - protected $casts = [ 'email_verified_at' => 'datetime', 'email_otp_expires_at' => 'datetime', @@ -44,39 +42,56 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail 'password' => 'hashed', ]; - public function canAccessPanel(Panel $panel): bool { return true; } - public function logins() { return $this->hasMany(UserLogin::class); } - public function transactions() { return $this->hasMany(\App\Models\BorrowTransaction::class, 'anggota_id'); } - public function anggota() { return $this->hasOne(\App\Models\Anggota::class, 'user_id'); } - public function getRecentLogins($perPage = 10) { return $this->logins()->latest()->paginate($perPage); } - + // ============================== + // πŸ”₯ EMAIL VERIFICATION (WAJIB) + // ============================== + + public function hasVerifiedEmail() + { + return !is_null($this->email_verified_at); + } + + public function markEmailAsVerified() + { + if ($this->hasVerifiedEmail()) { + return true; + } + + $this->forceFill([ + 'email_verified_at' => Carbon::now(), + ])->save(); + + return true; + } + public function sendEmailVerificationNotification() { $this->notify(new \Illuminate\Auth\Notifications\VerifyEmail()); } } + diff --git a/app/Models/UserLogin.php b/app/Models/UserLogin.php index 5c93a65..b72df9d 100644 --- a/app/Models/UserLogin.php +++ b/app/Models/UserLogin.php @@ -76,3 +76,4 @@ public static function detectLocation() return null; } } + diff --git a/app/Notifications/PerpanjanganPeminjamanNotification.php b/app/Notifications/PerpanjanganPeminjamanNotification.php index 78b63dc..d273919 100644 --- a/app/Notifications/PerpanjanganPeminjamanNotification.php +++ b/app/Notifications/PerpanjanganPeminjamanNotification.php @@ -46,3 +46,4 @@ public function toArray(object $notifiable): array ]; } } + diff --git a/app/Observers/IotModeObserver.php b/app/Observers/IotModeObserver.php new file mode 100644 index 0000000..537b37c --- /dev/null +++ b/app/Observers/IotModeObserver.php @@ -0,0 +1,72 @@ +info("Observer updated: ".$iotMode->mode); + + $this->publish($iotMode); +} + + public function created(IotMode $iotMode): void + { + $this->publish($iotMode); + } + + /** + * ========================= + * MQTT PUBLISH (FIX FINAL) + * ========================= + */ + private function publish(IotMode $iotMode): void + { + try { + $mqtt = new MqttClient( + '127.0.0.1', + 1883, + 'observer-' . uniqid() + ); + + // πŸ”₯ CONNECTION SETTINGS (STABIL) + $connectionSettings = (new ConnectionSettings) + ->setKeepAliveInterval(60) + ->setConnectTimeout(3); + + $mqtt->connect($connectionSettings, true); + + // πŸ”₯ PAYLOAD FINAL (STANDARIZED) + $payload = json_encode([ + 'mode' => $iotMode->mode, + 'book_id' => $iotMode->book_id, + 'ts' => now()->timestamp, + 'source' => 'observer' + ]); + + // πŸ”₯ PUBLISH REALTIME (IMPORTANT FIX) + $mqtt->publish( + 'rfid/mode', + $payload, + 1, // QoS 1 (anti loss) + false // RETAIN (INI WAJIB BIAR ESP32 TIDAK KETINGGALAN) + ); + + $mqtt->disconnect(); + + } catch (\Exception $e) { + logger()->error("MQTT Observer Error: " . $e->getMessage()); + } + } +} + diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 400f5e3..48b6632 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,18 +3,51 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use App\Models\IotMode; +use App\Observers\IotModeObserver; +use Illuminate\Support\Facades\URL; +use Illuminate\Support\Facades\Request; // <-- Tambahkan ini untuk deteksi host class AppServiceProvider extends ServiceProvider { - + /** + * Register any application services. + */ public function register(): void { // } - + /** + * Bootstrap any application services. + */ public function boot(): void { - // + /** + * 1. DINAMIS URL & HTTPS (SOLUSI DUA URL: LOCAL & NGROK) + * Logika ini mendeteksi jika aplikasi dibuka via Ngrok, maka otomatis + * menggunakan HTTPS dan URL Ngrok. Jika dibuka via Localhost, tetap HTTP. + */ + $host = Request::header('host'); + + if (str_contains($host, 'ngrok-free.dev')) { + // Jika akses lewat Ngrok + URL::forceScheme('https'); + URL::forceRootUrl("https://" . $host); + } else { + // Jika akses lewat Localhost / IP 127.0.0.1 + URL::forceScheme('http'); + // Jika Anda menggunakan port khusus (misal 8000), ini akan otomatis mengikutinya + URL::forceRootUrl("http://" . $host); + } + + /** + * 2. REGISTRASI OBSERVER IOT MODE + * Memastikan perubahan status pada tabel IotMode terpantau untuk sinkronisasi hardware + */ + if (class_exists(IotMode::class)) { + IotMode::observe(IotModeObserver::class); + } } } + diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 5038d0c..dcafc83 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -56,3 +56,4 @@ public function panel(Panel $panel): Panel ]); } } + diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 8be715d..8c44553 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -33,3 +33,4 @@ public function boot(): void }); } } + diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..e89dde8 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,12 +7,23 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', + channels: __DIR__.'/../routes/channels.php', health: '/up', ) - ->withMiddleware(function (Middleware $middleware): void { - // + ->withMiddleware(function (Middleware $middleware) { + // Menggunakan string fully qualified class name untuk menghindari error saat compile + // Pastikan file ada di app/Http/Middleware/BypassNgrok.php + if (class_exists(\App\Http\Middleware\BypassNgrok::class)) { + $middleware->append(\App\Http\Middleware\BypassNgrok::class); + } + + // πŸ”₯ SOLUSI TOTAL: Buang semua kata 'guest:' dan 'authenticated:' + // Parameter ke-1 otomatis dibaca sebagai 'guest', parameter ke-2 sebagai 'authenticated' + $middleware->redirectTo('/login', '/dashboard'); }) - ->withExceptions(function (Exceptions $exceptions): void { + ->withExceptions(function (Exceptions $exceptions) { // })->create(); + diff --git a/bootstrap/cache/pac5DA9.tmp b/bootstrap/cache/pac5DA9.tmp new file mode 100644 index 0000000..74cf827 --- /dev/null +++ b/bootstrap/cache/pac5DA9.tmp @@ -0,0 +1,196 @@ + + array ( + 'aliases' => + array ( + 'EloquentSerialize' => 'AnourValar\\EloquentSerialize\\Facades\\EloquentSerializeFacade', + ), + ), + 'barryvdh/laravel-dompdf' => + array ( + 'aliases' => + array ( + 'PDF' => 'Barryvdh\\DomPDF\\Facade\\Pdf', + 'Pdf' => 'Barryvdh\\DomPDF\\Facade\\Pdf', + ), + 'providers' => + array ( + 0 => 'Barryvdh\\DomPDF\\ServiceProvider', + ), + ), + 'blade-ui-kit/blade-heroicons' => + array ( + 'providers' => + array ( + 0 => 'BladeUI\\Heroicons\\BladeHeroiconsServiceProvider', + ), + ), + 'blade-ui-kit/blade-icons' => + array ( + 'providers' => + array ( + 0 => 'BladeUI\\Icons\\BladeIconsServiceProvider', + ), + ), + 'filament/actions' => + array ( + 'providers' => + array ( + 0 => 'Filament\\Actions\\ActionsServiceProvider', + ), + ), + 'filament/filament' => + array ( + 'providers' => + array ( + 0 => 'Filament\\FilamentServiceProvider', + ), + ), + 'filament/forms' => + array ( + 'providers' => + array ( + 0 => 'Filament\\Forms\\FormsServiceProvider', + ), + ), + 'filament/infolists' => + array ( + 'providers' => + array ( + 0 => 'Filament\\Infolists\\InfolistsServiceProvider', + ), + ), + 'filament/notifications' => + array ( + 'providers' => + array ( + 0 => 'Filament\\Notifications\\NotificationsServiceProvider', + ), + ), + 'filament/support' => + array ( + 'providers' => + array ( + 0 => 'Filament\\Support\\SupportServiceProvider', + ), + ), + 'filament/tables' => + array ( + 'providers' => + array ( + 0 => 'Filament\\Tables\\TablesServiceProvider', + ), + ), + 'filament/widgets' => + array ( + 'providers' => + array ( + 0 => 'Filament\\Widgets\\WidgetsServiceProvider', + ), + ), + 'kirschbaum-development/eloquent-power-joins' => + array ( + 'providers' => + array ( + 0 => 'Kirschbaum\\PowerJoins\\PowerJoinsServiceProvider', + ), + ), + 'laravel/pail' => + array ( + 'providers' => + array ( + 0 => 'Laravel\\Pail\\PailServiceProvider', + ), + ), + 'laravel/sail' => + array ( + 'providers' => + array ( + 0 => 'Laravel\\Sail\\SailServiceProvider', + ), + ), + 'laravel/socialite' => + array ( + 'aliases' => + array ( + 'Socialite' => 'Laravel\\Socialite\\Facades\\Socialite', + ), + 'providers' => + array ( + 0 => 'Laravel\\Socialite\\SocialiteServiceProvider', + ), + ), + 'laravel/tinker' => + array ( + 'providers' => + array ( + 0 => 'Laravel\\Tinker\\TinkerServiceProvider', + ), + ), + 'livewire/livewire' => + array ( + 'aliases' => + array ( + 'Livewire' => 'Livewire\\Livewire', + ), + 'providers' => + array ( + 0 => 'Livewire\\LivewireServiceProvider', + ), + ), + 'milon/barcode' => + array ( + 'aliases' => + array ( + 'DNS1D' => 'Milon\\Barcode\\Facades\\DNS1DFacade', + 'DNS2D' => 'Milon\\Barcode\\Facades\\DNS2DFacade', + ), + 'providers' => + array ( + 0 => 'Milon\\Barcode\\BarcodeServiceProvider', + ), + ), + 'nesbot/carbon' => + array ( + 'providers' => + array ( + 0 => 'Carbon\\Laravel\\ServiceProvider', + ), + ), + 'nunomaduro/collision' => + array ( + 'providers' => + array ( + 0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', + ), + ), + 'nunomaduro/termwind' => + array ( + 'providers' => + array ( + 0 => 'Termwind\\Laravel\\TermwindServiceProvider', + ), + ), + 'ryangjchandler/blade-capture-directive' => + array ( + 'aliases' => + array ( + 'BladeCaptureDirective' => 'RyanChandler\\BladeCaptureDirective\\Facades\\BladeCaptureDirective', + ), + 'providers' => + array ( + 0 => 'RyanChandler\\BladeCaptureDirective\\BladeCaptureDirectiveServiceProvider', + ), + ), + 'simplesoftwareio/simple-qrcode' => + array ( + 'aliases' => + array ( + 'QrCode' => 'SimpleSoftwareIO\\QrCode\\Facades\\QrCode', + ), + 'providers' => + array ( + 0 => 'SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider', + ), + ), +); \ No newline at end of file diff --git a/bootstrap/cache/packages.php b/bootstrap/cache/packages.php index 74cf827..a93e5c8 100644 --- a/bootstrap/cache/packages.php +++ b/bootstrap/cache/packages.php @@ -102,6 +102,14 @@ 0 => 'Laravel\\Pail\\PailServiceProvider', ), ), + 'laravel/reverb' => + array ( + 'providers' => + array ( + 0 => 'Laravel\\Reverb\\ApplicationManagerServiceProvider', + 1 => 'Laravel\\Reverb\\ReverbServiceProvider', + ), + ), 'laravel/sail' => array ( 'providers' => @@ -193,4 +201,5 @@ 0 => 'SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider', ), ), -); \ No newline at end of file +); + diff --git a/bootstrap/cache/ser864F.tmp b/bootstrap/cache/ser864F.tmp new file mode 100644 index 0000000..ce37bb3 --- /dev/null +++ b/bootstrap/cache/ser864F.tmp @@ -0,0 +1,297 @@ + + array ( + 0 => 'Illuminate\\Auth\\AuthServiceProvider', + 1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', + 2 => 'Illuminate\\Bus\\BusServiceProvider', + 3 => 'Illuminate\\Cache\\CacheServiceProvider', + 4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 5 => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider', + 6 => 'Illuminate\\Cookie\\CookieServiceProvider', + 7 => 'Illuminate\\Database\\DatabaseServiceProvider', + 8 => 'Illuminate\\Encryption\\EncryptionServiceProvider', + 9 => 'Illuminate\\Filesystem\\FilesystemServiceProvider', + 10 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider', + 11 => 'Illuminate\\Hashing\\HashServiceProvider', + 12 => 'Illuminate\\Mail\\MailServiceProvider', + 13 => 'Illuminate\\Notifications\\NotificationServiceProvider', + 14 => 'Illuminate\\Pagination\\PaginationServiceProvider', + 15 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider', + 16 => 'Illuminate\\Pipeline\\PipelineServiceProvider', + 17 => 'Illuminate\\Queue\\QueueServiceProvider', + 18 => 'Illuminate\\Redis\\RedisServiceProvider', + 19 => 'Illuminate\\Session\\SessionServiceProvider', + 20 => 'Illuminate\\Translation\\TranslationServiceProvider', + 21 => 'Illuminate\\Validation\\ValidationServiceProvider', + 22 => 'Illuminate\\View\\ViewServiceProvider', + 23 => 'Barryvdh\\DomPDF\\ServiceProvider', + 24 => 'BladeUI\\Heroicons\\BladeHeroiconsServiceProvider', + 25 => 'BladeUI\\Icons\\BladeIconsServiceProvider', + 26 => 'Filament\\Actions\\ActionsServiceProvider', + 27 => 'Filament\\FilamentServiceProvider', + 28 => 'Filament\\Forms\\FormsServiceProvider', + 29 => 'Filament\\Infolists\\InfolistsServiceProvider', + 30 => 'Filament\\Notifications\\NotificationsServiceProvider', + 31 => 'Filament\\Support\\SupportServiceProvider', + 32 => 'Filament\\Tables\\TablesServiceProvider', + 33 => 'Filament\\Widgets\\WidgetsServiceProvider', + 34 => 'Kirschbaum\\PowerJoins\\PowerJoinsServiceProvider', + 35 => 'Laravel\\Pail\\PailServiceProvider', + 36 => 'Laravel\\Sail\\SailServiceProvider', + 37 => 'Laravel\\Socialite\\SocialiteServiceProvider', + 38 => 'Laravel\\Tinker\\TinkerServiceProvider', + 39 => 'Livewire\\LivewireServiceProvider', + 40 => 'Milon\\Barcode\\BarcodeServiceProvider', + 41 => 'Carbon\\Laravel\\ServiceProvider', + 42 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', + 43 => 'Termwind\\Laravel\\TermwindServiceProvider', + 44 => 'RyanChandler\\BladeCaptureDirective\\BladeCaptureDirectiveServiceProvider', + 45 => 'SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider', + 46 => 'App\\Providers\\AppServiceProvider', + 47 => 'App\\Providers\\Filament\\AdminPanelProvider', + ), + 'eager' => + array ( + 0 => 'Illuminate\\Auth\\AuthServiceProvider', + 1 => 'Illuminate\\Cookie\\CookieServiceProvider', + 2 => 'Illuminate\\Database\\DatabaseServiceProvider', + 3 => 'Illuminate\\Encryption\\EncryptionServiceProvider', + 4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider', + 5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider', + 6 => 'Illuminate\\Notifications\\NotificationServiceProvider', + 7 => 'Illuminate\\Pagination\\PaginationServiceProvider', + 8 => 'Illuminate\\Session\\SessionServiceProvider', + 9 => 'Illuminate\\View\\ViewServiceProvider', + 10 => 'Barryvdh\\DomPDF\\ServiceProvider', + 11 => 'BladeUI\\Heroicons\\BladeHeroiconsServiceProvider', + 12 => 'BladeUI\\Icons\\BladeIconsServiceProvider', + 13 => 'Filament\\Actions\\ActionsServiceProvider', + 14 => 'Filament\\FilamentServiceProvider', + 15 => 'Filament\\Forms\\FormsServiceProvider', + 16 => 'Filament\\Infolists\\InfolistsServiceProvider', + 17 => 'Filament\\Notifications\\NotificationsServiceProvider', + 18 => 'Filament\\Support\\SupportServiceProvider', + 19 => 'Filament\\Tables\\TablesServiceProvider', + 20 => 'Filament\\Widgets\\WidgetsServiceProvider', + 21 => 'Kirschbaum\\PowerJoins\\PowerJoinsServiceProvider', + 22 => 'Laravel\\Pail\\PailServiceProvider', + 23 => 'Livewire\\LivewireServiceProvider', + 24 => 'Milon\\Barcode\\BarcodeServiceProvider', + 25 => 'Carbon\\Laravel\\ServiceProvider', + 26 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', + 27 => 'Termwind\\Laravel\\TermwindServiceProvider', + 28 => 'RyanChandler\\BladeCaptureDirective\\BladeCaptureDirectiveServiceProvider', + 29 => 'SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider', + 30 => 'App\\Providers\\AppServiceProvider', + 31 => 'App\\Providers\\Filament\\AdminPanelProvider', + ), + 'deferred' => + array ( + 'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', + 'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', + 'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', + 'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider', + 'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider', + 'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider', + 'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider', + 'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider', + 'cache' => 'Illuminate\\Cache\\CacheServiceProvider', + 'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider', + 'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider', + 'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider', + 'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider', + 'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Concurrency\\Console\\InvokeSerializedClosureCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ApiInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\BroadcastingInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ClassMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ConfigMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ConfigPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\EnumMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\InterfaceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\JobMiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\TraitMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Migrations\\Migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 'Illuminate\\Concurrency\\ConcurrencyManager' => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider', + 'hash' => 'Illuminate\\Hashing\\HashServiceProvider', + 'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider', + 'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider', + 'mailer' => 'Illuminate\\Mail\\MailServiceProvider', + 'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider', + 'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider', + 'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider', + 'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider', + 'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider', + 'queue' => 'Illuminate\\Queue\\QueueServiceProvider', + 'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider', + 'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider', + 'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider', + 'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider', + 'redis' => 'Illuminate\\Redis\\RedisServiceProvider', + 'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider', + 'translator' => 'Illuminate\\Translation\\TranslationServiceProvider', + 'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider', + 'validator' => 'Illuminate\\Validation\\ValidationServiceProvider', + 'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider', + 'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider', + 'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider', + 'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider', + 'Laravel\\Socialite\\Contracts\\Factory' => 'Laravel\\Socialite\\SocialiteServiceProvider', + 'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider', + ), + 'when' => + array ( + 'Illuminate\\Broadcasting\\BroadcastServiceProvider' => + array ( + ), + 'Illuminate\\Bus\\BusServiceProvider' => + array ( + ), + 'Illuminate\\Cache\\CacheServiceProvider' => + array ( + ), + 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' => + array ( + ), + 'Illuminate\\Concurrency\\ConcurrencyServiceProvider' => + array ( + ), + 'Illuminate\\Hashing\\HashServiceProvider' => + array ( + ), + 'Illuminate\\Mail\\MailServiceProvider' => + array ( + ), + 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' => + array ( + ), + 'Illuminate\\Pipeline\\PipelineServiceProvider' => + array ( + ), + 'Illuminate\\Queue\\QueueServiceProvider' => + array ( + ), + 'Illuminate\\Redis\\RedisServiceProvider' => + array ( + ), + 'Illuminate\\Translation\\TranslationServiceProvider' => + array ( + ), + 'Illuminate\\Validation\\ValidationServiceProvider' => + array ( + ), + 'Laravel\\Sail\\SailServiceProvider' => + array ( + ), + 'Laravel\\Socialite\\SocialiteServiceProvider' => + array ( + ), + 'Laravel\\Tinker\\TinkerServiceProvider' => + array ( + ), + ), +); \ No newline at end of file diff --git a/bootstrap/cache/services.php b/bootstrap/cache/services.php index ce37bb3..d7b88b1 100644 --- a/bootstrap/cache/services.php +++ b/bootstrap/cache/services.php @@ -37,18 +37,20 @@ 33 => 'Filament\\Widgets\\WidgetsServiceProvider', 34 => 'Kirschbaum\\PowerJoins\\PowerJoinsServiceProvider', 35 => 'Laravel\\Pail\\PailServiceProvider', - 36 => 'Laravel\\Sail\\SailServiceProvider', - 37 => 'Laravel\\Socialite\\SocialiteServiceProvider', - 38 => 'Laravel\\Tinker\\TinkerServiceProvider', - 39 => 'Livewire\\LivewireServiceProvider', - 40 => 'Milon\\Barcode\\BarcodeServiceProvider', - 41 => 'Carbon\\Laravel\\ServiceProvider', - 42 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', - 43 => 'Termwind\\Laravel\\TermwindServiceProvider', - 44 => 'RyanChandler\\BladeCaptureDirective\\BladeCaptureDirectiveServiceProvider', - 45 => 'SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider', - 46 => 'App\\Providers\\AppServiceProvider', - 47 => 'App\\Providers\\Filament\\AdminPanelProvider', + 36 => 'Laravel\\Reverb\\ApplicationManagerServiceProvider', + 37 => 'Laravel\\Reverb\\ReverbServiceProvider', + 38 => 'Laravel\\Sail\\SailServiceProvider', + 39 => 'Laravel\\Socialite\\SocialiteServiceProvider', + 40 => 'Laravel\\Tinker\\TinkerServiceProvider', + 41 => 'Livewire\\LivewireServiceProvider', + 42 => 'Milon\\Barcode\\BarcodeServiceProvider', + 43 => 'Carbon\\Laravel\\ServiceProvider', + 44 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', + 45 => 'Termwind\\Laravel\\TermwindServiceProvider', + 46 => 'RyanChandler\\BladeCaptureDirective\\BladeCaptureDirectiveServiceProvider', + 47 => 'SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider', + 48 => 'App\\Providers\\AppServiceProvider', + 49 => 'App\\Providers\\Filament\\AdminPanelProvider', ), 'eager' => array ( @@ -75,15 +77,16 @@ 20 => 'Filament\\Widgets\\WidgetsServiceProvider', 21 => 'Kirschbaum\\PowerJoins\\PowerJoinsServiceProvider', 22 => 'Laravel\\Pail\\PailServiceProvider', - 23 => 'Livewire\\LivewireServiceProvider', - 24 => 'Milon\\Barcode\\BarcodeServiceProvider', - 25 => 'Carbon\\Laravel\\ServiceProvider', - 26 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', - 27 => 'Termwind\\Laravel\\TermwindServiceProvider', - 28 => 'RyanChandler\\BladeCaptureDirective\\BladeCaptureDirectiveServiceProvider', - 29 => 'SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider', - 30 => 'App\\Providers\\AppServiceProvider', - 31 => 'App\\Providers\\Filament\\AdminPanelProvider', + 23 => 'Laravel\\Reverb\\ReverbServiceProvider', + 24 => 'Livewire\\LivewireServiceProvider', + 25 => 'Milon\\Barcode\\BarcodeServiceProvider', + 26 => 'Carbon\\Laravel\\ServiceProvider', + 27 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', + 28 => 'Termwind\\Laravel\\TermwindServiceProvider', + 29 => 'RyanChandler\\BladeCaptureDirective\\BladeCaptureDirectiveServiceProvider', + 30 => 'SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider', + 31 => 'App\\Providers\\AppServiceProvider', + 32 => 'App\\Providers\\Filament\\AdminPanelProvider', ), 'deferred' => array ( @@ -238,6 +241,8 @@ 'validator' => 'Illuminate\\Validation\\ValidationServiceProvider', 'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider', 'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider', + 'Laravel\\Reverb\\ApplicationManager' => 'Laravel\\Reverb\\ApplicationManagerServiceProvider', + 'Laravel\\Reverb\\Contracts\\ApplicationProvider' => 'Laravel\\Reverb\\ApplicationManagerServiceProvider', 'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider', 'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider', 'Laravel\\Socialite\\Contracts\\Factory' => 'Laravel\\Socialite\\SocialiteServiceProvider', @@ -284,6 +289,9 @@ 'Illuminate\\Validation\\ValidationServiceProvider' => array ( ), + 'Laravel\\Reverb\\ApplicationManagerServiceProvider' => + array ( + ), 'Laravel\\Sail\\SailServiceProvider' => array ( ), @@ -294,4 +302,5 @@ array ( ), ), -); \ No newline at end of file +); + diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 22744d1..8099c53 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -4,3 +4,4 @@ App\Providers\AppServiceProvider::class, App\Providers\Filament\AdminPanelProvider::class, ]; + diff --git a/composer.json b/composer.json index 4341514..228fc10 100644 --- a/composer.json +++ b/composer.json @@ -11,10 +11,14 @@ "filament/filament": "3.3", "intervention/image": "^3.11", "laravel/framework": "^12.0", - "laravel/socialite": "^5.23", + "laravel/reverb": "^1.10", + "laravel/socialite": "^5.27", "laravel/tinker": "^2.10.1", "milon/barcode": "^12.0", + "php-mqtt/client": "^2.3", "picqer/php-barcode-generator": "^3.2", + "predis/predis": "^3.4", + "pusher/pusher-php-server": "^7.2", "simplesoftwareio/simple-qrcode": "^4.2" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 71a783a..dcb9c9d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5b04bd92502b5de5b6639f383e7ba8a1", + "content-hash": "4c3573c4a1aa21061c85a392adb30cae", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -128,26 +128,26 @@ }, { "name": "barryvdh/laravel-dompdf", - "version": "v3.1.1", + "version": "v3.1.2", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-dompdf.git", - "reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d" + "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d", - "reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc", + "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc", "shasum": "" }, "require": { "dompdf/dompdf": "^3.0", - "illuminate/support": "^9|^10|^11|^12", + "illuminate/support": "^9|^10|^11|^12|^13.0", "php": "^8.1" }, "require-dev": { "larastan/larastan": "^2.7|^3.0", - "orchestra/testbench": "^7|^8|^9|^10", + "orchestra/testbench": "^7|^8|^9.16|^10|^11.0", "phpro/grumphp": "^2.5", "squizlabs/php_codesniffer": "^3.5" }, @@ -189,7 +189,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-dompdf/issues", - "source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.1" + "source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.2" }, "funding": [ { @@ -201,7 +201,7 @@ "type": "github" } ], - "time": "2025-02-13T15:07:54+00:00" + "time": "2026-02-21T08:51:10+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -482,6 +482,136 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "clue/redis-protocol", + "version": "v0.3.2", + "source": { + "type": "git", + "url": "https://github.com/clue/redis-protocol.git", + "reference": "6f565332f5531b7722d1e9c445314b91862f6d6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/redis-protocol/zipball/6f565332f5531b7722d1e9c445314b91862f6d6c", + "reference": "6f565332f5531b7722d1e9c445314b91862f6d6c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\Redis\\Protocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@lueck.tv" + } + ], + "description": "A streaming Redis protocol (RESP) parser and serializer written in pure PHP.", + "homepage": "https://github.com/clue/redis-protocol", + "keywords": [ + "parser", + "protocol", + "redis", + "resp", + "serializer", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/redis-protocol/issues", + "source": "https://github.com/clue/redis-protocol/tree/v0.3.2" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2024-08-07T11:06:28+00:00" + }, + { + "name": "clue/redis-react", + "version": "v2.8.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-redis.git", + "reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-redis/zipball/84569198dfd5564977d2ae6a32de4beb5a24bdca", + "reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca", + "shasum": "" + }, + "require": { + "clue/redis-protocol": "^0.3.2", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.0 || ^1.1", + "react/promise-timer": "^1.11", + "react/socket": "^1.16" + }, + "require-dev": { + "clue/block-react": "^1.5", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\Redis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering" + } + ], + "description": "Async Redis client implementation, built on top of ReactPHP.", + "homepage": "https://github.com/clue/reactphp-redis", + "keywords": [ + "async", + "client", + "database", + "reactphp", + "redis" + ], + "support": { + "issues": "https://github.com/clue/reactphp-redis/issues", + "source": "https://github.com/clue/reactphp-redis/tree/v2.8.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2025-01-03T16:18:33+00:00" + }, { "name": "danharrin/date-format-converter", "version": "v0.3.1", @@ -1320,6 +1450,53 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Γ‰vΓ©nement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, { "name": "filament/actions", "version": "v3.3.0", @@ -2844,6 +3021,85 @@ }, "time": "2025-09-19T13:47:56+00:00" }, + { + "name": "laravel/reverb", + "version": "v1.10.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/reverb.git", + "reference": "43a5c0a99b1aaba33dc32f97fcf51f182dd8c8ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/reverb/zipball/43a5c0a99b1aaba33dc32f97fcf51f182dd8c8ac", + "reference": "43a5c0a99b1aaba33dc32f97fcf51f182dd8c8ac", + "shasum": "" + }, + "require": { + "clue/redis-react": "^2.6", + "guzzlehttp/psr7": "^2.6", + "illuminate/console": "^10.47|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.47|^11.0|^12.0|^13.0", + "illuminate/http": "^10.47|^11.0|^12.0|^13.0", + "illuminate/support": "^10.47|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.15|^0.2.0|^0.3.0", + "php": "^8.2", + "pusher/pusher-php-server": "^7.2", + "ratchet/rfc6455": "^0.4", + "react/promise-timer": "^1.10", + "react/socket": "^1.14", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-foundation": "^6.3|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", + "pestphp/pest": "^2.0|^3.0|^4.0", + "phpstan/phpstan": "^1.10", + "ratchet/pawl": "^0.4.1", + "react/async": "^4.2", + "react/http": "^1.9" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Reverb\\ApplicationManagerServiceProvider", + "Laravel\\Reverb\\ReverbServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Reverb\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Joe Dixon", + "email": "joe@laravel.com" + } + ], + "description": "Laravel Reverb provides a real-time WebSocket communication backend for Laravel applications.", + "keywords": [ + "WebSockets", + "laravel", + "real-time", + "websocket" + ], + "support": { + "issues": "https://github.com/laravel/reverb/issues", + "source": "https://github.com/laravel/reverb/tree/v1.10.2" + }, + "time": "2026-05-10T15:47:52+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.6", @@ -2907,34 +3163,34 @@ }, { "name": "laravel/socialite", - "version": "v5.23.0", + "version": "v5.27.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5" + "reference": "40e0757a75637c7b2dff05d3286b0d8fc25e5c0e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", - "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", + "url": "https://api.github.com/repos/laravel/socialite/zipball/40e0757a75637c7b2dff05d3286b0d8fc25e5c0e", + "reference": "40e0757a75637c7b2dff05d3286b0d8fc25e5c0e", "shasum": "" }, "require": { "ext-json": "*", - "firebase/php-jwt": "^6.4", + "firebase/php-jwt": "^6.4|^7.0", "guzzlehttp/guzzle": "^6.0|^7.0", - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", "league/oauth1-client": "^1.11", "php": "^7.2|^8.0", "phpseclib/phpseclib": "^3.0" }, "require-dev": { "mockery/mockery": "^1.0", - "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", + "orchestra/testbench": "^4.18|^5.20|^6.47|^7.55|^8.36|^9.15|^10.8|^11.0", "phpstan/phpstan": "^1.12.23", - "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5" + "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5|^12.0" }, "type": "library", "extra": { @@ -2975,7 +3231,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-07-23T14:16:08+00:00" + "time": "2026-04-24T14:05:47+00:00" }, { "name": "laravel/tinker", @@ -4082,6 +4338,69 @@ ], "time": "2025-03-24T10:02:05+00:00" }, + { + "name": "myclabs/php-enum", + "version": "1.8.5", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.6.2 || ^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "https://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.5" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "time": "2025-01-14T11:49:03+00:00" + }, { "name": "nesbot/carbon", "version": "3.10.3", @@ -4695,6 +5014,159 @@ }, "time": "2020-10-15T08:29:30+00:00" }, + { + "name": "paragonie/sodium_compat", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/4714da6efdc782c06690bc72ce34fae7941c2d9f", + "reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f", + "shasum": "" + }, + "require": { + "php": "^8.1", + "php-64bit": "*" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^7|^8|^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "suggest": { + "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "files": [ + "autoload.php" + ], + "psr-4": { + "ParagonIE\\Sodium\\": "namespaced/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v2.5.0" + }, + "time": "2025-12-30T16:12:18+00:00" + }, + { + "name": "php-mqtt/client", + "version": "v2.3.2", + "source": { + "type": "git", + "url": "https://github.com/php-mqtt/client.git", + "reference": "97df2f592599f72fb7975eb504333a8df3841ea9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mqtt/client/zipball/97df2f592599f72fb7975eb504333a8df3841ea9", + "reference": "97df2f592599f72fb7975eb504333a8df3841ea9", + "shasum": "" + }, + "require": { + "myclabs/php-enum": "^1.7", + "php": "^8.0", + "psr/log": "^1.1|^2.0|^3.0" + }, + "require-dev": { + "phpunit/php-invoker": "^3.0", + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-redis": "Required for the RedisRepository" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpMqtt\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marvin Mall", + "email": "marvin-mall@msn.com", + "role": "developer" + } + ], + "description": "An MQTT client written in and for PHP.", + "keywords": [ + "client", + "mqtt", + "publish", + "subscribe" + ], + "support": { + "issues": "https://github.com/php-mqtt/client/issues", + "source": "https://github.com/php-mqtt/client/tree/v2.3.2" + }, + "time": "2026-03-28T12:01:13+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.4", @@ -4961,6 +5433,69 @@ }, "time": "2025-07-29T07:15:20+00:00" }, + { + "name": "predis/predis", + "version": "v3.4.2", + "source": { + "type": "git", + "url": "https://github.com/predis/predis.git", + "reference": "2033429520d8997a7815a2485f56abe6d2d0e075" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/predis/predis/zipball/2033429520d8997a7815a2485f56abe6d2d0e075", + "reference": "2033429520d8997a7815a2485f56abe6d2d0e075", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.0|^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "phpstan/phpstan": "^1.9", + "phpunit/phpcov": "^6.0 || ^8.0", + "phpunit/phpunit": "^8.0 || ~9.4.4" + }, + "suggest": { + "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Till KrΓΌss", + "homepage": "https://till.im", + "role": "Maintainer" + } + ], + "description": "A flexible and feature-complete Redis/Valkey client for PHP.", + "homepage": "http://github.com/predis/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "support": { + "issues": "https://github.com/predis/predis/issues", + "source": "https://github.com/predis/predis/tree/v3.4.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/tillkruss", + "type": "github" + } + ], + "time": "2026-03-09T20:33:04+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -5500,6 +6035,67 @@ }, "time": "2025-09-20T13:46:31+00:00" }, + { + "name": "pusher/pusher-php-server", + "version": "7.2.7", + "source": { + "type": "git", + "url": "https://github.com/pusher/pusher-http-php.git", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.2", + "paragonie/sodium_compat": "^1.6|^2.0", + "php": "^7.3|^8.0", + "psr/log": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "overtrue/phplint": "^2.3", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "Pusher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Library for interacting with the Pusher REST API", + "keywords": [ + "events", + "messaging", + "php-pusher-server", + "publish", + "push", + "pusher", + "real time", + "real-time", + "realtime", + "rest", + "trigger" + ], + "support": { + "issues": "https://github.com/pusher/pusher-http-php/issues", + "source": "https://github.com/pusher/pusher-http-php/tree/7.2.7" + }, + "time": "2025-01-06T10:56:20+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -5698,6 +6294,595 @@ }, "time": "2025-09-04T20:59:21+00:00" }, + { + "name": "ratchet/rfc6455", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/859d95f85dda0912c6d5b936d036d044e3af47ef", + "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef", + "shasum": "" + }, + "require": { + "php": ">=7.4", + "psr/http-factory-implementation": "^1.0", + "symfony/polyfill-php80": "^1.15" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.7", + "phpunit/phpunit": "^9.5", + "react/socket": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\RFC6455\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.0" + }, + "time": "2025-02-24T01:18:22+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/dns", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-18T19:34:28+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/promise-timer", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7.0 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\Timer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", + "keywords": [ + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" + ], + "support": { + "issues": "https://github.com/reactphp/promise-timer/issues", + "source": "https://github.com/reactphp/promise-timer/tree/v1.11.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-04T14:27:45+00:00" + }, + { + "name": "react/socket", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, { "name": "ryangjchandler/blade-capture-directive", "version": "v1.1.0", diff --git a/config/app.php b/config/app.php index 6ec2c34..5114296 100644 --- a/config/app.php +++ b/config/app.php @@ -62,3 +62,4 @@ ], ]; + diff --git a/config/auth.php b/config/auth.php index 31e4b2f..622ead7 100644 --- a/config/auth.php +++ b/config/auth.php @@ -58,3 +58,4 @@ 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), ]; + diff --git a/config/broadcasting.php b/config/broadcasting.php new file mode 100644 index 0000000..1350131 --- /dev/null +++ b/config/broadcasting.php @@ -0,0 +1,83 @@ + env('BROADCAST_CONNECTION', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over WebSockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'reverb' => [ + 'driver' => 'reverb', + 'key' => env('REVERB_APP_KEY'), + 'secret' => env('REVERB_APP_SECRET'), + 'app_id' => env('REVERB_APP_ID'), + 'options' => [ + 'host' => env('REVERB_HOST'), + 'port' => env('REVERB_PORT', 443), + 'scheme' => env('REVERB_SCHEME', 'https'), + 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', + 'port' => env('PUSHER_PORT', 443), + 'scheme' => env('PUSHER_SCHEME', 'https'), + 'encrypted' => true, + 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; + diff --git a/config/cache.php b/config/cache.php index d04bebc..fd2db33 100644 --- a/config/cache.php +++ b/config/cache.php @@ -75,3 +75,4 @@ 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), ]; + diff --git a/config/database.php b/config/database.php index 0102169..4981469 100644 --- a/config/database.php +++ b/config/database.php @@ -144,3 +144,4 @@ ], ]; + diff --git a/config/filament.php b/config/filament.php index d546786..c5b4ccb 100644 --- a/config/filament.php +++ b/config/filament.php @@ -44,12 +44,11 @@ 'user_model' => App\Models\User::class, - 'auth_check' => function ($user) { - return $user && $user->role === 'admin'; - }, + 'auth_check' => null, ], 'path' => env('FILAMENT_PATH', 'admin'), ]; + diff --git a/config/filesystems.php b/config/filesystems.php index 784787d..7ee11b7 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -47,3 +47,4 @@ ], ]; + diff --git a/config/logging.php b/config/logging.php index 2959495..6e6ec21 100644 --- a/config/logging.php +++ b/config/logging.php @@ -100,3 +100,4 @@ ], ]; + diff --git a/config/mail.php b/config/mail.php index 5d11dc2..6ccb744 100644 --- a/config/mail.php +++ b/config/mail.php @@ -80,3 +80,4 @@ ], ]; + diff --git a/config/qrcode.php b/config/qrcode.php index fdeab3c..ebbf7f5 100644 --- a/config/qrcode.php +++ b/config/qrcode.php @@ -17,3 +17,4 @@ 'writer' => 'gd', ]; + diff --git a/config/queue.php b/config/queue.php index 9b5a8e6..377daf2 100644 --- a/config/queue.php +++ b/config/queue.php @@ -70,3 +70,4 @@ ], ]; + diff --git a/config/reverb.php b/config/reverb.php new file mode 100644 index 0000000..41c8f13 --- /dev/null +++ b/config/reverb.php @@ -0,0 +1,103 @@ + env('REVERB_SERVER', 'reverb'), + + /* + |-------------------------------------------------------------------------- + | Reverb Servers + |-------------------------------------------------------------------------- + | + | Here you may define details for each of the supported Reverb servers. + | Each server has its own configuration options that are defined in + | the array below. You should ensure all the options are present. + | + */ + + 'servers' => [ + + 'reverb' => [ + 'host' => env('REVERB_SERVER_HOST', '0.0.0.0'), + 'port' => env('REVERB_SERVER_PORT', 8080), + 'path' => env('REVERB_SERVER_PATH', ''), + 'hostname' => env('REVERB_HOST'), + 'options' => [ + 'tls' => [], + ], + 'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000), + 'scaling' => [ + 'enabled' => env('REVERB_SCALING_ENABLED', false), + 'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'), + 'server' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'port' => env('REDIS_PORT', '6379'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'database' => env('REDIS_DB', '0'), + 'timeout' => env('REDIS_TIMEOUT', 60), + ], + ], + 'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15), + 'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Reverb Applications + |-------------------------------------------------------------------------- + | + | Here you may define how Reverb applications are managed. If you choose + | to use the "config" provider, you may define an array of apps which + | your server will support, including their connection credentials. + | + */ + + 'apps' => [ + + 'provider' => 'config', + + 'apps' => [ + [ + 'key' => env('REVERB_APP_KEY'), + 'secret' => env('REVERB_APP_SECRET'), + 'app_id' => env('REVERB_APP_ID'), + 'options' => [ + 'host' => env('REVERB_HOST'), + 'port' => env('REVERB_PORT', 443), + 'scheme' => env('REVERB_SCHEME', 'https'), + 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', + ], + 'allowed_origins' => ['*'], + 'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60), + 'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30), + 'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'), + 'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000), + 'accept_client_events_from' => env('REVERB_APP_ACCEPT_CLIENT_EVENTS_FROM', 'members'), + 'rate_limiting' => [ + 'enabled' => env('REVERB_APP_RATE_LIMITING_ENABLED', false), + 'max_attempts' => env('REVERB_APP_RATE_LIMIT_MAX_ATTEMPTS', 60), + 'decay_seconds' => env('REVERB_APP_RATE_LIMIT_DECAY_SECONDS', 60), + 'terminate_on_limit' => env('REVERB_APP_RATE_LIMIT_TERMINATE', false), + ], + ], + ], + + ], + +]; + diff --git a/config/services.php b/config/services.php index c201e0b..c7aa949 100644 --- a/config/services.php +++ b/config/services.php @@ -32,3 +32,4 @@ ]; + diff --git a/config/session.php b/config/session.php index 2cd91c2..8405acc 100644 --- a/config/session.php +++ b/config/session.php @@ -68,3 +68,4 @@ 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), ]; + diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 584104c..f1ca584 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -42,3 +42,4 @@ public function unverified(): static ]); } } + diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 444fafb..a3f402e 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -30,3 +30,4 @@ public function down(): void Schema::dropIfExists('users'); } }; + diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php index b9c106b..f4207fc 100644 --- a/database/migrations/0001_01_01_000001_create_cache_table.php +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -33,3 +33,4 @@ public function down(): void Schema::dropIfExists('cache_locks'); } }; + diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php index 425e705..1e7c230 100644 --- a/database/migrations/0001_01_01_000002_create_jobs_table.php +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -55,3 +55,4 @@ public function down(): void Schema::dropIfExists('failed_jobs'); } }; + diff --git a/database/migrations/2025_10_13_041740_create_books_table.php b/database/migrations/2025_10_13_041740_create_books_table.php index 34e0b9f..449df00 100644 --- a/database/migrations/2025_10_13_041740_create_books_table.php +++ b/database/migrations/2025_10_13_041740_create_books_table.php @@ -25,3 +25,4 @@ public function down(): void Schema::dropIfExists('books'); } } + diff --git a/database/migrations/2025_10_13_041741_create_members_table.php b/database/migrations/2025_10_13_041741_create_members_table.php index 8a3418a..fd3eddf 100644 --- a/database/migrations/2025_10_13_041741_create_members_table.php +++ b/database/migrations/2025_10_13_041741_create_members_table.php @@ -23,3 +23,4 @@ public function down(): void Schema::dropIfExists('members'); } }; + diff --git a/database/migrations/2025_10_13_061335_create_sessions_table.php b/database/migrations/2025_10_13_061335_create_sessions_table.php index bc2da43..bcc22bc 100644 --- a/database/migrations/2025_10_13_061335_create_sessions_table.php +++ b/database/migrations/2025_10_13_061335_create_sessions_table.php @@ -22,3 +22,4 @@ public function down(): void Schema::dropIfExists('sessions'); } }; + diff --git a/database/migrations/2025_10_13_115703_create_koleksis_table.php b/database/migrations/2025_10_13_115703_create_koleksis_table.php index 2305b6a..e91568f 100644 --- a/database/migrations/2025_10_13_115703_create_koleksis_table.php +++ b/database/migrations/2025_10_13_115703_create_koleksis_table.php @@ -23,3 +23,4 @@ public function down(): void Schema::dropIfExists('koleksis'); } }; + diff --git a/database/migrations/2025_10_13_131310_add_role_to_users_table.php b/database/migrations/2025_10_13_131310_add_role_to_users_table.php index fe3f6b0..d650a7e 100644 --- a/database/migrations/2025_10_13_131310_add_role_to_users_table.php +++ b/database/migrations/2025_10_13_131310_add_role_to_users_table.php @@ -24,3 +24,4 @@ public function down(): void } } } + diff --git a/database/migrations/2025_10_13_132558_add_admin_role_to_users_table.php b/database/migrations/2025_10_13_132558_add_admin_role_to_users_table.php index 4b7218c..73ddb47 100644 --- a/database/migrations/2025_10_13_132558_add_admin_role_to_users_table.php +++ b/database/migrations/2025_10_13_132558_add_admin_role_to_users_table.php @@ -24,3 +24,4 @@ public function down(): void }); } } + diff --git a/database/migrations/2025_10_14_141805_create_anggotas_table.php b/database/migrations/2025_10_14_141805_create_anggotas_table.php index a9c35cc..2dd551d 100644 --- a/database/migrations/2025_10_14_141805_create_anggotas_table.php +++ b/database/migrations/2025_10_14_141805_create_anggotas_table.php @@ -22,3 +22,4 @@ public function down(): void Schema::dropIfExists('anggotas'); } } + diff --git a/database/migrations/2025_10_14_163805_create_borrow_transactions_table.php b/database/migrations/2025_10_14_163805_create_borrow_transactions_table.php index a049b03..60f131c 100644 --- a/database/migrations/2025_10_14_163805_create_borrow_transactions_table.php +++ b/database/migrations/2025_10_14_163805_create_borrow_transactions_table.php @@ -24,3 +24,4 @@ public function down(): void Schema::dropIfExists('borrow_transactions'); } }; + diff --git a/database/migrations/2025_10_15_171208_update_koleksis_table_to_new_structure.php b/database/migrations/2025_10_15_171208_update_koleksis_table_to_new_structure.php index 220f79f..30d74a8 100644 --- a/database/migrations/2025_10_15_171208_update_koleksis_table_to_new_structure.php +++ b/database/migrations/2025_10_15_171208_update_koleksis_table_to_new_structure.php @@ -146,4 +146,5 @@ public function down(): void } }); } -}; \ No newline at end of file +}; + diff --git a/database/migrations/2025_10_16_035039_create_publishers_table.php b/database/migrations/2025_10_16_035039_create_publishers_table.php index ce36b20..82c3c23 100644 --- a/database/migrations/2025_10_16_035039_create_publishers_table.php +++ b/database/migrations/2025_10_16_035039_create_publishers_table.php @@ -19,3 +19,4 @@ public function down() Schema::dropIfExists('publishers'); } }; + diff --git a/database/migrations/2025_10_16_035040_create_publish_places_table.php b/database/migrations/2025_10_16_035040_create_publish_places_table.php index 6300f89..a8b508f 100644 --- a/database/migrations/2025_10_16_035040_create_publish_places_table.php +++ b/database/migrations/2025_10_16_035040_create_publish_places_table.php @@ -19,3 +19,4 @@ public function down() Schema::dropIfExists('publish_places'); } }; + diff --git a/database/migrations/2025_10_16_164735_add_qr_code_to_koleksis_table.php b/database/migrations/2025_10_16_164735_add_qr_code_to_koleksis_table.php index 2f8076d..f66762f 100644 --- a/database/migrations/2025_10_16_164735_add_qr_code_to_koleksis_table.php +++ b/database/migrations/2025_10_16_164735_add_qr_code_to_koleksis_table.php @@ -22,3 +22,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_10_17_171153_create_settings_table.php b/database/migrations/2025_10_17_171153_create_settings_table.php index 0bb94b2..ee1fb0c 100644 --- a/database/migrations/2025_10_17_171153_create_settings_table.php +++ b/database/migrations/2025_10_17_171153_create_settings_table.php @@ -28,3 +28,4 @@ public function down(): void Schema::dropIfExists('settings'); } }; + diff --git a/database/migrations/2025_10_18_075508_add_koleksi_id_to_borrow_transactions_table.php b/database/migrations/2025_10_18_075508_add_koleksi_id_to_borrow_transactions_table.php index 1e1abdf..c8757e5 100644 --- a/database/migrations/2025_10_18_075508_add_koleksi_id_to_borrow_transactions_table.php +++ b/database/migrations/2025_10_18_075508_add_koleksi_id_to_borrow_transactions_table.php @@ -28,3 +28,4 @@ public function down(): void } }; + diff --git a/database/migrations/2025_10_18_081328_clean_borrow_transactions_table.php b/database/migrations/2025_10_18_081328_clean_borrow_transactions_table.php index 1252ebe..5a280c4 100644 --- a/database/migrations/2025_10_18_081328_clean_borrow_transactions_table.php +++ b/database/migrations/2025_10_18_081328_clean_borrow_transactions_table.php @@ -35,3 +35,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_10_18_093200_add_is_admin_to_users_table.php b/database/migrations/2025_10_18_093200_add_is_admin_to_users_table.php index f2109ea..9ca821f 100644 --- a/database/migrations/2025_10_18_093200_add_is_admin_to_users_table.php +++ b/database/migrations/2025_10_18_093200_add_is_admin_to_users_table.php @@ -19,3 +19,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_10_19_091732_remove_role_from_users_table.php b/database/migrations/2025_10_19_091732_remove_role_from_users_table.php index 3049739..cd86a04 100644 --- a/database/migrations/2025_10_19_091732_remove_role_from_users_table.php +++ b/database/migrations/2025_10_19_091732_remove_role_from_users_table.php @@ -22,3 +22,4 @@ public function down(): void }); } } + diff --git a/database/migrations/2025_10_19_092031_add_role_to_users_table.php b/database/migrations/2025_10_19_092031_add_role_to_users_table.php index c738a00..c56afdd 100644 --- a/database/migrations/2025_10_19_092031_add_role_to_users_table.php +++ b/database/migrations/2025_10_19_092031_add_role_to_users_table.php @@ -20,3 +20,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_10_22_025829_drop_is_admin_from_users_table.php b/database/migrations/2025_10_22_025829_drop_is_admin_from_users_table.php index 1138576..1d568a4 100644 --- a/database/migrations/2025_10_22_025829_drop_is_admin_from_users_table.php +++ b/database/migrations/2025_10_22_025829_drop_is_admin_from_users_table.php @@ -25,3 +25,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_10_23_091712_add_surat_pengantar_to_borrow_transactions_table.php b/database/migrations/2025_10_23_091712_add_surat_pengantar_to_borrow_transactions_table.php index d82d304..eed2847 100644 --- a/database/migrations/2025_10_23_091712_add_surat_pengantar_to_borrow_transactions_table.php +++ b/database/migrations/2025_10_23_091712_add_surat_pengantar_to_borrow_transactions_table.php @@ -28,3 +28,4 @@ public function down() } }; + diff --git a/database/migrations/2025_10_23_150118_create_user_logins_table.php b/database/migrations/2025_10_23_150118_create_user_logins_table.php index 02ff30b..0758684 100644 --- a/database/migrations/2025_10_23_150118_create_user_logins_table.php +++ b/database/migrations/2025_10_23_150118_create_user_logins_table.php @@ -23,3 +23,4 @@ public function up(): void } }; + diff --git a/database/migrations/2025_10_23_154111_create_fasilitas_table.php b/database/migrations/2025_10_23_154111_create_fasilitas_table.php index b4cfab9..20fe9a5 100644 --- a/database/migrations/2025_10_23_154111_create_fasilitas_table.php +++ b/database/migrations/2025_10_23_154111_create_fasilitas_table.php @@ -25,3 +25,4 @@ public function down(): void Schema::dropIfExists('fasilitas'); } }; + diff --git a/database/migrations/2025_10_26_150808_add_status_and_pickup_date_to_borrow_transactions_table.php b/database/migrations/2025_10_26_150808_add_status_and_pickup_date_to_borrow_transactions_table.php index 6dd4f76..61ae7ce 100644 --- a/database/migrations/2025_10_26_150808_add_status_and_pickup_date_to_borrow_transactions_table.php +++ b/database/migrations/2025_10_26_150808_add_status_and_pickup_date_to_borrow_transactions_table.php @@ -28,3 +28,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_10_26_160132_add_pickup_date_to_borrow_transactions_table.php b/database/migrations/2025_10_26_160132_add_pickup_date_to_borrow_transactions_table.php index 4b8638c..86930b1 100644 --- a/database/migrations/2025_10_26_160132_add_pickup_date_to_borrow_transactions_table.php +++ b/database/migrations/2025_10_26_160132_add_pickup_date_to_borrow_transactions_table.php @@ -22,3 +22,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_10_26_161221_rename_tanggal_pengambilan_to_pickup_date_in_borrow_transactions_table.php b/database/migrations/2025_10_26_161221_rename_tanggal_pengambilan_to_pickup_date_in_borrow_transactions_table.php index 9954186..df354eb 100644 --- a/database/migrations/2025_10_26_161221_rename_tanggal_pengambilan_to_pickup_date_in_borrow_transactions_table.php +++ b/database/migrations/2025_10_26_161221_rename_tanggal_pengambilan_to_pickup_date_in_borrow_transactions_table.php @@ -24,3 +24,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_10_27_224846_add_email_login_token_to_users_table.php b/database/migrations/2025_10_27_224846_add_email_login_token_to_users_table.php index 2a78e38..9f81ada 100644 --- a/database/migrations/2025_10_27_224846_add_email_login_token_to_users_table.php +++ b/database/migrations/2025_10_27_224846_add_email_login_token_to_users_table.php @@ -27,3 +27,4 @@ public function down() }); } }; + diff --git a/database/migrations/2025_10_28_211910_add_verification_token_to_users_table.php b/database/migrations/2025_10_28_211910_add_verification_token_to_users_table.php index 59f4f52..132d6c0 100644 --- a/database/migrations/2025_10_28_211910_add_verification_token_to_users_table.php +++ b/database/migrations/2025_10_28_211910_add_verification_token_to_users_table.php @@ -45,3 +45,4 @@ public function down() }); } }; + diff --git a/database/migrations/2025_10_28_223403_add_email_otp_expires_at_to_users_table.php b/database/migrations/2025_10_28_223403_add_email_otp_expires_at_to_users_table.php index 2729376..48cc273 100644 --- a/database/migrations/2025_10_28_223403_add_email_otp_expires_at_to_users_table.php +++ b/database/migrations/2025_10_28_223403_add_email_otp_expires_at_to_users_table.php @@ -23,3 +23,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_10_28_225354_add_otp_last_sent_at_to_users_table.php b/database/migrations/2025_10_28_225354_add_otp_last_sent_at_to_users_table.php index a6d0337..b8815d4 100644 --- a/database/migrations/2025_10_28_225354_add_otp_last_sent_at_to_users_table.php +++ b/database/migrations/2025_10_28_225354_add_otp_last_sent_at_to_users_table.php @@ -25,3 +25,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_10_29_222551_add_status_to_anggotas_table.php b/database/migrations/2025_10_29_222551_add_status_to_anggotas_table.php index c4f1ec9..87d0990 100644 --- a/database/migrations/2025_10_29_222551_add_status_to_anggotas_table.php +++ b/database/migrations/2025_10_29_222551_add_status_to_anggotas_table.php @@ -20,3 +20,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_10_29_232225_create_ratings_table.php b/database/migrations/2025_10_29_232225_create_ratings_table.php index f6009bb..6922805 100644 --- a/database/migrations/2025_10_29_232225_create_ratings_table.php +++ b/database/migrations/2025_10_29_232225_create_ratings_table.php @@ -29,3 +29,4 @@ public function down(): void Schema::dropIfExists('ratings'); } }; + diff --git a/database/migrations/2025_10_31_141549_add_pickup_time_to_borrow_transactions_table.php b/database/migrations/2025_10_31_141549_add_pickup_time_to_borrow_transactions_table.php index 3d384d0..58f455b 100644 --- a/database/migrations/2025_10_31_141549_add_pickup_time_to_borrow_transactions_table.php +++ b/database/migrations/2025_10_31_141549_add_pickup_time_to_borrow_transactions_table.php @@ -24,3 +24,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_11_01_184223_add_filter_columns_to_koleksis_table.php b/database/migrations/2025_11_01_184223_add_filter_columns_to_koleksis_table.php index eefae35..0585f7f 100644 --- a/database/migrations/2025_11_01_184223_add_filter_columns_to_koleksis_table.php +++ b/database/migrations/2025_11_01_184223_add_filter_columns_to_koleksis_table.php @@ -34,3 +34,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_11_03_202447_add_perpanjangan_to_borrow_transactions_table.php b/database/migrations/2025_11_03_202447_add_perpanjangan_to_borrow_transactions_table.php index fd94c79..f7dc543 100644 --- a/database/migrations/2025_11_03_202447_add_perpanjangan_to_borrow_transactions_table.php +++ b/database/migrations/2025_11_03_202447_add_perpanjangan_to_borrow_transactions_table.php @@ -36,3 +36,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_11_03_220609_update_status_enum_in_borrow_transactions_table.php b/database/migrations/2025_11_03_220609_update_status_enum_in_borrow_transactions_table.php index 291f0a1..8ae138a 100644 --- a/database/migrations/2025_11_03_220609_update_status_enum_in_borrow_transactions_table.php +++ b/database/migrations/2025_11_03_220609_update_status_enum_in_borrow_transactions_table.php @@ -35,3 +35,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_11_03_222917_add_perpanjangan_count_to_borrow_transactions_table.php b/database/migrations/2025_11_03_222917_add_perpanjangan_count_to_borrow_transactions_table.php index 4e20feb..bbe51a1 100644 --- a/database/migrations/2025_11_03_222917_add_perpanjangan_count_to_borrow_transactions_table.php +++ b/database/migrations/2025_11_03_222917_add_perpanjangan_count_to_borrow_transactions_table.php @@ -23,3 +23,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_11_03_223619_add_perpanjangan_columns_to_borrow_transactions_table.php b/database/migrations/2025_11_03_223619_add_perpanjangan_columns_to_borrow_transactions_table.php index fa515da..f7e7c1e 100644 --- a/database/migrations/2025_11_03_223619_add_perpanjangan_columns_to_borrow_transactions_table.php +++ b/database/migrations/2025_11_03_223619_add_perpanjangan_columns_to_borrow_transactions_table.php @@ -37,3 +37,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_11_04_093323_create_password_reset_tokens_table.php b/database/migrations/2025_11_04_093323_create_password_reset_tokens_table.php index fda2b31..c96b254 100644 --- a/database/migrations/2025_11_04_093323_create_password_reset_tokens_table.php +++ b/database/migrations/2025_11_04_093323_create_password_reset_tokens_table.php @@ -19,3 +19,4 @@ public function down(): void Schema::dropIfExists('password_reset_tokens'); } }; + diff --git a/database/migrations/2025_11_06_233947_update_status_enum_add_ditolak_to_borrow_transactions_table.php b/database/migrations/2025_11_06_233947_update_status_enum_add_ditolak_to_borrow_transactions_table.php index 1d48b16..5cab940 100644 --- a/database/migrations/2025_11_06_233947_update_status_enum_add_ditolak_to_borrow_transactions_table.php +++ b/database/migrations/2025_11_06_233947_update_status_enum_add_ditolak_to_borrow_transactions_table.php @@ -39,3 +39,4 @@ public function down(): void "); } }; + diff --git a/database/migrations/2025_11_14_231251_add_barcode_to_koleksis_table.php b/database/migrations/2025_11_14_231251_add_barcode_to_koleksis_table.php index d5ee14b..1f88304 100644 --- a/database/migrations/2025_11_14_231251_add_barcode_to_koleksis_table.php +++ b/database/migrations/2025_11_14_231251_add_barcode_to_koleksis_table.php @@ -24,3 +24,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_11_19_131904_add_nip_nim_to_anggota_table.php b/database/migrations/2025_11_19_131904_add_nip_nim_to_anggota_table.php index b1cf378..ee4e634 100644 --- a/database/migrations/2025_11_19_131904_add_nip_nim_to_anggota_table.php +++ b/database/migrations/2025_11_19_131904_add_nip_nim_to_anggota_table.php @@ -20,3 +20,4 @@ public function down() }); } }; + diff --git a/database/migrations/2025_11_19_175517_add_foto_to_users_table.php b/database/migrations/2025_11_19_175517_add_foto_to_users_table.php index 7e31651..ddcf739 100644 --- a/database/migrations/2025_11_19_175517_add_foto_to_users_table.php +++ b/database/migrations/2025_11_19_175517_add_foto_to_users_table.php @@ -24,3 +24,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_11_22_231739_add_blocked_at_to_users_table.php b/database/migrations/2025_11_22_231739_add_blocked_at_to_users_table.php index 757d928..4221e10 100644 --- a/database/migrations/2025_11_22_231739_add_blocked_at_to_users_table.php +++ b/database/migrations/2025_11_22_231739_add_blocked_at_to_users_table.php @@ -24,3 +24,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2025_11_29_133733_add_is_late_to_borrow_transactions_table.php b/database/migrations/2025_11_29_133733_add_is_late_to_borrow_transactions_table.php index 23a1088..69ca042 100644 --- a/database/migrations/2025_11_29_133733_add_is_late_to_borrow_transactions_table.php +++ b/database/migrations/2025_11_29_133733_add_is_late_to_borrow_transactions_table.php @@ -28,3 +28,4 @@ public function down() } }; + diff --git a/database/migrations/2025_12_03_203552_add_foto_to_members_table.php b/database/migrations/2025_12_03_203552_add_foto_to_members_table.php index 2cd398a..072afa7 100644 --- a/database/migrations/2025_12_03_203552_add_foto_to_members_table.php +++ b/database/migrations/2025_12_03_203552_add_foto_to_members_table.php @@ -24,3 +24,4 @@ public function down(): void }); } }; + diff --git a/database/migrations/2026_03_11_195541_create_iot_modes_table.php b/database/migrations/2026_03_11_195541_create_iot_modes_table.php new file mode 100644 index 0000000..ca10e01 --- /dev/null +++ b/database/migrations/2026_03_11_195541_create_iot_modes_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('mode'); + $table->timestamps(); +}); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('iot_modes'); + } +}; + diff --git a/database/migrations/2026_04_05_211611_add_source_to_koleksis_table.php b/database/migrations/2026_04_05_211611_add_source_to_koleksis_table.php new file mode 100644 index 0000000..4cdd87d --- /dev/null +++ b/database/migrations/2026_04_05_211611_add_source_to_koleksis_table.php @@ -0,0 +1,29 @@ +string('source')->nullable(); +}); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('koleksis', function (Blueprint $table) { + // + }); + } +}; + diff --git a/database/migrations/2026_04_19_192224_add_rfid_to_users_table.php b/database/migrations/2026_04_19_192224_add_rfid_to_users_table.php new file mode 100644 index 0000000..6c5bb0d --- /dev/null +++ b/database/migrations/2026_04_19_192224_add_rfid_to_users_table.php @@ -0,0 +1,29 @@ +string('rfid_uid')->nullable(); + }); +} + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + // + }); + } +}; + diff --git a/database/migrations/2026_04_19_192643_add_book_to_iot_mode.php b/database/migrations/2026_04_19_192643_add_book_to_iot_mode.php new file mode 100644 index 0000000..5d860c7 --- /dev/null +++ b/database/migrations/2026_04_19_192643_add_book_to_iot_mode.php @@ -0,0 +1,20 @@ +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration { + public function up() + { + Schema::table('iot_modes', function (Blueprint $table) { + $table->unsignedBigInteger('book_id')->nullable(); + }); + } + + public function down() + { + Schema::table('iot_modes', function (Blueprint $table) { + $table->dropColumn('book_id'); + }); + } +}; + diff --git a/database/migrations/2026_04_23_002401_add_transaction_id_to_iot_modes_table.php b/database/migrations/2026_04_23_002401_add_transaction_id_to_iot_modes_table.php new file mode 100644 index 0000000..e2a8d5f --- /dev/null +++ b/database/migrations/2026_04_23_002401_add_transaction_id_to_iot_modes_table.php @@ -0,0 +1,27 @@ +unsignedBigInteger('transaction_id')->nullable()->after('book_id'); + }); +} + +public function down(): void +{ + Schema::table('iot_modes', function (Blueprint $table) { + $table->dropColumn('transaction_id'); + }); +} +}; + diff --git a/database/migrations/2026_04_27_001236_add_performance_indexes.php b/database/migrations/2026_04_27_001236_add_performance_indexes.php new file mode 100644 index 0000000..fcb597a --- /dev/null +++ b/database/migrations/2026_04_27_001236_add_performance_indexes.php @@ -0,0 +1,95 @@ +index('uid', 'idx_uid'); + } catch (\Exception $e) { + // Index sudah ada atau error lainnya + } + }); + + // Index untuk koleksis.kode_eksemplar - digunakan di /api/esp/scan-buku + Schema::table('koleksis', function (Blueprint $table) { + try { + $table->index('kode_eksemplar', 'idx_kode_eksemplar'); + } catch (\Exception $e) { + // Index sudah ada + } + }); + + // Composite index untuk borrow_transactions (status + kolecsi_id) + // Digunakan di /api/esp/scan-buku WHERE status='dipinjam' AND kolecsi_id=? + Schema::table('borrow_transactions', function (Blueprint $table) { + try { + $table->index(['status', 'kolecsi_id'], 'idx_status_kolecsi'); + } catch (\Exception $e) { + // Index sudah ada + } + }); + + // Index untuk borrow_transactions.status - digunakan dalam berbagai queries + Schema::table('borrow_transactions', function (Blueprint $table) { + try { + $table->index('status', 'idx_status'); + } catch (\Exception $e) { + // Index sudah ada + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('anggotas', function (Blueprint $table) { + try { + $table->dropIndex('idx_uid'); + } catch (\Exception $e) { + // Index tidak ada + } + }); + + Schema::table('koleksis', function (Blueprint $table) { + try { + $table->dropIndex('idx_kode_eksemplar'); + } catch (\Exception $e) { + // Index tidak ada + } + }); + + Schema::table('borrow_transactions', function (Blueprint $table) { + try { + $table->dropIndex('idx_status_kolecsi'); + } catch (\Exception $e) { + // Index tidak ada + } + try { + $table->dropIndex('idx_status'); + } catch (\Exception $e) { + // Index tidak ada + } + }); + } +}; + diff --git a/database/migrations/2026_04_28_214437_update_status_enum_in_borrow_transactions.php b/database/migrations/2026_04_28_214437_update_status_enum_in_borrow_transactions.php new file mode 100644 index 0000000..cad9358 --- /dev/null +++ b/database/migrations/2026_04_28_214437_update_status_enum_in_borrow_transactions.php @@ -0,0 +1,23 @@ +text('admin_feedback')->nullable()->after('denda'); + }); +} + +public function down() +{ + Schema::table('borrow_transactions', function (Blueprint $table) { + $table->dropColumn('admin_feedback'); + }); +} +}; + diff --git a/database/migrations/2026_05_12_212919_add_google_id_to_users_table.php b/database/migrations/2026_05_12_212919_add_google_id_to_users_table.php new file mode 100644 index 0000000..4f0e78b --- /dev/null +++ b/database/migrations/2026_05_12_212919_add_google_id_to_users_table.php @@ -0,0 +1,30 @@ +string('google_id')->nullable()->after('id'); + }); +} + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + // + }); + } + +}; + diff --git a/database/migrations/2026_05_27_235808_modify_status_enum_add_perpanjangan.php b/database/migrations/2026_05_27_235808_modify_status_enum_add_perpanjangan.php new file mode 100644 index 0000000..1b440e1 --- /dev/null +++ b/database/migrations/2026_05_27_235808_modify_status_enum_add_perpanjangan.php @@ -0,0 +1,36 @@ +enum('status', [ + 'menunggu', + 'disetujui', + 'dipinjam', + 'dikembalikan', + 'ditolak', + 'pengembalian_menunggu', + 'perpanjangan_menunggu' // βœ… TAMBAHAN BARU + ])->default('menunggu')->change(); + }); + } + + public function down(): void + { + Schema::table('borrow_transactions', function (Blueprint $table) { + $table->enum('status', [ + 'menunggu', + 'disetujui', + 'dipinjam', + 'dikembalikan', + 'ditolak', + 'pengembalian_menunggu' + ])->change(); + }); + } +}; + diff --git a/database/migrations/2026_07_04_185342_add_gender_to_anggota_table.php b/database/migrations/2026_07_04_185342_add_gender_to_anggota_table.php new file mode 100644 index 0000000..cd61fb2 --- /dev/null +++ b/database/migrations/2026_07_04_185342_add_gender_to_anggota_table.php @@ -0,0 +1,28 @@ +call(LayananSeeder::class); } } + diff --git a/database/seeders/FasilitasSeeder.php b/database/seeders/FasilitasSeeder.php index ac268f8..28bcf98 100644 --- a/database/seeders/FasilitasSeeder.php +++ b/database/seeders/FasilitasSeeder.php @@ -26,3 +26,4 @@ public function run(): void } } } + diff --git a/database/seeders/KoleksiSeeder.php b/database/seeders/KoleksiSeeder.php index 7fd8a80..e64b9d3 100644 --- a/database/seeders/KoleksiSeeder.php +++ b/database/seeders/KoleksiSeeder.php @@ -16,3 +16,4 @@ public function run(): void ]); } } + diff --git a/database/seeders/LibrarySeeder.php b/database/seeders/LibrarySeeder.php index 0c04a0c..d3e3dd6 100644 --- a/database/seeders/LibrarySeeder.php +++ b/database/seeders/LibrarySeeder.php @@ -89,3 +89,4 @@ public function run(): void ]); } } + diff --git a/dump.rdb b/dump.rdb new file mode 100644 index 0000000000000000000000000000000000000000..69ffeb5df14fc86852718a111f3c87cdb3be6769 GIT binary patch literal 95 zcmWG?b@2=~Ffg$E#aWb^l3A=LI1C wp;^B;N{drdbaPX44@sX^!~g&Q literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index ec50d63..4fd1046 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,13 @@ { - "name": "bakorwil", + "name": "bakorwil2", "lockfileVersion": 3, "requires": true, "packages": { "": { + "dependencies": { + "laravel-echo": "^2.3.4", + "pusher-js": "^8.5.0" + }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", "axios": "^1.11.0", @@ -813,6 +817,13 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT", + "peer": true + }, "node_modules/@tailwindcss/node": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.15.tgz", @@ -1254,6 +1265,24 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1296,6 +1325,30 @@ "dev": true, "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -1633,6 +1686,19 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/laravel-echo": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.3.4.tgz", + "integrity": "sha512-rpALCIK1uw2SrttcK9P5JzItt5I85RcfXQKUNnkcorzhtKeXi5GS0PVFFBH8ppNo8wnbdBKuD1EtIHgTbXo9FQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "pusher-js": "*", + "socket.io-client": "*" + } + }, "node_modules/laravel-vite-plugin": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz", @@ -1957,6 +2023,13 @@ "node": ">= 0.6" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2032,6 +2105,15 @@ "dev": true, "license": "MIT" }, + "node_modules/pusher-js": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.5.0.tgz", + "integrity": "sha512-V7uzGi9bqOOOyM/6IkJdpFyjGZj7llz1v0oWnYkZKcYLvbz6VcHVLmzKqkvegjuMumpfIEKGLmWHwFb39XFCpw==", + "license": "MIT", + "dependencies": { + "tweetnacl": "^1.0.3" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2107,6 +2189,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2216,6 +2328,12 @@ "dev": true, "license": "0BSD" }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/vite": { "version": "7.1.11", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", @@ -2333,6 +2451,37 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index af0db45..dfc9975 100644 --- a/package.json +++ b/package.json @@ -13,5 +13,9 @@ "laravel-vite-plugin": "^2.0.0", "tailwindcss": "^4.0.0", "vite": "^7.0.7" + }, + "dependencies": { + "laravel-echo": "^2.3.4", + "pusher-js": "^8.5.0" } } diff --git a/public/.htaccess b/public/.htaccess index 2e25a42..51c8f28 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -8,28 +8,86 @@ +# ========================================= +# REWRITE LARAVEL +# ========================================= + Options -MultiViews -Indexes RewriteEngine On - # Handle Authorization Header + # Authorization Header RewriteCond %{HTTP:Authorization} . RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] - # Handle X-XSRF-Token Header + # XSRF Token Header RewriteCond %{HTTP:x-xsrf-token} . RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] - # Redirect Trailing Slashes If Not A Folder... + # Remove Trailing Slash RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_URI} (.+)/$ RewriteRule ^ %1 [L,R=301] - # Send Requests To Front Controller... + # Front Controller RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L] + + +# ========================================= +# CACHE BROWSER +# ========================================= + + + ExpiresActive On + + # CSS & JS + ExpiresByType text/css "access plus 1 month" + ExpiresByType application/javascript "access plus 1 month" + ExpiresByType application/x-javascript "access plus 1 month" + + # JSON + ExpiresByType application/json "access plus 1 week" + + # Images + ExpiresByType image/png "access plus 1 month" + ExpiresByType image/jpeg "access plus 1 month" + ExpiresByType image/gif "access plus 1 month" + ExpiresByType image/webp "access plus 1 month" + ExpiresByType image/svg+xml "access plus 1 month" + + # Fonts + ExpiresByType font/woff "access plus 1 month" + ExpiresByType font/woff2 "access plus 1 month" + + # Default + ExpiresDefault "access plus 2 days" + + + +# ========================================= +# GZIP COMPRESSION +# ========================================= + + + AddOutputFilterByType DEFLATE text/plain + AddOutputFilterByType DEFLATE text/html + AddOutputFilterByType DEFLATE text/xml + AddOutputFilterByType DEFLATE text/css + + AddOutputFilterByType DEFLATE application/xml + AddOutputFilterByType DEFLATE application/xhtml+xml + AddOutputFilterByType DEFLATE application/rss+xml + + AddOutputFilterByType DEFLATE application/javascript + AddOutputFilterByType DEFLATE application/x-javascript + AddOutputFilterByType DEFLATE application/json + + AddOutputFilterByType DEFLATE image/svg+xml + + \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..730f9c3ef1c80021db7a607cf30a1a86c0cf0401 100644 GIT binary patch literal 99637 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


la12FvU%p>B9!1=PqkhPhSbZru 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 literal 0 HcmV?d00001 diff --git a/public/index.php b/public/index.php index 58bac35..3f67ec7 100644 --- a/public/index.php +++ b/public/index.php @@ -15,3 +15,4 @@ $response->send(); $kernel->terminate($request, $response); + diff --git a/resources/views/admin/anggota/cetak.blade.php b/resources/views/admin/anggota/cetak.blade.php deleted file mode 100644 index ff34ef8..0000000 --- a/resources/views/admin/anggota/cetak.blade.php +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - Kartu Anggota {{ $member->nama }} - - - - - - - Cetak Kartu - -
-
- Logo BAKORWIL -
-

BAKORWIL III MALANG PROV JATIM

- KARTU TANDA ANGGOTA - PUSTAKA NAWASENA -
-
- -
-
-

{{ $member->nama }}

-

NIP/NIM: {{ $member->nip_nim ?? '-' }}

-

Email: {{ $member->email }}

-

Alamat: {{ $member->alamat }}

-

Nomor Telepon: {{ $member->no_hp ?? '-' }}

-
- -
- @php - $fotoPath = $member->foto ? storage_path('app/public/foto_anggota/'.$member->foto) : public_path('images/default-user.png'); - if(file_exists($fotoPath)){ - $fotoData = base64_encode(file_get_contents($fotoPath)); - $fotoExt = pathinfo($fotoPath, PATHINFO_EXTENSION); - $fotoSrc = "data:image/{$fotoExt};base64,{$fotoData}"; - } else { - $fotoSrc = asset('images/default-user.png'); - } - @endphp - Foto {{ $member->nama }} -
-
- - -
- - - - diff --git a/resources/views/admin/anggota/edit.blade.php b/resources/views/admin/anggota/edit.blade.php index 70479b5..c08904c 100644 --- a/resources/views/admin/anggota/edit.blade.php +++ b/resources/views/admin/anggota/edit.blade.php @@ -1,63 +1,305 @@ @extends('layouts.anggota') @section('content') -
+
+
+ +

Edit Data Anggota πŸ–‹οΈ

+
-
- -

Edit Data Anggota

-
+
+
+ + {{-- 🌟 KOTAK DETEKSI ERROR --}} + @if ($errors->any()) +
+
⚠️ Gagal Menyimpan Perubahan:
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif -
-
-
-
-
Form Edit Anggota πŸ–‹οΈ
-

Perbarui informasi anggota sesuai kebutuhan.

-
+
+
+
+ @csrf + @method('PUT') -
- - @csrf - @method('PUT') +
+ +
+
Informasi Dasar
+ +
+ + + @error('nama') +
{{ $message }}
+ @enderror +
-
- - +
+ + + @error('gender') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('nik_nip') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('email') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('no_hp') +
{{ $message }}
+ @enderror +
-
- - -
+ +
+
Lokasi & Alamat
+ +
+ + + @error('provinsi') +
{{ $message }}
+ @enderror +
-
- - -
+
+ + + @error('kota') +
{{ $message }}
+ @enderror +
-
- - Batal / Kembali - - +
+
+ + + @error('kecamatan') +
{{ $message }}
+ @enderror +
+
+ + + @error('desa') +
{{ $message }}
+ @enderror +
+
+ +
+ + + @error('alamat') +
{{ $message }}
+ @enderror +
- -
+
+ +
+ +
+ + Batal + + +
+
+
+ + + @endsection \ No newline at end of file diff --git a/resources/views/admin/anggota/index.blade.php b/resources/views/admin/anggota/index.blade.php index d98c860..7c7df8b 100644 --- a/resources/views/admin/anggota/index.blade.php +++ b/resources/views/admin/anggota/index.blade.php @@ -1,227 +1,382 @@ @extends('layouts.anggota') @section('content') - - @keyframes slideInDown { - from { - transform: translateY(-50px); - opacity: 0; - } +
+
+
+

Database Anggota

+

Manajemen Identitas, NIK/NIP, dan Verifikasi Anggota

+
+ {{-- πŸ”₯ FIX 1: Pengaman Total Record dengan Ternary Operator --}} +
+ Total Record: {{ ($members && method_exists($members, 'total')) ? $members->total() : 0 }} +
+
- to { - transform: translateY(0); - opacity: 1; - } - } +
+
+
+
+
+ + +
+
+
+ +
+
+ + @if(request('search') || request('status')) + Reset + @endif +
+
+
+
- @keyframes fadeIn { - from { - opacity: 0; - } +
+
+ + + + + + + + + + + + + + @forelse($members as $member) + {{-- πŸ”₯ FIX 2: Menambahkan ID baris agar fungsi injeksi real-time MQTT bekerja sempurna --}} + + + + + + + + + + @empty + + + + @endforelse + +
NoUIDNIK / NIPNama & EmailAlamat LengkapStatusAksi
+ {{ ($members->currentPage()-1) * $members->perPage() + $loop->iteration }} + + {{-- πŸ”₯ FIX 3: Menambahkan class rfid-uid-cell untuk target tangkapan websocket MQTT --}} + {{ $member->uid ?? '-' }} + + {{ $member->nik_nip ?? 'N/A' }} + +
+ {{ $member->nama }} + {{ $member->email }} +
+
+
+ {{ $member->alamat }} +
+
+ + {{ ucfirst($member->status) }} + + +
+ + Edit + - to { - opacity: 1; - } - } - + + + Cetak + -
+ @if($member->status === 'menunggu') +
+ @csrf + +
-
- -

Daftar Anggota

+
+ @csrf + +
+ @endif + +
+ @csrf + @method('DELETE') + +
+
+
+
+ Data anggota tidak tersedia dalam database. +
-
-
-
- Anggota Terdaftar -
-
- -
- -
-
- - - - - - @if(request('search') || request('status')) - - Reset - - @endif -
-
- -
- - - - - - - - - - - - - - @forelse($members as $member) - - - - - - - - - - - - @empty - - - - @endforelse - -
- NoNama - Email - No. - TeleponAlamat - - Status - Aksi
{{ $loop->iteration }}{{ $member->nama }}{{ $member->email }}{{ $member->no_hp ?? '-' }}{{ $member->alamat }} - @if($member->status == 'menunggu') - Menunggu - @elseif($member->status == 'disetujui') - Disetujui - @else - Ditolak - @endif - -
- @if($member->status === 'menunggu') -
- @csrf - -
-
- @csrf - -
- @endif - - - Edit - - - Cetak - -
-
- - Tidak ada data anggota ditemukan. -
-
- -
- {{ $members->links('pagination::bootstrap-4') }} -
- +
-@endsection \ No newline at end of file +
+ + + + + + + + +@endsection + diff --git a/resources/views/admin/anggota/print.blade.php b/resources/views/admin/anggota/print.blade.php new file mode 100644 index 0000000..6671f3b --- /dev/null +++ b/resources/views/admin/anggota/print.blade.php @@ -0,0 +1,261 @@ + + + + + Kartu Anggota {{ $member->nama }} + + + + +
Bagian Depan
+ + + + + + + + + + + + + + +
+ + + + + +
+ + +

BAKORWIL III MALANG PROV JATIM

+ KARTU TANDA ANGGOTA - PUSTAKA NAWASENA +
+
+

{{ $member->nama }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ID/UID Anggota:{{ $member->uid ?? '-' }}
NIP/NIM:{{ $member->nip_nim ?? '-' }}
Email:{{ $member->email }}
Alamat:{{ Str::limit($member->alamat, 45) }}
No. Telepon:{{ $member->no_hp ?? '-' }}
+
+ @php + $fotoPath = $member->foto ? storage_path('app/public/foto_anggota/'.$member->foto) : public_path('images/default-user.png'); + if(file_exists($fotoPath)){ + $fotoData = base64_encode(file_get_contents($fotoPath)); + $fotoExt = pathinfo($fotoPath, PATHINFO_EXTENSION); + $fotoSrc = "data:image/{$fotoExt};base64,{$fotoData}"; + } else { + $fotoSrc = ""; + } + @endphp + @if($fotoSrc) + Foto + @endif +
+ + +
Bagian Belakang
+ + + + + + + +
+

SYARAT & KETENTUAN ANGGOTA

+
    +
  1. Kartu ini bukti keanggotaan resmi Pustaka Nawasena BAKORWIL III Malang.
  2. +
  3. Kartu wajib dibawa setiap melakukan kunjungan, peminjaman, atau pengembalian buku.
  4. +
  5. Peminjam bertanggung jawab penuh atas keutuhan buku yang dipinjam.
  6. +
  7. Jika kartu hilang/rusak, segera laporkan ke petugas perpustakaan.
  8. +
  9. Kartu tidak dapat dipindahtangankan kepada orang lain.
  10. +
+
+ + + + diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index c16db42..e0a2654 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -9,8 +9,7 @@ - - + @@ -18,381 +17,527 @@ :root { --primary-color: #0d47a1; --secondary-color: #ffb300; + --iot-online: #2ecc71; + --glass-bg: rgba(255, 255, 255, 0.85); + } + /* --- LOGIKA MENGHILANGKAN SCROLLBAR --- */ + body::-webkit-scrollbar { + display: none; } - body { font-family: 'Inter', sans-serif; - background: url('{{ asset("images/utama.jpg") }}') no-repeat center center fixed; + background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), + url('{{ asset("images/utama.jpg") }}') no-repeat center center fixed; background-size: cover; min-height: 100vh; - backdrop-filter: blur(4px); + backdrop-filter: blur(5px); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } .glass { - background: rgba(255, 255, 255, 0.7); + background: var(--glass-bg); border-radius: 20px; - box-shadow: 0 10px 40px rgba(31, 38, 135, 0.15); - backdrop-filter: blur(10px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.4); - padding: 30px; - transition: all 0.3s ease-in-out; + padding: 25px; + transition: all 0.3s ease; } .glass:hover { transform: translateY(-5px); - box-shadow: 0 15px 50px rgba(0, 0, 0, 0.2); + box-shadow: 0 15px 40px rgba(0, 0, 0, 0.25); } .navbar-custom { - background: rgba(13, 71, 161, 0.9); - backdrop-filter: blur(8px); - border-bottom: 3px solid var(--secondary-color); + background: rgba(13, 71, 161, 0.95); + backdrop-filter: blur(10px); + border-bottom: 4px solid var(--secondary-color); + padding: 12px 0; } - .navbar-custom .nav-link { - color: #fff !important; - font-weight: 500; - padding: 15px 15px; + .datetime-widget { + background: rgba(0, 0, 0, 0.4); + padding: 8px 20px; + border-radius: 50px; + color: white; + border: 1px solid rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + gap: 15px; + font-size: 0.85rem; } - .navbar-custom .nav-link:hover { - color: var(--secondary-color) !important; + .time-text { font-weight: 800; color: var(--secondary-color); font-family: monospace; font-size: 1.1rem; } + .date-text { font-weight: 400; border-left: 1px solid rgba(255,255,255,0.3); padding-left: 15px; } + + .text-shadow-strong { + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); } - .dashboard-title { + .iot-status-pill { + display: inline-flex; + align-items: center; + gap: 10px; + background: white; + padding: 8px 18px; + border-radius: 50px; + font-size: 0.75rem; font-weight: 800; - color: var(--primary-color); - text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 15px rgba(0,0,0,0.2); + letter-spacing: 0.5px; + } + + .dot-pulse { + width: 12px; height: 12px; + background-color: var(--iot-online); + border-radius: 50%; + animation: pulse-green 2s infinite; + } + + @keyframes pulse-green { + 0% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.7); } + 70% { box-shadow: 0 0 0 10px rgba(46, 204, 113, 0); } + 100% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0); } } .stat-box { - border-left: 5px solid var(--primary-color); + border-left: 6px solid var(--primary-color); padding: 20px; border-radius: 15px; - background-color: rgba(255, 255, 255, 0.95); - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); - transition: 0.3s; + background-color: white; height: 100%; + box-shadow: 0 4px 15px rgba(0,0,0,0.1); } - .stat-box:hover { - transform: scale(1.02); - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); - } - - .footer-custom { - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(5px); + /* --- STYLE BADGE NOTIFIKASI --- */ + .badge-floating { + position: absolute; + top: -10px; + right: -10px; + background-color: #dc3545; color: white; - } - - .logo-circle { - width: 48px; - height: 48px; - background-color: #f9f8f5ff; + width: 24px; + height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; - overflow: hidden; - margin-right: 10px; + font-size: 0.7rem; + font-weight: 800; + border: 2px solid white; + z-index: 99; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); } - .logo-circle img { - width: 32px; - height: 32px; - object-fit: contain; + .logo-circle { + width: 45px; height: 45px; + background-color: white; + border-radius: 50%; + display: flex; align-items: center; justify-content: center; + overflow: hidden; margin-right: 12px; } - .subtitle-small { - font-size: 0.8rem; - font-weight: 500; - color: #555; + .logo-circle img { width: 30px; height: 30px; object-fit: contain; } + + .menu-link:hover .glass { + background: white; + border-color: var(--primary-color); } + /* Judul Utama Kotak Feedback */ +.title-feedback { + color: #ffffff; /* Biru Navy khas profesional */ + font-weight: 800; /* Sangat Tebal */ + text-transform: uppercase; + letter-spacing: 1px; + font-size: 1.1rem; +} + +.avatar-initial { + width: 35px; + height: 35px; + background: linear-gradient(135deg, #004d91, #007bff); + color: white; + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; + font-weight: 800; + font-size: 0.85rem; + margin-right: 12px; +} + +.book-title-small { + font-size: 0.85rem; + color: #004d91; /* Judul buku di tabel juga biru */ + font-weight: 700; +} + +.comment-text { + font-size: 0.8rem; + color: #000000; /* Teks ulasan hitam pekat agar mudah dibaca */ + font-style: italic; + max-width: 250px; + line-height: 1.4; + font-weight: 500; +} + +.rating-badge { + background: #fffcf0; + color: #b45309; + padding: 4px 10px; + border-radius: 8px; + font-weight: 700; + font-size: 0.8rem; + display: inline-flex; + align-items: center; + gap: 5px; + border: 1px solid #fef3c7; +} -
-

- Dashboard Administrasi -

-

Sistem Informasi Perpustakaan - Pustaka Nawasena | Bakorwil III - Malang

+ +
+
+

Panel Dashboard Utama

+

Manajemen Perpustakaan Nawasena Bakorwil III Malang.

+
+
+ +
+
+
-
+
-
Buku Paling Sering Dipinjam
-

{{ $mostBorrowedBook->koleksi->title ?? 'Belum ada data' }} -

-

- Dipinjam - {{ $mostBorrowedBook->total ?? 0 }} kali -

+
Koleksi Terfavorit
+

{{ $mostBorrowedBook->koleksi->title ?? 'Belum ada data' }}

-
- @if($mostBorrowedBook->koleksi->cover ?? false) - Cover Buku - @else - - @endif +
+ Terpinjam {{ $mostBorrowedBook->total ?? 0 }}x
-
- -
-
+
-
Jumlah Koleksi
-

{{ number_format($totalBuku ?? 0) }}

-

+5% dari - bulan lalu

+
TOTAL BUKU
+

{{ number_format($totalBuku ?? 0) }}

-
+
-
Total Anggota
-

{{ number_format($anggotaBulanIni ?? 0) }}

-

Aktif bulan ini

+
ANGGOTA
+

{{ number_format($totalAnggota ?? 0) }}

-
+
-
Peminjaman Aktif
-

{{ number_format($totalPeminjaman ?? 0) }}

-

Transaksi berjalan

+
DIPINJAM
+

{{ number_format($totalPeminjaman ?? 0) }}

-
+
-
Pengembalian
-

{{ number_format($totalPengembalian ?? 0) }}

-

Minggu ini

+
KEMBALI
+

{{ number_format($totalPengembalian ?? 0) }}

-
-
-
-
- Tren Minat Baca Pengguna -
-
- -
-
-
+
+
Analisis Minat Baca
+
- - -
-
-
-
- Rating & Feedback Pengguna -
-
- - - - - - - - - - - - - @forelse($ratings ?? [] as $index => $rating) - - - - - - - - - @empty - - - - @endforelse - -
NoNama PenggunaJudul BukuRatingFeedbackTanggal
{{ $index + 1 }}{{ $rating->user->name ?? 'Anonim' }}{{ $rating->transaction->koleksi->title ?? '-' }} - @for($i = 1; $i <= 5; $i++) - - @endfor - {{ $rating->feedback ?? '-' }}{{ date('d M Y', strtotime($rating->created_at)) }}
Belum ada rating atau feedback. -
-
-
-
-
- - -

- Menu Akses Cepat -

- -
- - - - - - - - - - - - + +
+

+ Navigasi Akses Cepat +

+

Kelola data dengan satu klik

+
+ +
+ @livewire('admin.notification-badges') +
+
-