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 0000000..68c301e Binary files /dev/null and b/app.zip differ diff --git a/app/Console/Commands/MqttListen.php b/app/Console/Commands/MqttListen.php new file mode 100644 index 0000000..fbf0b08 --- /dev/null +++ b/app/Console/Commands/MqttListen.php @@ -0,0 +1,329 @@ +connect(null, 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 0000000..69ffeb5 Binary files /dev/null and b/dump.rdb differ 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 e69de29..730f9c3 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ 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') +
+
-