Project Bakorwil

This commit is contained in:
AHMAD_ZEKI_SAPUTRA 2026-07-23 19:11:17 +07:00
commit d9dfc54f9a
353 changed files with 47737 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[compose.yaml]
indent_size = 4

65
.env.example Normal file
View File

@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

10
.gitattributes vendored Normal file
View File

@ -0,0 +1,10 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore
public/pdf/magazine.pdf filter=lfs diff=lfs merge=lfs -text

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.fleet
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
Homestead.json
Homestead.yaml
Thumbs.db

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

28
README.md Normal file
View File

@ -0,0 +1,28 @@
# Pustaka Nawasena
Pustaka Nawasena adalah sistem perpustakaan digital yang menyediakan layanan bagi pengguna dan pengelolaan bagi admin. Sistem ini memudahkan pencarian koleksi buku, peminjaman, serta manajemen data anggota dan fasilitas perpustakaan.
## Fitur Utama
### Fitur Admin
- Manajemen koleksi buku: tambah, edit, hapus buku
- Registrasi dan manajemen anggota
- Pantau peminjaman dan pengembalian buku
- Laporan aktivitas perpustakaan (statistik, buku populer, anggota aktif)
- Pengaturan fasilitas dan layanan perpustakaan
### Fitur User
- Pencarian koleksi buku dengan filter kategori, pengarang, dan judul
- Melihat detail buku termasuk ketersediaan
- Melakukan peminjaman buku secara mandiri di layanan perpus bakorwil 3 malang
- Melihat riwayat peminjaman dan status pengembalian
- Update profil dan informasi anggota
## Tujuan
Meningkatkan efisiensi pengelolaan perpustakaan dan memberikan pengalaman pengguna yang mudah dalam mengakses, meminjam, dan menemukan koleksi buku di Pustaka Nawasena.
## Cara Mulai
1. Clone repository:
```bash
git clone https://gitlab.jatimprov.go.id/bakorwil-iii-malang/pustaka-nawasena.git

0
Set-Content Normal file
View File

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

@ -0,0 +1,56 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class SetupLanguages extends Command
{
protected $signature = 'lang:setup';
protected $description = 'Membuat dan mengisi file bahasa Indonesia dan Inggris secara otomatis';
public function handle()
{
$langPath = resource_path('lang');
// Buat folder lang kalau belum ada
if (!File::exists($langPath)) {
File::makeDirectory($langPath, 0755, true);
$this->info('📁 Folder "resources/lang" berhasil dibuat.');
}
// Isi default Bahasa Indonesia
$idData = [
"Welcome" => "Selamat Datang",
"Search" => "Pencarian",
"Book Collection" => "Koleksi Buku",
"Language" => "Bahasa",
"Filter" => "Filter",
"Apply" => "Terapkan",
"Reset" => "Atur Ulang",
"Available" => "Tersedia",
"Borrowed" => "Dipinjam"
];
// Isi default Bahasa Inggris
$enData = [
"Welcome" => "Welcome",
"Search" => "Search",
"Book Collection" => "Book Collection",
"Language" => "Language",
"Filter" => "Filter",
"Apply" => "Apply",
"Reset" => "Reset",
"Available" => "Available",
"Borrowed" => "Borrowed"
];
// Simpan file JSON
File::put($langPath . '/id.json', json_encode($idData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
File::put($langPath . '/en.json', json_encode($enData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
$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

@ -0,0 +1,96 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\BookResource\Pages;
use App\Models\Book;
use Filament\Forms;
use Filament\Tables;
use Filament\Resources\Resource;
use Filament\Tables\Columns\TextColumn;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Grid;
class BookResource extends Resource
{
protected static ?string $model = Book::class;
protected static ?string $navigationIcon = 'heroicon-o-book-open';
protected static ?string $navigationLabel = 'Manajemen Koleksi';
protected static ?string $pluralLabel = 'Koleksi Buku';
protected static ?string $slug = 'books';
protected static ?string $navigationGroup = '📚 Manajemen Perpustakaan';
public static function form(Forms\Form $form): Forms\Form
{
return $form
->schema([
Section::make('Informasi Buku')
->description('Lengkapi data bibliografi koleksi perpustakaan.')
->schema([
Grid::make(2)->schema([
TextInput::make('judul')
->label('Judul Buku')
->required()
->maxLength(255),
TextInput::make('penulis')
->label('Penulis')
->maxLength(255),
TextInput::make('penerbit')
->label('Penerbit')
->maxLength(255),
TextInput::make('tahun_terbit')
->label('Tahun Terbit')
->numeric()
->minValue(1900)
->maxValue(date('Y')),
Select::make('kategori')
->label('Kategori')
->options([
'Fiksi' => 'Fiksi',
'Non-Fiksi' => 'Non-Fiksi',
'Jurnal' => 'Jurnal',
'Referensi' => 'Referensi',
'Lainnya' => 'Lainnya',
]),
TextInput::make('jumlah_eksemplar')
->label('Jumlah Eksemplar')
->numeric()
->default(1)
->minValue(1),
]),
]),
]);
}
public static function table(Tables\Table $table): Tables\Table
{
return $table
->columns([
TextColumn::make('judul')->label('Judul Buku')->searchable()->sortable(),
TextColumn::make('penulis')->label('Penulis')->sortable(),
TextColumn::make('penerbit')->label('Penerbit')->sortable(),
TextColumn::make('tahun_terbit')->label('Tahun'),
TextColumn::make('kategori')->label('Kategori')->badge(),
TextColumn::make('jumlah_eksemplar')->label('Stok')->color('success'),
TextColumn::make('created_at')->label('Ditambahkan')->date(),
])
->defaultSort('created_at', 'desc');
}
public static function getPages(): array
{
return [
'index' => Pages\ListBooks::route('/'),
'create' => Pages\CreateBook::route('/create'),
'edit' => Pages\EditBook::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Filament\Resources\BookResource\Pages;
use App\Filament\Resources\BookResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateBook extends CreateRecord
{
protected static string $resource = BookResource::class;
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\BookResource\Pages;
use App\Filament\Resources\BookResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditBook extends EditRecord
{
protected static string $resource = BookResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\BookResource\Pages;
use App\Filament\Resources\BookResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListBooks extends ListRecords
{
protected static string $resource = BookResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\MemberResource\Pages;
use App\Filament\Resources\MemberResource\RelationManagers;
use App\Models\Member;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class MemberResource extends Resource
{
protected static ?string $model = Member::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
//
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListMembers::route('/'),
'create' => Pages\CreateMember::route('/create'),
'edit' => Pages\EditMember::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Filament\Resources\MemberResource\Pages;
use App\Filament\Resources\MemberResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateMember extends CreateRecord
{
protected static string $resource = MemberResource::class;
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\MemberResource\Pages;
use App\Filament\Resources\MemberResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditMember extends EditRecord
{
protected static string $resource = MemberResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\MemberResource\Pages;
use App\Filament\Resources\MemberResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListMembers extends ListRecords
{
protected static string $resource = MemberResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,334 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
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)
{
// Ambil input dengan nama variabel yang jelas agar tidak bentrok dengan scope Eloquent ($query)
$searchKeyword = $request->input('search');
$statusFilter = $request->input('status');
// 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);
// 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)
{
$anggota = Anggota::findOrFail($id);
// 1. Validasi Input Form
$validatedData = $request->validate([
'nama' => 'required|string|max:100',
'gender' => 'required|in:Laki-laki,Perempuan',
'nik_nip' => 'required|string|max:255|unique:anggota,nik_nip,' . $anggota->id,
'email' => 'required|email|unique:anggota,email,' . $anggota->id,
'no_hp' => 'nullable|string|max:20',
'alamat' => 'required|string|max:255', // Alamat lengkap gabungan dari JS Blade
'provinsi' => 'nullable|string',
'kota' => 'nullable|string',
'kecamatan' => 'nullable|string',
'desa' => 'nullable|string',
]);
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! 🎉');
} 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);
$qrCode = base64_encode(QrCode::format('svg')->size(120)->generate($member->nama));
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']);
// 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()
]);
// 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)
{
$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

@ -0,0 +1,141 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Anggota;
use App\Models\Koleksi;
use App\Models\BorrowTransaction;
use App\Models\Rating;
use Illuminate\Support\Facades\DB;
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 = 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();
/**
* 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')
->orderBy('bulan')
->pluck('total', 'bulan')
->toArray();
$chartDataFull = [];
for ($i = 1; $i <= 12; $i++) {
$chartDataFull[] = $chartData[$i] ?? 0;
}
$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();
// 6. Buku Terpopuler
$mostBorrowedBook = BorrowTransaction::select('koleksi_id')
->selectRaw('COUNT(*) as total')
->groupBy('koleksi_id')
->orderByDesc('total')
->with('koleksi')
->first();
// 7. Return ke View
return view('admin.dashboard', compact(
'jumlahJudul',
'jumlahEksemplar',
'jumlahUser',
'totalBuku',
'totalAnggota',
'anggotaBulanIni',
'totalPeminjaman',
'totalPengembalian',
'pendingMembers',
'pendingLoans',
'chartDataFull',
'bulanLabel',
'ratings',
'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

@ -0,0 +1,316 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Koleksi;
use App\Models\Publisher;
use App\Models\PublishPlace;
use App\Models\Setting;
use Illuminate\Support\Facades\Storage;
use Barryvdh\DomPDF\Facade\Pdf;
use Milon\Barcode\DNS1D;
class KoleksiController extends Controller
{
public function __construct()
{
}
public function index(Request $request)
{
$query = Koleksi::query();
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('sor', 'like', "%{$search}%")
->orWhere('classification', 'like', "%{$search}%")
->orWhere('kode_eksemplar', 'like', "%{$search}%");
});
}
$results = $query->orderBy('input_date', 'desc')
->paginate(20)
->withQueryString();
return view('admin.koleksi.index', compact('results'));
}
public function create()
{
$publishers = Publisher::all();
$publishPlaces = PublishPlace::all();
return view('admin.koleksi.create', compact('publishers', 'publishPlaces'));
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'sor' => 'nullable|string|max:255',
'jumlah_eksemplar' => 'required|integer|min:1',
'publisher_name' => 'nullable|string|max:255',
'publish_place_name' => 'nullable|string|max:255',
'publish_year' => 'nullable|integer',
'isbn_issn' => 'nullable|string|max:50',
'edition' => 'nullable|string|max:100',
'classification' => 'nullable|string|max:100',
'labels' => 'nullable|string|max:255',
'image' => 'nullable|file|mimes:jpg,jpeg,png,gif',
'opac_hide' => 'nullable|boolean',
'promoted' => 'nullable|boolean',
]);
$jumlah = $request->jumlah_eksemplar;
$imagePath = $request->hasFile('image') ? $request->file('image')->store('images', 'public') : null;
for ($i = 1; $i <= $jumlah; $i++) {
$kodeEksemplar = $this->generateKodeEksemplar();
$barcodePath = $this->generateBarcode($kodeEksemplar);
Koleksi::create([
'title' => $request->title,
'sor' => $request->sor,
'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,
'labels' => $request->labels,
'image' => $imagePath,
'barcode' => $barcodePath,
'kode_eksemplar' => $kodeEksemplar,
'input_date' => now(),
'last_update' => now(),
'uid' => auth()->id(),
'opac_hide' => $request->opac_hide ?? 0,
'promoted' => $request->promoted ?? 0,
]);
}
return redirect()->route('admin.koleksi.index')
->with('success', '✅ Koleksi berhasil ditambahkan dengan Barcode.');
}
protected function generateKodeEksemplar()
{
do {
$random = str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
$kode = 'P' . $random . 'S';
} while (Koleksi::where('kode_eksemplar', $kode)->exists());
return $kode;
}
protected function generateBarcode($kodeEksemplar)
{
$barcode = new DNS1D();
$barcode->setStorPath(storage_path('app/public/barcodes'));
if (!Storage::disk('public')->exists('barcodes')) {
Storage::disk('public')->makeDirectory('barcodes');
}
$barcodePng = $barcode->getBarcodePNG($kodeEksemplar, 'C39+', 3, 100);
$barcodePath = "barcodes/{$kodeEksemplar}.png";
Storage::disk('public')->put($barcodePath, base64_decode($barcodePng));
return $barcodePath;
}
public function printBarcodeSingle($kode_eksemplar)
{
$koleksi = Koleksi::where('kode_eksemplar', $kode_eksemplar)->firstOrFail();
$setting = Setting::first();
$pdf = Pdf::loadView('admin.koleksi.label_barcode_single', compact('koleksi', 'setting'))
->setPaper([0, 0, 200, 120], 'portrait');
return $pdf->stream("barcode_{$koleksi->kode_eksemplar}.pdf");
}
public function printBarcodeMultiple(Request $request)
{
$selected = $request->input('selected', []);
if (!is_array($selected)) {
$selected = explode(',', $selected);
}
if (empty($selected)) {
return redirect()->back()->with('error', 'Pilih minimal satu koleksi untuk dicetak.');
}
$koleksiList = Koleksi::whereIn('kode_eksemplar', $selected)->get();
$setting = Setting::first();
$pdf = Pdf::loadView('admin.koleksi.label_barcode_multiple', compact('koleksiList', 'setting'))
->setPaper('a4', 'portrait');
return $pdf->stream("barcode_massal.pdf");
}
public function edit($kode_eksemplar)
{
$koleksi = Koleksi::where('kode_eksemplar', $kode_eksemplar)->firstOrFail();
$publishers = Publisher::all();
$publishPlaces = PublishPlace::all();
return view('admin.koleksi.edit', compact('koleksi', 'publishers', 'publishPlaces'));
}
public function update(Request $request, $kode_eksemplar)
{
$koleksi = Koleksi::where('kode_eksemplar', $kode_eksemplar)->firstOrFail();
$request->validate([
'title' => 'required|string|max:255',
'sor' => 'nullable|string|max:255',
'publisher_name' => 'nullable|string|max:255',
'publish_place_name' => 'nullable|string|max:255',
'publish_year' => 'nullable|integer',
'isbn_issn' => 'nullable|string|max:50',
'edition' => 'nullable|string|max:100',
'classification' => 'nullable|string|max:100',
'labels' => 'nullable|string|max:255',
'image' => 'nullable|file|mimes:jpg,jpeg,png,gif',
'opac_hide' => 'nullable|boolean',
'promoted' => 'nullable|boolean',
]);
if ($request->hasFile('image')) {
if ($koleksi->image) {
Storage::disk('public')->delete($koleksi->image);
}
$koleksi->image = $request->file('image')->store('images', 'public');
}
$koleksi->update([
'title' => $request->title,
'sor' => $request->sor,
'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,
'labels' => $request->labels,
'image' => $koleksi->image,
'opac_hide' => $request->opac_hide ?? 0,
'promoted' => $request->promoted ?? 0,
'last_update' => now(),
]);
return redirect()->route('admin.koleksi.index')
->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);
}
$koleksi->delete();
return redirect()->route('admin.koleksi.index')
->with('success', '✅ Koleksi berhasil dihapus.');
}
// ... (di dalam KoleksiController)
public function printMultipleBarcode(Request $request)
{
// 1. Validasi input
if (!$request->has('kode_eksemplar') || !is_array($request->kode_eksemplar)) {
return redirect()->back()->with('error', 'Silakan centang minimal satu koleksi terlebih dahulu.');
}
$kodeEksemplarArray = $request->kode_eksemplar;
// --- BAGIAN INI DIPERBAIKI ---
// Mengurutkan data agar sama persis dengan urutan centang (array input)
$orderList = "'" . implode("','", $kodeEksemplarArray) . "'";
$items = Koleksi::whereIn('kode_eksemplar', $kodeEksemplarArray)
->orderByRaw("FIELD(kode_eksemplar, $orderList)")
->get();
// -----------------------------
if ($items->isEmpty()) {
return redirect()->back()->with('error', 'Data koleksi yang Anda pilih tidak ditemukan.');
}
$setting = Setting::first();
// 2. Render PDF
$pdf = Pdf::loadView('admin.koleksi.barcode_multiple', compact('items', 'setting'));
// 3. Atur kertas
$pdf->setPaper('a4', 'portrait');
// 4. Stream PDF
return $pdf->stream('Label_Barcode_Massal.pdf');
}
public function destroyMultiple(Request $request)
{
$kodese = $request->input('kode_eksemplar', []);
if (empty($kodese)) {
return redirect()->back()->with('error', 'Tidak ada item yang dipilih.');
}
// Ambil semua data koleksi yang kodenya sesuai array
$koleksis = Koleksi::whereIn('kode_eksemplar', $kodese)->get();
foreach ($koleksis as $koleksi) {
// Contoh logika hapus file cover jika tidak dipakai eksemplar lain
if ($koleksi->image) {
$fotoMasihDipakai = Koleksi::where('image', $koleksi->image)
->whereNotIn('kode_eksemplar', $kodese)
->exists();
if (!$fotoMasihDipakai) {
Storage::disk('public')->delete($koleksi->image);
}
}
// Hapus file barcode asli jika disimpan fisik
if ($koleksi->barcode) {
Storage::disk('public')->delete($koleksi->barcode);
}
$koleksi->delete();
}
return redirect()->route('admin.koleksi.index')->with('success', count($kodese) . ' data koleksi berhasil dihapus sekaligus.');
}
}

View File

@ -0,0 +1,318 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\BorrowTransaction;
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()
{
// 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.');
}
return Storage::download($filePath, $transaction->surat_pengantar);
}
public function setujui(Request $request, $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',
'tanggal_kembali' => 'nullable|date|after_or_equal:pickup_date',
]);
$tanggalKembali = $request->tanggal_kembali
? Carbon::parse($request->tanggal_kembali)
: Carbon::parse($request->pickup_date)->addDays(7);
// Update Data
$transaction->update([
'pickup_date' => $request->pickup_date,
'pickup_time' => $request->pickup_time,
'tanggal_kembali' => $tanggalKembali,
'status' => 'disetujui'
]);
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($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 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.');
}
$transaction->markAsDipinjam();
IotMode::query()->update([
'mode' => 'standby',
'updated_at' => now()
]);
return redirect()->route('admin.peminjaman')->with('success', 'Buku berhasil diambil.');
}
public function tolak($id)
{
$transaction = BorrowTransaction::with('anggota.user')->findOrFail($id);
if (!in_array($transaction->status, ['menunggu', 'pending'])) {
return redirect()->back()->with('error', 'Transaksi ini sudah diproses.');
}
$transaction->markAsDitolak();
$recipientEmail = $transaction->anggota->user->email ?? $transaction->anggota->email;
if ($recipientEmail) {
try {
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.');
}
public function setujuiPerpanjangan(Request $request, $id)
{
$transaction = BorrowTransaction::with('anggota.user')->findOrFail($id);
if ($transaction->status !== 'perpanjangan_menunggu') {
return back()->with('error', 'Permohonan perpanjangan tidak ditemukan.');
}
// 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);
$transaction->update([
'tanggal_kembali' => $newTanggalKembali,
'status' => 'dipinjam'
]);
$recipientEmail = $transaction->anggota->user->email ?? $transaction->anggota->email;
if ($recipientEmail) {
try {
Mail::to($recipientEmail)->send(new PerpanjanganDisetujuiMail($transaction));
} catch (\Exception $e) {
Log::error('Gagal kirim email PerpanjanganDisetujui: ' . $e->getMessage());
}
}
return back()->with('success', 'Perpanjangan disetujui.');
}
public function tolakPerpanjangan($id)
{
$transaction = BorrowTransaction::with('anggota.user')->findOrFail($id);
if ($transaction->status !== 'perpanjangan_menunggu') {
return back()->with('error', 'Permohonan tidak valid.');
}
$transaction->update(['status' => 'dipinjam']);
$recipientEmail = $transaction->anggota->user->email ?? $transaction->anggota->email;
if ($recipientEmail) {
try {
Mail::to($recipientEmail)->send(new PerpanjanganDitolakMail($transaction));
} catch (\Exception $e) {
Log::error('Gagal kirim email PerpanjanganDitolak: ' . $e->getMessage());
}
}
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

@ -0,0 +1,199 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Koleksi;
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 = 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')
->pluck('total', 'bulan');
$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', 'anggotaBaruBulanIni',
'totalPeminjaman', 'totalPengembalian', 'totalTerlambat', 'totalDenda', 'rasioKeterpakaian',
'wilayahLabels', 'wilayahData',
'userHariIni', 'userBulanIni', 'userTahunIni',
'bukuReferensi', 'bukuRusak', 'bukuHilang', 'totalUsulanBuku',
'genderLaki', 'genderPerempuan', 'bulanLabels', 'dataPeminjaman',
'chartUsersCreatedAt', 'chartAnggotaCreatedAt'
));
}
}

View File

@ -0,0 +1,206 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
class SystemController extends Controller
{
/*
|--------------------------------------------------------------------------
| HALAMAN KELOLA SISTEM
|--------------------------------------------------------------------------
*/
public function index()
{
// 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)
{
// HANYA SUPER ADMIN
if (
!Auth::check() ||
Auth::user()->role !== 'super_admin'
) {
abort(403, 'Akses ditolak.');
}
$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."
);
}
/*
|--------------------------------------------------------------------------
| CABUT ADMIN MENJADI USER
|--------------------------------------------------------------------------
*/
public function removeAdmin(Request $request)
{
// HANYA SUPER ADMIN
if (
!Auth::check() ||
Auth::user()->role !== 'super_admin'
) {
abort(403, 'Akses ditolak.');
}
$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 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."
);
}
/*
|--------------------------------------------------------------------------
| BLOKIR USER
|--------------------------------------------------------------------------
*/
public function block(Request $request)
{
// HANYA SUPER ADMIN
if (
!Auth::check() ||
Auth::user()->role !== 'super_admin'
) {
abort(403, 'Akses ditolak.');
}
$request->validate([
'user_id' => 'required|exists:users,id'
]);
$user = User::find($request->user_id);
if (!$user) {
return back()->with(
'error',
'User tidak ditemukan.'
);
}
// 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."
);
}
/*
|--------------------------------------------------------------------------
| BUKA BLOKIR USER
|--------------------------------------------------------------------------
*/
public function unblock(Request $request)
{
// HANYA SUPER ADMIN
if (
!Auth::check() ||
Auth::user()->role !== 'super_admin'
) {
abort(403, 'Akses ditolak.');
}
$request->validate([
'user_id' => 'required|exists:users,id'
]);
$user = User::find($request->user_id);
if (!$user) {
return back()->with(
'error',
'User tidak ditemukan.'
);
}
$user->blocked_at = null;
$user->save();
return back()->with(
'success',
"Blokir untuk {$user->name} telah dibuka."
);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class AdminController extends Controller
{
public function dashboard() {
$totalBuku = 1250;
return view('admin.dashboard', compact('totalBuku'));
}
public function laporan() { return view('admin.laporan'); }
public function koleksi() { return view('admin.koleksi'); }
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

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use App\Models\User;
class GoogleController extends Controller
{
public function redirectToGoogle()
{
return Socialite::driver('google')->redirect();
}
public function handleGoogleCallback()
{
try {
$googleUser = Socialite::driver('google')->stateless()->user();
$user = User::where('email', $googleUser->getEmail())->first();
if (!$user) {
$user = User::create([
'name' => $googleUser->getName(),
'email' => $googleUser->getEmail(),
'password' => bcrypt(uniqid()),
]);
}
Auth::login($user);
return redirect()->route('home');
} catch (\Exception $e) {
return redirect()->route('login')->with('error', 'Login Google gagal!');
}
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use App\Models\User;
use Carbon\Carbon;
class LoginWithEmailController extends Controller
{
public function showForm() {
return view('auth.login-email');
}
public function sendLink(Request $request) {
$request->validate(['email' => 'required|email|exists:users,email']);
$user = User::where('email', $request->email)->first();
$token = Str::random(64);
$user->email_login_token = $token;
$user->token_expires_at = Carbon::now()->addMinutes(15);
$user->save();
$link = route('login.email.token', $token);
Mail::raw("Klik link ini untuk login: $link", function($message) use($user) {
$message->to($user->email)
->subject('Link Login Sekali Pakai');
});
return back()->with('success', 'Link login sudah dikirim ke email Anda.');
}
public function login($token) {
$user = User::where('email_login_token', $token)
->where('token_expires_at', '>', Carbon::now())
->firstOrFail();
Auth::login($user);
$user->email_login_token = null;
$user->token_expires_at = null;
$user->save();
return redirect()->route('dashboard');
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
class OtpVerificationController extends Controller
{
public function showForm(Request $request)
{
$email = session('email');
if (!$email) {
return redirect()->route('register')->with('error', 'Email tidak ditemukan.');
}
return view('auth.verify-otp', compact('email'));
}
public function verify(Request $request)
{
$request->validate([
'email' => 'required|email',
'otp' => 'required|digits:6',
]);
$user = User::where('email', $request->email)
->where('email_otp', $request->otp)
->first();
if (!$user) {
return back()->with('error', 'Kode OTP salah atau tidak valid.');
}
$user->email_verified_at = now();
$user->email_otp = null;
$user->save();
return redirect()->route('login')->with('success', 'Email berhasil diverifikasi! Silakan login.');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules\Password;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Mail;
use App\Mail\VerifyEmail;
class RegisterController extends Controller
{
public function showRegistrationForm()
{
return view('auth.register');
}
public function register(Request $request)
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Password::min(8)],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => $request->password,
]);
// Kirim email verifikasi
$user->sendEmailVerificationNotification();
return redirect()->route('verification.notice');
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
class VerificationController extends Controller
{
public function verify($token)
{
$user = User::where('verification_token', $token)->first();
if (!$user) {
return redirect()->route('login')->with('error', 'Token verifikasi tidak valid.');
}
$user->email_verified_at = now();
$user->verification_token = null;
$user->save();
return redirect()->route('login')->with('success', 'Email berhasil diverifikasi. Silakan login.');
}
}

View File

@ -0,0 +1,410 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Password;
use App\Models\User;
use App\Models\Koleksi;
use Carbon\Carbon;
class AuthController extends Controller
{
public function showRegister()
{
if (Auth::check()) {
return $this->redirectBasedOnRole();
}
return view('register');
}
public function showLogin()
{
if (Auth::check()) {
return $this->redirectBasedOnRole();
}
return view('login');
}
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'password' => [
'required',
'string',
'min:8',
'regex:/[A-Z]/',
'regex:/[a-z]/',
'regex:/[0-9]/',
'regex:/[.,]/',
'confirmed',
],
], [
'password.min' => 'Kata sandi minimal 8 karakter.',
'password.regex' => 'Kata sandi harus mengandung huruf besar, huruf kecil, angka, dan tanda baca (.,)',
'password.confirmed' => 'Konfirmasi kata sandi tidak cocok.',
]);
$otp = rand(100000, 999999);
$now = Carbon::now();
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'role' => 'user',
'email_otp' => $otp,
'email_otp_expires_at' => $now,
'otp_last_sent_at' => $now,
]);
$this->sendOtpEmail($user, $otp);
return redirect()->route('verify.email.form', ['email' => $user->email])
->with('success', 'Registrasi berhasil! Silakan cek email untuk kode verifikasi (berlaku 1 menit).');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required|string',
]);
$user = User::where('email', $request->email)->first();
if ($user && $user->blocked_at !== null) {
return back()->withErrors([
'email' => 'Akun Anda telah diblokir dan tidak dapat login.'
])->onlyInput('email');
}
if ($user && !$user->email_verified_at) {
return back()->withErrors(['email' => 'Email belum diverifikasi. Silakan verifikasi terlebih dahulu.'])
->onlyInput('email');
}
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();
}
return back()->withErrors(['email' => 'Email atau password salah!'])->onlyInput('email');
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/login');
}
public function dashboard()
{
// Jembatan utama penentu rute setelah melewati guard auth bawaan
return $this->redirectBasedOnRole();
}
public function adminDashboard()
{
$user = Auth::user();
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');
}
return view('user.dashboard', compact('user'));
}
public function profile()
{
$user = Auth::user();
if (!$user) return redirect()->route('login');
return ($user->role === 'admin' || $user->role === 'super_admin')
? view('admin.profile', compact('user'))
: view('user.profile', compact('user'));
}
public function security()
{
$user = Auth::user();
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' && $user->role !== 'super_admin')) {
return redirect()->route('login');
}
return view('admin.security', compact('user'));
}
public function updateProfile(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:users,email,' . Auth::id(),
]);
$user = Auth::user();
$user->name = $request->name;
$user->email = $request->email;
if ($request->filled('new_password')) {
$request->validate([
'current_password' => 'required',
'new_password' => [
'required',
'string',
'min:8',
'regex:/[A-Z]/',
'regex:/[a-z]/',
'regex:/[0-9]/',
'regex:/[.,]/',
'confirmed',
],
]);
if (!Hash::check($request->current_password, $user->password)) {
return back()->withErrors(['current_password' => 'Password saat ini salah']);
}
$user->password = Hash::make($request->new_password);
}
$user->save();
$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()
{
$user = Auth::user();
if (!$user || $user->role !== 'user') return redirect()->route('login');
$koleksi = Koleksi::where('user_id', $user->id)->get();
return view('user.koleksi', compact('user', 'koleksi'));
}
private function redirectBasedOnRole()
{
$user = Auth::user();
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)
{
$email = $request->query('email');
return view('verify-email', compact('email'));
}
public function verifyEmail(Request $request)
{
$request->validate([
'email' => 'required|email',
'otp' => 'required|digits:6',
]);
$user = User::where('email', $request->email)
->where('email_otp', $request->otp)
->first();
if (!$user) {
return back()->withErrors(['otp' => 'Kode verifikasi salah.']);
}
if ($user->email_otp_expires_at) {
$otpCreatedAt = Carbon::parse($user->email_otp_expires_at);
$secondsElapsed = Carbon::now()->diffInSeconds($otpCreatedAt);
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([
'email_verified_at' => now(),
'email_otp' => null,
'email_otp_expires_at' => null,
'otp_last_sent_at' => null,
]);
return redirect()->route('login')->with('success', 'Email berhasil diverifikasi! Silakan login.');
}
public function resendOtp(Request $request)
{
$request->validate(['email' => 'required|email']);
$user = User::where('email', $request->email)->first();
if (!$user) return back()->withErrors(['email' => 'Email tidak ditemukan.']);
if (empty($user->otp_last_sent_at)) {
return $this->sendNewOtp($user);
}
$lastSent = Carbon::parse($user->otp_last_sent_at);
$diff = Carbon::now()->diffInSeconds($lastSent);
if ($diff < 60) {
$remaining = 60 - $diff;
return back()->withErrors(['otp' => "Silakan tunggu {$remaining} detik sebelum kirim ulang OTP."]);
}
return $this->sendNewOtp($user);
}
private function sendNewOtp($user)
{
$otp = rand(100000, 999999);
$now = Carbon::now();
$user->update([
'email_otp' => $otp,
'email_otp_expires_at' => $now,
'otp_last_sent_at' => $now,
]);
$this->sendOtpEmail($user, $otp);
return back()->with('success', 'Kode OTP baru telah dikirim ke email Anda. Berlaku 1 menit.');
}
private function sendOtpEmail($user, $otp)
{
Mail::raw(
"Halo {$user->name},\n\nKode verifikasi akun Anda adalah: {$otp}\nKode ini berlaku selama 1 menit.\n\nTerima kasih,\nBakorwil III Malang",
function ($message) use ($user) {
$message->to($user->email)
->subject('Kode Verifikasi Akun Anda - Bakorwil III Malang');
}
);
}
public function showForgotPasswordForm()
{
return view('auth.forgot-password');
}
public function sendResetLink(Request $request)
{
$request->validate(['email' => 'required|email']);
$status = Password::sendResetLink($request->only('email'));
return $status === Password::RESET_LINK_SENT
? back()->with(['success' => __($status)])
: back()->withErrors(['email' => __($status)]);
}
public function showResetPasswordForm($token)
{
return view('auth.reset-password', ['token' => $token]);
}
public function resetPassword(Request $request)
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => [
'required',
'string',
'min:8',
'regex:/[A-Z]/',
'regex:/[a-z]/',
'regex:/[0-9]/',
'regex:/[.,]/',
'confirmed',
],
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) {
$user->forceFill([
'password' => Hash::make($password),
])->save();
}
);
return $status === Password::PASSWORD_RESET
? redirect()->route('login')->with('success', __($status))
: back()->withErrors(['email' => [__($status)]]);
}
public function updateFoto(Request $request)
{
$request->validate([
'foto' => 'required|image|mimes:jpeg,png,jpg|max:2048',
]);
$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));
}
$file = $request->file('foto');
$namaFile = time() . '_' . $file->getClientOriginalName();
$file->move(public_path('storage/foto'), $namaFile);
$user->foto = $namaFile;
$user->save();
}
return back()->with('success', 'Foto profil berhasil diperbarui!');
}
public function deleteFoto()
{
$user = Auth::user();
if ($user->foto && file_exists(public_path('storage/foto/'.$user->foto))) {
unlink(public_path('storage/foto/'.$user->foto));
}
$user->foto = null;
$user->save();
return back()->with('success', 'Foto profil berhasil dihapus!');
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\BorrowTransaction;
use App\Models\Anggota;
use App\Models\Koleksi;
class BorrowTransactionController extends Controller
{
public function index()
{
$transactions = BorrowTransaction::with(['anggota', 'koleksi'])
->orderBy('tanggal_pinjam', 'desc')
->get();
return view('dashboard.sirkulasi.index', compact('transactions'));
}
public function create()
{
$anggota = Anggota::all();
$koleksi = Koleksi::available()->get();
return view('dashboard.sirkulasi.create', compact('anggota', 'koleksi'));
}
public function store(Request $request)
{
$request->validate([
'anggota_id' => 'required|exists:anggota,id',
'koleksi_id' => 'required|exists:koleksis,biblio_id',
'tanggal_pinjam' => 'required|date',
'tanggal_kembali' => 'nullable|date|after_or_equal:tanggal_pinjam',
'surat_pengantar' => 'nullable|mimes:pdf|max:2048',
]);
$alreadyBorrowed = BorrowTransaction::where('koleksi_id', $request->koleksi_id)
->where('status', 'dipinjam')
->exists();
if ($alreadyBorrowed) {
return redirect()->back()->withErrors(['koleksi_id' => 'Buku ini sedang dipinjam.']);
}
$fileName = null;
if ($request->hasFile('surat_pengantar')) {
$file = $request->file('surat_pengantar');
$fileName = time() . '_' . $file->getClientOriginalName();
$file->storeAs('public/surat_pengantar', $fileName);
}
BorrowTransaction::create([
'anggota_id' => $request->anggota_id,
'koleksi_id' => $request->koleksi_id,
'tanggal_pinjam' => $request->tanggal_pinjam,
'tanggal_kembali' => $request->tanggal_kembali,
'status' => 'dipinjam',
'surat_pengantar' => $fileName,
]);
return redirect()->route('dashboard.sirkulasi.index')
->with('success', 'Transaksi peminjaman berhasil disimpan!');
}
public function kembalikan($id)
{
$transaction = BorrowTransaction::findOrFail($id);
if ($transaction->status != 'dipinjam') {
return redirect()->route('dashboard.sirkulasi.index')
->with('error', 'Transaksi ini sudah dikembalikan.');
}
$transaction->update([
'status' => 'dikembalikan',
'tanggal_kembali' => now(),
]);
return redirect()->route('dashboard.sirkulasi.index')
->with('success', 'Buku berhasil dikembalikan!');
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Koleksi;
use App\Models\Anggota;
use App\Models\BorrowTransaction;
use Carbon\Carbon;
class DashboardController extends Controller
{
public function index()
{
$totalBuku = Koleksi::count();
$totalAnggota = Anggota::count();
$peminjamanAktif = BorrowTransaction::whereNull('tanggal_kembali')->count();
$pengembalian = BorrowTransaction::whereNotNull('tanggal_kembali')
->whereBetween('tanggal_kembali', [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()])
->count();
$bulan = ['Jun','Jul','Agt','Sep','Okt','Nov','Des'];
$jumlahTransaksi = [];
foreach($bulan as $bln){
$count = BorrowTransaction::whereMonth('created_at', date('m', strtotime($bln.' 1')))->count();
$jumlahTransaksi[] = $count;
}
return view('dashboard.index', compact(
'totalBuku',
'totalAnggota',
'peminjamanAktif',
'pengembalian',
'bulan',
'jumlahTransaksi'
));
}
}

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

@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Koleksi;
class HomeController extends Controller
{
public function index()
{
$jilid = Koleksi::count();
$eksemplar = Koleksi::count();
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

@ -0,0 +1,218 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\Koleksi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
class KoleksiController extends Controller
{
public function index(Request $request)
{
$query = Koleksi::with('borrowTransactions');
if ($request->filled('search')) {
$kategori = $request->input('kategori');
$search = $request->input('search');
switch ($kategori) {
case 'judul':
$query->where('title', 'like', "%{$search}%");
break;
case 'pengarang':
$query->where('sor', 'like', "%{$search}%");
break;
case 'isbn':
$query->where(function ($q) use ($search) {
$q->where('isbn_issn', 'like', "%{$search}%")
->orWhere('kode_eksemplar', 'like', "%{$search}%");
});
break;
default:
$query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('sor', 'like', "%{$search}%")
->orWhere('isbn_issn', 'like', "%{$search}%")
->orWhere('kode_eksemplar', 'like', "%{$search}%");
});
break;
}
}
if ($request->filled('tipe')) {
$query->where('tipe', $request->input('tipe'));
}
if ($request->filled('bahasa')) {
$query->where('bahasa', $request->input('bahasa'));
}
if ($request->filled('subjek')) {
$query->where('subjek', $request->input('subjek'));
}
if ($request->filled('tahun_min')) {
$query->where('publish_year', '>=', $request->input('tahun_min'));
}
if ($request->filled('tahun_max')) {
$query->where('publish_year', '<=', $request->input('tahun_max'));
}
if ($request->filled('lokasi')) {
$query->where('lokasi', $request->input('lokasi'));
}
if ($request->filled('status')) {
$status = $request->input('status');
if ($status === 'tersedia') {
$query->whereDoesntHave('borrowTransactions', function ($q) {
$q->whereIn('status', [
'menunggu',
'disetujui',
'dipinjam',
'pengembalian_menunggu'
]);
});
} elseif ($status === 'dipinjam') {
$query->whereHas('borrowTransactions', function ($q) {
$q->whereIn('status', [
'menunggu',
'disetujui',
'dipinjam',
'pengembalian_menunggu'
]);
});
}
}
$koleksi = $query->orderBy('input_date', 'desc')
->paginate(10)
->withQueryString();
$jumlahJilid = Koleksi::count();
return view('user.koleksi', compact('koleksi', 'jumlahJilid'));
}
public function show(Koleksi $koleksi)
{
return view('user.koleksi.show', compact('koleksi'));
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'sor' => 'nullable|string|max:255',
'publisher_id' => 'nullable|string|max:255',
'publish_place_id' => 'nullable|string|max:255',
'publish_year' => 'nullable|integer',
'kode_eksemplar' => 'nullable|string|max:100',
'tipe' => 'nullable|string|max:100',
'bahasa' => 'nullable|string|max:100',
'subjek' => 'nullable|string|max:100',
'lokasi' => 'nullable|string|max:100',
'isbn_issn' => 'nullable|string|max:100',
'image' => 'nullable|image|mimes:jpg,jpeg,png|max:2048',
]);
if ($request->hasFile('image')) {
$file = $request->file('image');
$filename = time() . '_' . $file->getClientOriginalName();
$path = storage_path('app/public/image/' . $filename);
Image::make($file)
->resize(300, null, function ($constraint) {
$constraint->aspectRatio();
})
->save($path);
$validated['image'] = 'image/' . $filename;
}
Koleksi::create($validated);
return redirect()->route('user.koleksi')
->with('success', 'Data buku berhasil ditambahkan.');
}
public function update(Request $request, $id)
{
$koleksi = Koleksi::findOrFail($id);
$validated = $request->validate([
'title' => 'required|string|max:255',
'sor' => 'nullable|string|max:255',
'publisher_id' => 'nullable|string|max:255',
'publish_place_id' => 'nullable|string|max:255',
'publish_year' => 'nullable|integer',
'kode_eksemplar' => 'nullable|string|max:100',
'tipe' => 'nullable|string|max:100',
'bahasa' => 'nullable|string|max:100',
'subjek' => 'nullable|string|max:100',
'lokasi' => 'nullable|string|max:100',
'isbn_issn' => 'nullable|string|max:100',
'image' => 'nullable|image|mimes:jpg,jpeg,png|max:2048',
]);
if ($request->hasFile('image')) {
if ($koleksi->image && Storage::exists('public/' . $koleksi->image)) {
Storage::delete('public/' . $koleksi->image);
}
$file = $request->file('image');
$filename = time() . '_' . $file->getClientOriginalName();
$path = storage_path('app/public/image/' . $filename);
Image::make($file)
->resize(300, null, function ($constraint) {
$constraint->aspectRatio();
})
->save($path);
$validated['image'] = 'image/' . $filename;
}
$koleksi->update($validated);
return redirect()->route('user.koleksi')
->with('success', 'Data buku berhasil diperbarui.');
}
public function homepage()
{
$jumlahJilid = Koleksi::count('title');
return view('home', compact('jumlahJilid'));
}
public function getPengarang(Request $request)
{
$judul = $request->query('judul');
if (!$judul) {
return response()->json(['pengarang' => '']);
}
$buku = Koleksi::where('title', 'like', '%' . $judul . '%')->first();
return response()->json([
'pengarang' => $buku->sor ?? ''
]);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class SecurityController extends Controller
{
public function index()
{
// Bisa return view
return view('user.keamanan.security'); // Pastikan view ini dibuat
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\UserLogin;
use Illuminate\Support\Facades\Auth;
class UserController extends Controller
{
/**
* Halaman keamanan: menampilkan riwayat login user
*/
public function security(Request $request)
{
// Ambil user yang login saat ini
$user = Auth::user();
// Ambil login history user, terbaru terlebih dahulu, dengan pagination
$logins = UserLogin::where('user_id', $user->id)
->orderBy('created_at', 'desc')
->paginate(10); // 10 record per halaman
// Tambahkan info tambahan jika perlu
// Contoh: total login, terakhir login
$totalLogins = UserLogin::where('user_id', $user->id)->count();
$lastLogin = UserLogin::where('user_id', $user->id)
->orderBy('created_at', 'desc')
->first();
// Kirim data ke view
return view('user.keamanan.security', compact('logins', 'totalLogins', 'lastLogin'));
}
}

View File

@ -0,0 +1,217 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Anggota;
use Illuminate\Support\Facades\Auth;
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',
'gender' => 'required|in:Laki-laki,Perempuan',
'nik_nip' => $nikNipRule, // 💡 SUDAH DISESUAIKAN
'provinsi' => 'required|string|max:255',
'kota' => 'required|string|max:255',
'kecamatan' => 'required|string|max:255',
'desa' => 'required|string|max:255',
'kode_pos' => 'required|string|max:5',
'alamat' => 'required|string|min:10',
'no_hp' => 'nullable|string|max:20',
'foto' => 'nullable|image|mimes:jpg,jpeg,png|max:5120',
]);
// 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 = $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() . '_' . uniqid() . '.' . $file->getClientOriginalExtension();
$file->storeAs('foto_anggota', $namaFoto, 'public');
}
// Gabungkan teks wilayah administratif ke dalam kolom alamat
$alamatLengkap = $request->alamat;
if (!str_contains($alamatLengkap, $request->desa)) {
$alamatLengkap = "Desa/Kel. {$request->desa}, Kec. {$request->kecamatan}, {$request->kota}, Provinsi {$request->provinsi}. Kode Pos: {$request->kode_pos}. Detail: " . $request->alamat;
}
$notifMessage = 'Ada pendaftar baru mandiri!';
if ($anggotaLama && $anggotaLama->status === 'ditolak') {
// 🔥 UPDATE DATA JIKA USER DAFTAR ULANG
$anggotaLama->update([
'nama' => $request->nama,
'gender' => $request->gender,
'nik_nip' => $request->nik_nip, // 💡 DISESUAIKAN: Menyimpan ke kolom 'nik_nip'
'alamat' => $alamatLengkap,
'no_hp' => $request->no_hp ?? null,
'status' => 'menunggu',
'foto' => $namaFoto,
]);
$anggota = $anggotaLama;
$notifMessage = 'Seorang anggota mengajukan pendaftaran ulang!';
} else {
// 🔥 BUAT BARU JIKA BELUM PERNAH DAFTAR SAMA SEKALI
if (Anggota::where('email', Auth::user()->email)->exists()) {
return back()->with('error', 'Email ini sudah terdaftar sebagai anggota.');
}
$anggota = Anggota::create([
'nama' => $request->nama,
'gender' => $request->gender,
'nik_nip' => $request->nik_nip, // 💡 DISESUAIKAN: Menyimpan ke kolom 'nik_nip'
'email' => Auth::user()->email,
'alamat' => $alamatLengkap,
'no_hp' => $request->no_hp ?? null,
'user_id' => Auth::id(),
'status' => 'menunggu',
'foto' => $namaFoto,
]);
}
// 🔥 TAMBAHAN MQTT
$this->triggerAdminRefresh($notifMessage);
// Kirim email notifikasi ke admin
try {
Mail::to('admin@bakorwil.com')->send(new AnggotaBaruMenungguPersetujuan($anggota));
} catch (\Exception $e) {
Log::error('Gagal mengirim email ke admin: ' . $e->getMessage());
}
Auth::user()->setRelation('anggota', $anggota);
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 && $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

@ -0,0 +1,231 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\BorrowTransaction;
use App\Models\Koleksi;
use App\Models\Rating;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Mail;
use App\Mail\PengembalianMenungguApproval;
use App\Mail\PerpanjanganMenungguApproval;
use App\Mail\PeringatanKeterlambatanMail;
use Carbon\Carbon;
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)
->latest()
->get()
->map(function ($item) {
// Konversi timezone untuk view
$fields = ['pickup_date', 'tanggal_pinjam', 'tanggal_kembali'];
foreach ($fields as $field) {
if ($item->$field) {
$item->$field = Carbon::parse($item->$field)->setTimezone('Asia/Jakarta');
}
}
return $item;
});
return view('user.peminjaman.index', compact('peminjaman', 'anggota', 'blokir'));
}
/**
* MENAMPILKAN FORM PERPANJANGAN (GET)
*/
public function showPerpanjangForm($id)
{
$peminjaman = BorrowTransaction::with(['koleksi', 'anggota'])->findOrFail($id);
// Proteksi: Cek apakah benar milik user yang login
if ($peminjaman->anggota_id !== Auth::user()->anggota->id) {
abort(403);
}
// 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 || $anggota->status !== 'disetujui') {
return back()->with('error', 'Status keanggotaan Anda belum diizinkan meminjam.');
}
$request->validate([
'koleksi_id' => 'required|exists:koleksis,biblio_id',
'tanggal_pinjam' => 'required|date',
'surat_pengantar' => 'nullable|file|mimes:pdf|max:2048',
]);
$pdfFilename = null;
if ($request->hasFile('surat_pengantar')) {
$file = $request->file('surat_pengantar');
$pdfFilename = time() . '_' . $file->getClientOriginalName();
$file->storeAs('public/surat_pengantar', $pdfFilename);
}
$transaction = BorrowTransaction::create([
'anggota_id' => $anggota->id,
'koleksi_id' => $request->koleksi_id,
'tanggal_pinjam' => $request->tanggal_pinjam,
'status' => 'menunggu',
'surat_pengantar' => $pdfFilename,
]);
return redirect()->route('user.peminjaman.index')->with('success', 'Pengajuan berhasil dikirim!');
}
public function kembalikan($id)
{
$transaction = BorrowTransaction::findOrFail($id);
if ($transaction->status != 'dipinjam') {
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 redirect()->route('user.peminjaman.index')->with('success', 'Permintaan pengembalian dikirim!');
}
// Fungsi Rating & Penilaian
public function penilaian($id)
{
$transaction = BorrowTransaction::findOrFail($id);
return view('user.rating.form', compact('transaction'));
}
public function simpanPenilaian(Request $request, $id)
{
$request->validate(['rating' => 'required|integer|min:1|max:5']);
Rating::create([
'user_id' => Auth::id(),
'borrow_transaction_id' => $id,
'rating' => $request->rating,
'feedback' => $request->feedback,
]);
return redirect()->route('user.peminjaman.index')->with('success', 'Terima kasih!');
}
public function downloadSuratPengantar($id)
{
$transaction = BorrowTransaction::findOrFail($id);
$path = 'public/surat_pengantar/' . $transaction->surat_pengantar;
if (!Storage::exists($path)) return back()->with('error', 'File tidak ditemukan.');
return Storage::download($path);
}
}

View File

@ -0,0 +1,50 @@
<?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() {
// Mengambil data user yang login beserta relasi ke tabel anggota
$user = User::with('anggota')->find(Auth::id());
return view('profile', compact('user'));
}
public function updateProfile(Request $request) {
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:users,email,'.Auth::id(),
]);
$user = Auth::user();
$user->name = $request->name;
$user->email = $request->email;
$user->save();
return redirect()->route('user.profile')->with('success', 'Profil berhasil diperbarui!');
}
public function updatePassword(Request $request) {
$request->validate([
'current_password' => 'required',
'new_password' => 'required|string|min:8|confirmed',
]);
$user = Auth::user();
if (!Hash::check($request->current_password, $user->password)) {
return back()->withErrors(['current_password' => 'Password saat ini salah']);
}
$user->password = Hash::make($request->new_password);
$user->save();
return redirect()->route('user.profile')->with('success', 'Password berhasil diperbarui!');
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\BorrowTransaction;
use App\Models\Rating;
use Illuminate\Support\Facades\Auth;
class UserRatingController extends Controller
{
public function form($id)
{
$transaction = BorrowTransaction::findOrFail($id);
if ($transaction->status !== 'dikembalikan') {
return redirect()->route('user.peminjaman.index')
->with('error', 'Anda hanya bisa memberi penilaian setelah pengembalian disetujui admin.');
}
$existingRating = Rating::where('user_id', Auth::id())
->where('borrow_transaction_id', $transaction->id)
->first();
return view('user.rating.form', compact('transaction', 'existingRating'));
}
public function store(Request $request, $id)
{
$transaction = BorrowTransaction::findOrFail($id);
$existingRating = Rating::where('user_id', Auth::id())
->where('borrow_transaction_id', $transaction->id)
->first();
if ($existingRating) {
return redirect()->route('user.peminjaman.index')
->with('error', 'Anda sudah memberi penilaian untuk transaksi ini.');
}
$request->validate([
'rating' => 'required|integer|min:1|max:5',
'feedback' => 'nullable|string|max:500',
]);
Rating::create([
'user_id' => Auth::id(),
'borrow_transaction_id' => $transaction->id,
'rating' => $request->rating,
'feedback' => $request->feedback,
]);
return redirect()->route('user.peminjaman.index')
->with('success', 'Terima kasih! Penilaian Anda telah kami terima 🙏');
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Koleksi;
use App\Models\BorrowTransaction;
class UserController extends Controller
{
public function index()
{
return view('user.dashboard');
}
public function koleksi()
{
$books = Koleksi::all();
return view('user.koleksi', compact('books'));
}
public function pinjaman()
{
$transactions = BorrowTransaction::where('user_id', auth()->id())->get();
return view('user.pinjaman', compact('transactions'));
}
public function anggota()
{
return view('user.anggota');
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserDashboardController extends Controller
{
public function index()
{
return view('user.dashboard');
}
}

59
app/Http/Kernel.php Normal file
View File

@ -0,0 +1,59 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
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,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's middleware groups.
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SetLocale::class,
],
'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

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AdminMiddleware
{
public function handle(Request $request, Closure $next)
{
if (auth()->check() && auth()->user()->role === 'admin') {
return $next($request);
}
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

@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckRole
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string $role): Response
{
if (!auth()->check()) {
return redirect('/login');
}
if (auth()->user()->role !== $role) {
abort(403, 'Akses Ditolak. Anda tidak memiliki izin yang diperlukan.');
}
return $next($request);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = session('locale', config('app.locale')); // default: 'id'
app()->setLocale($locale);
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

@ -0,0 +1,27 @@
<?php
namespace App\Mail;
use App\Models\Anggota;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class AdminNotifikasiAnggotaBaru extends Mailable
{
use Queueable, SerializesModels;
public $anggota;
public function __construct(Anggota $anggota)
{
$this->anggota = $anggota;
}
public function build()
{
return $this->subject('Pendaftaran Anggota Baru - Menunggu Persetujuan')
->markdown('emails.admin.anggota_baru');
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Mail;
use App\Models\Anggota;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class AnggotaBaruMenungguPersetujuan extends Mailable
{
use Queueable, SerializesModels;
public $anggota;
/**
* Create a new message instance.
*/
public function __construct(Anggota $anggota)
{
$this->anggota = $anggota;
}
/**
* Build the message.
*/
public function build()
{
return $this->subject('🕒 Anggota Baru Menunggu Persetujuan')
->view('emails.anggota_baru_menunggu');
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Models\BorrowTransaction;
class BukuDikembalikanMail extends Mailable
{
use Queueable, SerializesModels;
public $transaction;
public function __construct(BorrowTransaction $transaction)
{
$this->transaction = $transaction->loadMissing(['anggota', 'koleksi']);
}
public function build()
{
return $this->subject('Konfirmasi Pengembalian Buku')
->view('emails.buku_dikembalikan')
->with([
'transaction' => $this->transaction,
]);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class BukuSiapDiambilMail extends Mailable
{
use Queueable, SerializesModels;
public $transaction;
public function __construct($transaction)
{
$this->transaction = $transaction;
}
public function build()
{
return $this->subject('Buku Siap Diambil di Perpustakaan')
->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

@ -0,0 +1,26 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PeminjamanDitolakMail extends Mailable
{
use Queueable, SerializesModels;
public $transaction;
public function __construct($transaction)
{
$this->transaction = $transaction;
}
public function build()
{
return $this->subject('Peminjaman Ditolak')
->view('emails.peminjaman_ditolak');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Models\BorrowTransaction;
class PeminjamanMenungguApproval extends Mailable
{
use Queueable, SerializesModels;
public $borrow;
public function __construct(BorrowTransaction $borrow)
{
$this->borrow = $borrow;
}
public function build()
{
return $this->subject('📚 Pengajuan Peminjaman Baru - Pustaka Nawasena')
->view('emails.peminjaman_menunggu');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Models\BorrowTransaction;
class PengembalianMenungguApproval extends Mailable
{
use Queueable, SerializesModels;
public $transaction;
public function __construct(BorrowTransaction $transaction)
{
$this->transaction = $transaction;
}
public function build()
{
return $this->subject('Permintaan Pengembalian Buku')
->view('emails.pengembalian-menunggu');
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PeringatanKeterlambatanMail extends Mailable
{
use Queueable, SerializesModels;
public $peminjaman;
public function __construct($peminjaman)
{
$this->peminjaman = $peminjaman;
}
public function build()
{
return $this->subject('Peringatan Keterlambatan Peminjaman')
->markdown('emails.peringatan');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Mail;
use App\Models\BorrowTransaction;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PerpanjanganDisetujuiMail extends Mailable
{
use Queueable, SerializesModels;
public $transaction;
public function __construct(BorrowTransaction $transaction)
{
$this->transaction = $transaction;
}
public function build()
{
return $this->subject('Perpanjangan Peminjaman Buku Disetujui')
->view('emails.perpanjangan_disetujui');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Mail;
use App\Models\BorrowTransaction;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PerpanjanganDitolakMail extends Mailable
{
use Queueable, SerializesModels;
public $transaction;
public function __construct(BorrowTransaction $transaction)
{
$this->transaction = $transaction;
}
public function build()
{
return $this->subject('Permintaan Perpanjangan Peminjaman Ditolak')
->view('emails.perpanjangan_ditolak');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Models\BorrowTransaction;
class PerpanjanganMenungguApproval extends Mailable
{
use Queueable, SerializesModels;
public $transaction;
public function __construct(BorrowTransaction $transaction)
{
$this->transaction = $transaction;
}
public function build()
{
return $this->subject('Permintaan Perpanjangan Peminjaman Buku')
->view('emails.perpanjangan_menunggu');
}
}

34
app/Mail/SendOtpMail.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class SendOtpMail extends Mailable
{
use Queueable, SerializesModels;
public $user;
public $otp;
public function __construct($user, $otp)
{
$this->user = $user;
$this->otp = $otp;
}
public function build()
{
return $this->markdown('emails.send-otp')
->subject('Kode Verifikasi Akun Anda - Pustaka Nawasena Bakorwil III Malang')
->with([
'name' => $this->user->name,
'otp' => $this->otp,
]);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Mail;
use App\Models\Anggota;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class StatusAnggotaDiperbarui extends Mailable
{
use Queueable, SerializesModels;
public $anggota;
public function __construct(Anggota $anggota)
{
$this->anggota = $anggota;
}
public function build()
{
return $this->subject('Status Pendaftaran Anggota Anda Diperbarui')
->view('emails.status_anggota_diperbarui');
}
}

60
app/Models/Anggota.php Normal file
View File

@ -0,0 +1,60 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Anggota extends Model
{
use HasFactory;
protected $table = 'anggota';
protected $fillable = [
'nama',
'gender',
'email',
'alamat',
'no_hp',
'user_id',
'status',
'nik_nip',
'foto',
];
public function borrowTransactions()
{
return $this->hasMany(BorrowTransaction::class, 'anggota_id');
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function getFormattedNameAttribute()
{
return ucwords(strtolower($this->nama));
}
public function getFormattedNikNipAttribute()
{
return $this->nik_nip ? strtoupper($this->nik_nip) : null;
}
public function hasActiveBorrow()
{
return $this->borrowTransactions()
->whereIn('status', ['dipinjam', 'menunggu', 'disetujui'])
->exists();
}
}

40
app/Models/Book.php Normal file
View File

@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
use HasFactory;
protected $fillable = [
'judul',
'penulis',
'penerbit',
'tahun_terbit',
'kategori',
'jumlah_eksemplar',
];
public function borrowTransactions()
{
return $this->hasMany(BorrowTransaction::class, 'book_id');
}
public function availableCopies()
{
$borrowed = $this->borrowTransactions()
->where('status', 'dipinjam')
->count();
return $this->jumlah_eksemplar - $borrowed;
}
public function getFormattedTitleAttribute()
{
return ucwords(strtolower($this->judul));
}
}

View File

@ -0,0 +1,240 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
use App\Mail\PeminjamanMenungguApproval;
class BorrowTransaction extends Model
{
use HasFactory;
protected $table = 'borrow_transactions';
protected $fillable = [
'anggota_id',
'koleksi_id',
'tanggal_pinjam',
'pickup_date',
'pickup_time',
'tanggal_kembali',
'status',
'surat_pengantar',
'tanggal_perpanjangan',
'alasan_perpanjangan',
'perpanjangan_diajukan',
'perpanjangan_disetujui',
];
public $timestamps = true;
public function anggota()
{
return $this->belongsTo(\App\Models\Anggota::class, 'anggota_id');
}
public function koleksi()
{
return $this->belongsTo(\App\Models\Koleksi::class, 'koleksi_id', 'biblio_id');
}
public function scopeMenunggu($query)
{
return $query->where('status', 'menunggu');
}
public function scopeDisetujui($query)
{
return $query->where('status', 'disetujui');
}
public function scopeDipinjam($query)
{
return $query->where('status', 'dipinjam');
}
public function scopeDikembalikan($query)
{
return $query->where('status', 'dikembalikan');
}
public function scopeDitolak($query)
{
return $query->where('status', 'ditolak');
}
public function scopePengembalianMenunggu($query)
{
return $query->where('status', 'pengembalian_menunggu');
}
public function scopePerpanjanganMenunggu($query)
{
return $query->where('status', 'perpanjangan_menunggu');
}
public static function sudahDipinjam($koleksiId)
{
return self::where('koleksi_id', $koleksiId)
->whereIn('status', ['dipinjam', 'menunggu', 'disetujui'])
->exists();
}
public function markAsDisetujui($pickupDate = null)
{
$this->status = 'disetujui';
$this->pickup_date = $pickupDate ? Carbon::parse($pickupDate) : Carbon::today();
$this->save();
}
public function markAsDipinjam()
{
$this->status = 'dipinjam';
$this->tanggal_pinjam = Carbon::today();
$this->save();
}
public function markAsDitolak()
{
$this->status = 'ditolak';
$this->save();
}
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\Carbon::today();
}
public function markAsPengembalianMenunggu()
{
$this->status = 'pengembalian_menunggu';
$this->tanggal_kembali = null;
$this->save();
}
public function ajukanPerpanjangan($tanggalBaru, $alasan)
{
if ($this->perpanjangan_diajukan) {
throw new \Exception('Anda sudah pernah mengajukan perpanjangan untuk buku ini.');
}
$this->tanggal_perpanjangan = Carbon::parse($tanggalBaru);
$this->alasan_perpanjangan = $alasan;
$this->status = 'perpanjangan_menunggu';
$this->perpanjangan_diajukan = true;
$this->perpanjangan_disetujui = null;
$this->save();
}
public function setujuiPerpanjangan()
{
$this->tanggal_kembali = $this->tanggal_perpanjangan;
$this->status = 'dipinjam';
$this->perpanjangan_disetujui = true;
$this->save();
}
public function tolakPerpanjangan()
{
$this->status = 'dipinjam';
$this->perpanjangan_disetujui = false;
$this->save();
}
public function getIsBorrowedAttribute()
{
return $this->status === 'dipinjam';
}
public function getIsApprovedAttribute()
{
return $this->status === 'disetujui';
}
public function getIsReturnPendingAttribute()
{
return $this->status === 'pengembalian_menunggu';
}
public function getIsExtensionPendingAttribute()
{
return $this->status === 'perpanjangan_menunggu';
}
public function getIsExtensionApprovedAttribute()
{
return $this->perpanjangan_disetujui === true;
}
public function getIsExtensionRejectedAttribute()
{
return $this->perpanjangan_disetujui === false;
}
public function canBeExtended()
{
return $this->status === 'dipinjam' && !$this->perpanjangan_diajukan;
}
public function canBeReturnedByUser()
{
return $this->status === 'dipinjam';
}
public function canBeRated()
{
return $this->status === 'dikembalikan';
}
public function isLate()
{
if (!$this->tanggal_kembali) return false;
return Carbon::now()->greaterThan(Carbon::parse($this->tanggal_kembali));
}
public function getLateDays()
{
if (!$this->isLate()) return 0;
return Carbon::parse($this->tanggal_kembali)->diffInDays(Carbon::now());
}
public static function boot()
{
parent::boot();
static::created(function ($transaction) {
if (env('MAIL_USERNAME')) {
try {
Mail::to(config('mail.from.address'))
->send(new PeminjamanMenungguApproval($transaction));
} catch (\Exception $e) {
\Log::error('Gagal mengirim email peminjaman ke admin: ' . $e->getMessage());
}
}
});
}
}

18
app/Models/Fasilitas.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Fasilitas extends Model
{
use HasFactory;
protected $fillable = [
'nama',
'jenis',
'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'
];
}

11
app/Models/Item.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Item extends Model
{
//
}

120
app/Models/Koleksi.php Normal file
View File

@ -0,0 +1,120 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class Koleksi extends Model
{
use HasFactory;
protected $table = 'koleksis';
protected $primaryKey = 'biblio_id';
public $incrementing = true;
protected $keyType = 'int';
public $timestamps = true;
protected $fillable = [
'title',
'sor',
'edition',
'isbn_issn',
'publisher_id',
'publish_year',
'tahun_terbit',
'collation',
'series_title',
'call_number',
'language_id',
'source',
'publish_place_id',
'classification',
'notes',
'image',
'file_att',
'opac_hide',
'promoted',
'labels',
'frequency_id',
'spec_detail_info',
'content_type_id',
'media_type_id',
'carrier_type_id',
'input_date',
'last_update',
'uid',
'kode_eksemplar',
'qr_code',
'barcode',
'created_at',
'updated_at',
];
public function getRouteKeyName()
{
return 'kode_eksemplar';
}
public function borrowTransactions()
{
return $this->hasMany(BorrowTransaction::class, 'koleksi_id', 'biblio_id');
}
public function scopeBorrowed($query)
{
return $query->whereHas('borrowTransactions', function ($q) {
$q->whereIn('status', ['dipinjam', 'disetujui', 'pengembalian_menunggu']);
});
}
public function scopeAvailable($query)
{
return $query->whereDoesntHave('borrowTransactions', function ($q) {
$q->whereIn('status', ['dipinjam', 'disetujui', 'pengembalian_menunggu']);
});
}
public function isBorrowed()
{
return $this->borrowTransactions()
->whereIn('status', ['dipinjam', 'disetujui', 'pengembalian_menunggu'])
->exists();
}
public function getStatusAttribute()
{
return $this->isBorrowed() ? 'Dipinjam' : 'Tersedia';
}
public function scopeWithStatus($query)
{
return $query->with(['borrowTransactions' => function ($q) {
$q->latest();
}]);
}
public function getBarcodeUrlAttribute()
{
if ($this->barcode && Storage::disk('public')->exists($this->barcode)) {
return asset('storage/' . $this->barcode);
}
return null;
}
public function hasBarcode()
{
return $this->barcode && Storage::disk('public')->exists($this->barcode);
}
}

17
app/Models/Member.php Normal file
View File

@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Member extends Model
{
use HasFactory;
// TAMBAHKAN BARIS INI AGAR LARAVEL MEMBACA TABEL ANGGOTA
protected $table = 'anggota';
// Kode isi model kamu yang lain tetap biarkan di bawah ini...
}

32
app/Models/Peminjaman.php Normal file
View File

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Peminjaman extends Model
{
use HasFactory;
protected $table = 'borrow_transactions';
protected $fillable = [
'member_id',
'koleksi_id',
'tgl_peminjaman',
'tgl_pengembalian',
'status',
];
public function member()
{
return $this->belongsTo(Member::class);
}
public function koleksi()
{
return $this->belongsTo(Koleksi::class);
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PublishPlace extends Model
{
//
}

12
app/Models/Publisher.php Normal file
View File

@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Publisher extends Model
{
protected $guarded = ['id'];
}

29
app/Models/Rating.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Rating extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'borrow_transaction_id',
'rating',
'feedback'
];
public function transaction()
{
return $this->belongsTo(BorrowTransaction::class, 'borrow_transaction_id');
}
public function user()
{
return $this->belongsTo(User::class);
}
}

15
app/Models/Setting.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
protected $fillable = [
'nama_lembaga',
'alamat',
'logo',
];
}

97
app/Models/User.php Normal file
View File

@ -0,0 +1,97 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
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',
'password',
'role',
'email_otp',
'email_otp_expires_at',
'otp_expires_at',
'otp_last_sent_at',
'verification_token',
'email_verified_at',
];
protected $hidden = [
'password',
'remember_token',
'email_otp',
];
protected $casts = [
'email_verified_at' => 'datetime',
'email_otp_expires_at' => 'datetime',
'otp_expires_at' => 'datetime',
'otp_last_sent_at' => 'datetime',
'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());
}
}

79
app/Models/UserLogin.php Normal file
View File

@ -0,0 +1,79 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\User;
class UserLogin extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'ip_address',
'user_agent',
'location',
'status'
];
public function user()
{
return $this->belongsTo(User::class);
}
public function setStatusAttribute($value)
{
$this->attributes['status'] = strtolower($value);
}
public function getCreatedAtFormattedAttribute()
{
return $this->created_at->format('d M Y H:i:s');
}
/**
* @param int $userId
* @param string $status
* @param string|null $location
* @param string|null $ipAddress
* @param string|null $userAgent
*/
public static function record(
int $userId,
string $status = 'success',
string $location = null,
string $ipAddress = null,
string $userAgent = null
) {
self::create([
'user_id' => $userId,
'status' => $status,
'location' => $location,
'ip_address' => $ipAddress ?? request()->ip(),
'user_agent' => $userAgent ?? request()->userAgent(),
]);
}
public function scopeLatestLogins($query, $limit = 10)
{
return $query->latest()->limit($limit);
}
public static function detectLocation()
{
if (class_exists(\Stevebauman\Location\Facades\Location::class)) {
$location = \Location::get(request()->ip());
return $location ? $location->cityName . ', ' . $location->countryName : null;
}
return null;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class PerpanjanganPeminjamanNotification extends Notification
{
use Queueable;
public function __construct()
{
//
}
/**
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->line('The introduction to the notification.')
->action('Notification Action', url('/'))
->line('Thank you for using our application!');
}
/**
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
//
];
}
}

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

@ -0,0 +1,53 @@
<?php
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

@ -0,0 +1,59 @@
<?php
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\AuthenticateSession;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->id('admin')
->path('admin')
->login()
->authGuard('web')
->colors([
'primary' => Color::Amber,
])
->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources')
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
->pages([
Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\Filament\\Admin\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
]);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
public const HOME = '/home';
public function boot(): void
{
$this->routes(function () {
Route::middleware('web')
->group(base_path('routes/web.php'));
Route::middleware(['web', 'auth', 'is_admin'])
->group(base_path('routes/admin.php'));
Route::middleware(['web', 'auth', 'is_user'])
->group(base_path('routes/user.php'));
Route::prefix('api')
->middleware('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/guest.php'));
});
}
}

29
bootstrap/app.php Normal file
View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
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) {
// 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) {
//
})->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',
),
),
);

205
bootstrap/cache/packages.php vendored Normal file
View File

@ -0,0 +1,205 @@
<?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/reverb' =>
array (
'providers' =>
array (
0 => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
1 => 'Laravel\\Reverb\\ReverbServiceProvider',
),
),
'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',
),
),
);

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

306
bootstrap/cache/services.php vendored Normal file
View File

@ -0,0 +1,306 @@
<?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\\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 (
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 => '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 (
'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\\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',
'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\\Reverb\\ApplicationManagerServiceProvider' =>
array (
),
'Laravel\\Sail\\SailServiceProvider' =>
array (
),
'Laravel\\Socialite\\SocialiteServiceProvider' =>
array (
),
'Laravel\\Tinker\\TinkerServiceProvider' =>
array (
),
),
);

7
bootstrap/providers.php Normal file
View File

@ -0,0 +1,7 @@
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
];

104
composer.json Normal file
View File

@ -0,0 +1,104 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"filament/filament": "3.3",
"intervention/image": "^3.11",
"laravel/framework": "^12.0",
"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": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^11.5.3"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"test": [
"@php artisan config:clear --ansi",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi",
"@php artisan filament:upgrade"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true,
"repositories": {
"packagist": {
"type": "composer",
"url": "https://packagist.org"
}
}
}

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