Initial commit

This commit is contained in:
zerobugteam-zekkk 2026-07-23 18:33:05 +07:00
parent 10cf36e178
commit 019375906d
273 changed files with 19136 additions and 5449 deletions

View File

@ -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!** 👍

282
Iot_Nawasena/rfid_suara.ino Normal file
View File

@ -0,0 +1,282 @@
#include <SPI.h>
#include <MFRC522.h>
#include "DFRobotDFPlayerMini.h"
#include <WiFi.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <PubSubClient.h>
/* ================= 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();
}
}
}
}

0
Set-Content Normal file
View File

BIN
app.zip Normal file

Binary file not shown.

View File

@ -0,0 +1,329 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use PhpMqtt\Client\MqttClient;
use Illuminate\Support\Facades\DB;
use App\Models\IotMode;
class MqttListen extends Command
{
protected $signature = 'mqtt:listen';
protected $description = 'Nawasena MQTT Backbone - Integrated Hardware, UI & Admin Registration with State Lock';
public function handle()
{
// Hubungkan ke broker
$mqtt = new MqttClient('127.0.0.1', 1883, 'laravel-backbone-' . uniqid());
try {
$mqtt->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)));
}
}

View File

@ -53,3 +53,4 @@ public function handle()
$this->info('✅ File bahasa "id.json" dan "en.json" berhasil dibuat dan diisi.');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class RfidScanned implements ShouldBroadcast
{
public $data;
public function __construct($data)
{
$this->data = $data;
}
public function broadcastOn()
{
return new Channel('rfid-channel');
}
}
{
\Log::info('EVENT DIKIRIM', $this->data);
return new Channel('rfid-channel');
}

View File

@ -93,3 +93,4 @@ public static function getPages(): array
];
}
}

View File

@ -10,3 +10,4 @@ class CreateBook extends CreateRecord
{
protected static string $resource = BookResource::class;
}

View File

@ -17,3 +17,4 @@ protected function getHeaderActions(): array
];
}
}

View File

@ -17,3 +17,4 @@ protected function getHeaderActions(): array
];
}
}

View File

@ -62,3 +62,4 @@ public static function getPages(): array
];
}
}

View File

@ -10,3 +10,4 @@ class CreateMember extends CreateRecord
{
protected static string $resource = MemberResource::class;
}

View File

@ -17,3 +17,4 @@ protected function getHeaderActions(): array
];
}
}

View File

@ -17,3 +17,4 @@ protected function getHeaderActions(): array
];
}
}

View File

@ -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',
'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';
$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);
$anggota = Anggota::findOrFail($id);
// 1. Validasi Input Form
$validatedData = $request->validate([
'nama' => 'required|string|max:100',
'email' => 'required|email|unique:anggota,email,' . $member->id,
'alamat' => 'required|string|max:255',
'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',
'status' => 'required|in:menunggu,disetujui,ditolak',
'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']);
// 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');
}
}

View File

@ -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'];
// 5. Rating & Feedback
$ratings = Rating::with(['user', 'transaction.koleksi'])
->orderBy('created_at', 'desc')
->take(10)
->get();
$ratings = Rating::with('user', 'transaction.koleksi')->orderBy('created_at', 'desc')->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.'
);
}
}

View File

@ -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()
@ -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,
@ -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,14 +219,23 @@ 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();
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);
}
@ -231,4 +245,72 @@ public function destroy($kode_eksemplar)
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', count($kodese) . ' data koleksi berhasil dihapus sekaligus.');
}
}

View File

@ -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);
}
}
}

View File

@ -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'
));
}
}

View File

@ -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."
);
}
}

View File

@ -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'); }
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\IotMode;
class IotController extends Controller
{
public function mode()
{
$mode = IotMode::first();
return response($mode->mode);
}
}

View File

@ -37,3 +37,4 @@ public function handleGoogleCallback()
}
}
}

View File

@ -49,3 +49,4 @@ public function login($token) {
}
}

View File

@ -42,3 +42,4 @@ public function verify(Request $request)
}

View File

@ -39,3 +39,4 @@ public function register(Request $request)
return redirect()->route('verification.notice');
}
}

View File

@ -22,3 +22,4 @@ public function verify($token)
return redirect()->route('login')->with('success', 'Email berhasil diverifikasi. Silakan login.');
}
}

View File

@ -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,14 +148,16 @@ 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');
if (!$user || ($user->role !== 'admin' && $user->role !== 'super_admin')) {
return redirect()->route('login');
}
return view('admin.security', compact('user'));
}
public function updateProfile(Request $request)
{
$request->validate([
@ -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_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!');
}
}

View File

@ -82,3 +82,4 @@ public function kembalikan($id)
->with('success', 'Buku berhasil dikembalikan!');
}
}

View File

@ -11,3 +11,4 @@ class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

View File

@ -37,3 +37,4 @@ public function index()
));
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use App\Models\BorrowTransaction;
use App\Models\IotMode;
class EspController extends Controller
{
/* Fungsi ini dipanggil oleh JavaScript Web saat ada Scan RFID.
Sesuai dengan URL di Web Anda: /api/esp/check-member?uid=...
*/
public function checkMember(Request $request)
{
// 1. Ambil UID dan bersihkan (Sama seperti logika scan Anda)
$uid = strtoupper(trim($request->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 ?? '-'
]);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Exception;
class GoogleController extends Controller
{
public function redirectToGoogle()
{
return Socialite::driver('google')->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!');
}
}
}

View File

@ -14,3 +14,4 @@ public function index()
return view('home'); // Akan memanggil resources/views/home.blade.php
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\IotMode;
use Illuminate\Support\Facades\Cache;
use PhpMqtt\Client\MqttClient;
class IotModeController extends Controller
{
/**
* Mengupdate mode sistem (Admin/User/Wait)
*/
public function updateMode(Request $request)
{
$mode = $request->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());
}
}
}

View File

@ -215,3 +215,4 @@ public function getPengarang(Request $request)
]);
}
}

View File

@ -12,3 +12,4 @@ public function index()
return view('user.keamanan.security'); // Pastikan view ini dibuat
}
}

View File

@ -33,3 +33,4 @@ public function security(Request $request)
return view('user.keamanan.security', compact('logins', 'totalLogins', 'lastLogin'));
}
}

View File

@ -9,91 +9,207 @@
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');
}
/**
* 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',
'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:2048',
'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);
$namaFoto = time() . '_' . uniqid() . '.' . $file->getClientOriginalExtension();
$file->storeAs('foto_anggota', $namaFoto, 'public');
}
$file->move($folderPath, $namaFoto);
if (!file_exists($folderPath . '/' . $namaFoto)) {
return back()->with('error', 'Foto gagal diupload.');
// 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,
'nip_nim' => $request->nip_nim,
'email' => $request->email,
'alamat' => $request->alamat,
'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))) {
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');

View File

@ -17,96 +17,146 @@
class UserPeminjamanController extends Controller
{
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');
// 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');
}
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');
}
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();
$lastTransaction = $anggota->borrowTransactions()->latest()->first();
return view('user.peminjaman.create', compact('anggota', 'koleksi', 'lastTransaction'));
// Proteksi: Cek status & keterlambatan
if ($peminjaman->status !== 'dipinjam') {
return redirect()->route('user.peminjaman.index')->with('error', 'Status buku tidak valid untuk diperpanjang.');
}
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.');
}
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 berhasil dikirim!');
}
}
return redirect()->route('user.peminjaman.index')
->with('success', 'Pengajuan peminjaman berhasil dikirim! Menunggu persetujuan admin.');
}
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);
}
}

View File

@ -1,15 +1,18 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use App\Models\User;
class UserProfileController extends Controller
{
public function index() {
$user = Auth::user();
// Mengambil data user yang login beserta relasi ke tabel anggota
$user = User::with('anggota')->find(Auth::id());
return view('profile', compact('user'));
}
@ -44,3 +47,4 @@ public function updatePassword(Request $request) {
return redirect()->route('user.profile')->with('success', 'Password berhasil diperbarui!');
}
}

View File

@ -55,3 +55,4 @@ public function store(Request $request, $id)
->with('success', 'Terima kasih! Penilaian Anda telah kami terima 🙏');
}
}

View File

@ -30,3 +30,4 @@ public function anggota()
return view('user.anggota');
}
}

View File

@ -11,3 +11,4 @@ public function index()
return view('user.dashboard');
}
}

View File

@ -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,
];
}

View File

@ -16,3 +16,4 @@ public function handle(Request $request, Closure $next)
abort(403, 'Akses ditolak!');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class BypassNgrok
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// Menggunakan method header() yang lebih aman untuk objek Response Laravel
if (method_exists($response, 'header')) {
$response->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;
}
}

View File

@ -26,3 +26,4 @@ public function handle(Request $request, Closure $next, string $role): Response
return $next($request);
}
}

View File

@ -15,3 +15,4 @@ public function handle(Request $request, Closure $next): Response
return $next($request);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SkipNgrokWarning
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
// 1. Ambil respon halaman yang mau ditampilkan
$response = $next($request);
// 2. Jika responnya valid, sisipkan header bypass sesuai perintah Ngrok
if (method_exists($response, 'header')) {
$response->header('ngrok-skip-browser-warning', 'any_value');
}
return $response;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Jobs;
use App\Models\BorrowTransaction;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessBorrowTransaction implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $data;
public function __construct($data)
{
$this->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'
]);
}
}

View File

@ -0,0 +1,186 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use PhpMqtt\Client\MqttClient;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\IotMode;
class ProcessPeminjamanJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $bookId;
protected $userId;
/**
* Maksimal retry
*/
public $tries = 3;
/**
* Timeout job
*/
public $timeout = 30;
public function __construct($bookId, $userId)
{
$this->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());
}
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use PhpMqtt\Client\MqttClient;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\IotMode;
class ProcessPengembalianJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $data;
public function __construct($data)
{
$this->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()
);
}
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Livewire\Admin;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
use App\Models\Anggota;
use App\Models\BorrowTransaction;
class NotificationBadges extends Component
{
public $pendingMembers = 0;
public $pendingLoans = 0;
public function mount()
{
$this->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,
]);
}
}

View File

@ -24,3 +24,4 @@ public function build()
->markdown('emails.admin.anggota_baru');
}
}

View File

@ -30,3 +30,4 @@ public function build()
->view('emails.anggota_baru_menunggu');
}
}

View File

@ -27,3 +27,4 @@ public function build()
]);
}
}

View File

@ -23,3 +23,4 @@ public function build()
->view('emails.buku_siap_diambil');
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Mail;
use Illuminate\Mail\Mailable;
class JatuhTempoMail extends Mailable
{
public $transaction;
public function __construct($transaction)
{
$this->transaction = $transaction;
}
public function build()
{
return $this->subject('Pengingat Jatuh Tempo Buku')
->view('emails.jatuh_tempo');
}
}

View File

@ -23,3 +23,4 @@ public function build()
->view('emails.peminjaman_ditolak');
}
}

View File

@ -26,3 +26,4 @@ public function build()
->view('emails.peminjaman_menunggu');
}
}

View File

@ -24,3 +24,4 @@ public function build()
->view('emails.pengembalian-menunggu');
}
}

View File

@ -23,3 +23,4 @@ public function build()
->markdown('emails.peringatan');
}
}

View File

@ -24,3 +24,4 @@ public function build()
->view('emails.perpanjangan_disetujui');
}
}

View File

@ -24,3 +24,4 @@ public function build()
->view('emails.perpanjangan_ditolak');
}
}

View File

@ -24,3 +24,4 @@ public function build()
->view('emails.perpanjangan_menunggu');
}
}

View File

@ -31,3 +31,4 @@ public function build()
]);
}
}

View File

@ -24,3 +24,4 @@ public function build()
->view('emails.status_anggota_diperbarui');
}
}

View File

@ -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();
}
}

View File

@ -37,3 +37,4 @@ public function getFormattedTitleAttribute()
return ucwords(strtolower($this->judul));
}
}

View File

@ -109,9 +109,15 @@ public function markAsDitolak()
public function markAsDikembalikan()
{
// 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::today();
$this->save();
$this->tanggal_kembali = \Carbon\Carbon::today();
}
public function markAsPengembalianMenunggu()
@ -231,3 +237,4 @@ public static function boot()
});
}
}

View File

@ -15,3 +15,4 @@ class Fasilitas extends Model
'deskripsi',
];
}

16
app/Models/IotMode.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class IotMode extends Model
{
protected $table = 'iot_modes';
protected $fillable = [
'mode',
'book_id'
];
}

View File

@ -8,3 +8,4 @@ class Item extends Model
{
//
}

View File

@ -23,6 +23,7 @@ class Koleksi extends Model
'isbn_issn',
'publisher_id',
'publish_year',
'tahun_terbit',
'collation',
'series_title',
'call_number',
@ -116,3 +117,4 @@ public function hasBarcode()
return $this->barcode && Storage::disk('public')->exists($this->barcode);
}
}

View File

@ -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;
// Kode isi model kamu yang lain tetap biarkan di bawah ini...
}
public function user() {
return $this->belongsTo(User::class);
}
}

View File

@ -29,3 +29,4 @@ public function koleksi()
return $this->belongsTo(Koleksi::class);
}
}

View File

@ -8,3 +8,4 @@ class PublishPlace extends Model
{
//
}

View File

@ -9,3 +9,4 @@ class Publisher extends Model
protected $guarded = ['id'];
}

View File

@ -26,3 +26,4 @@ public function user()
return $this->belongsTo(User::class);
}
}

View File

@ -12,3 +12,4 @@ class Setting extends Model
'logo',
];
}

View File

@ -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());
}
}

View File

@ -76,3 +76,4 @@ public static function detectLocation()
return null;
}
}

View File

@ -46,3 +46,4 @@ public function toArray(object $notifiable): array
];
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Observers;
use App\Models\IotMode;
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
class IotModeObserver
{
/**
* =========================
* TRIGGER SETIAP UPDATE DB
* =========================
*/
public function updated(IotMode $iotMode): void
{
logger()->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());
}
}
}

View File

@ -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);
}
}
}

View File

@ -56,3 +56,4 @@ public function panel(Panel $panel): Panel
]);
}
}

View File

@ -33,3 +33,4 @@ public function boot(): void
});
}
}

View File

@ -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();

196
bootstrap/cache/pac5DA9.tmp vendored Normal file
View File

@ -0,0 +1,196 @@
<?php return array (
'anourvalar/eloquent-serialize' =>
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',
),
),
);

View File

@ -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' =>
@ -194,3 +202,4 @@
),
),
);

297
bootstrap/cache/ser864F.tmp vendored Normal file
View File

@ -0,0 +1,297 @@
<?php return array (
'providers' =>
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 (
),
),
);

View File

@ -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 (
),
@ -295,3 +303,4 @@
),
),
);

View File

@ -4,3 +4,4 @@
App\Providers\AppServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
];

View File

@ -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": {

1225
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -62,3 +62,4 @@
],
];

View File

@ -58,3 +58,4 @@
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

83
config/broadcasting.php Normal file
View File

@ -0,0 +1,83 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
*/
'default' => 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',
],
],
];

Some files were not shown because too many files have changed in this diff Show More