fix controller,model,migrate,views pengumuman and fix tampilan ruangan,kelas,jurusan

fix tampilan pengumuman
This commit is contained in:
rendygaafk 2025-05-05 18:41:38 +07:00
parent 43a61488c0
commit 734bc685f3
14 changed files with 923 additions and 1268 deletions

View File

@ -4,353 +4,158 @@
use App\Models\Announcement;
use App\Models\Ruangan;
use App\Services\MqttService;
use App\Http\Requests\StoreAnnouncementRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Storage;
use Carbon\Carbon;
use PhpMqtt\Client\Facades\MQTT;
class AnnouncementController extends Controller
{
// Mode constants
const MODE_REGULER = 'reguler';
const MODE_TTS = 'tts';
// Relay constants
const RELAY_ON = 'ON';
const RELAY_OFF = 'OFF';
// TTS API constants
const TTS_API_URL = 'http://api.voicerss.org/';
const TTS_API_KEY = '90927de8275148d79080facd20fb486c';
const TTS_DEFAULT_VOICE = 'id-id';
const TTS_DEFAULT_SPEED = 0;
const TTS_DEFAULT_FORMAT = 'wav';
protected $mqttService;
protected $mqttConfig;
public function __construct(MqttService $mqttService)
{
$this->mqttService = $mqttService;
$this->mqttConfig = config('mqtt');
$this->initializeMqttSubscriptions();
}
protected function initializeMqttSubscriptions()
{
try {
$this->mqttService->subscribe(
$this->mqttConfig['topics']['responses']['announcement_ack'],
function (string $topic, string $message) {
$this->handleAnnouncementAck($message);
}
);
$this->mqttService->subscribe(
$this->mqttConfig['topics']['responses']['announcement_error'],
function (string $topic, string $message) {
$this->handleAnnouncementError($message);
}
);
$this->mqttService->subscribe(
$this->mqttConfig['topics']['responses']['relay_status'],
function (string $topic, string $message) {
$this->handleRelayStatusUpdate($message);
}
);
} catch (\Exception $e) {
Log::error('MQTT Subscription Error: ' . $e->getMessage());
}
}
public function index()
{
$ruangan = Ruangan::with(['kelas', 'jurusan'])->get();
$announcements = Announcement::with(['ruangans'])
->latest()
->paginate(10);
try {
$mqttStatus = $this->mqttService->isConnected() ? 'Connected' : 'Disconnected';
} catch (\Exception $e) {
$mqttStatus = 'Disconnected';
Log::error('MQTT check failed: ' . $e->getMessage());
}
return view('admin.announcement.index', [
'ruangans' => $ruangan,
'announcements' => $announcements,
'modes' => [self::MODE_REGULER, self::MODE_TTS],
'relayStates' => [self::RELAY_ON, self::RELAY_OFF],
'mqttStatus' => $mqttStatus
]);
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'mode' => 'required|in:reguler,tts',
'ruangans' => 'required|array',
'ruangans.*' => 'exists:ruangan,id',
'relay_action' => 'required_if:mode,reguler|in:ON,OFF',
'tts_text' => 'required_if:mode,tts|string|max:1000',
'tts_voice' => 'required_if:mode,tts',
'tts_speed' => 'required_if:mode,tts|integer|min:-10|max:10',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput();
}
try {
$announcement = new Announcement();
$announcement->mode = $request->mode;
if ($request->mode === self::MODE_REGULER) {
$announcement->message = $request->relay_action === self::RELAY_ON
? 'Aktivasi Relay Ruangan'
: 'Deaktivasi Relay Ruangan';
$announcement->is_active = $request->relay_action === self::RELAY_ON;
$announcement->relay_state = $request->relay_action;
} else {
$audioContent = $this->generateTTS(
$request->tts_text,
$request->tts_voice,
$request->tts_speed
);
if (!$audioContent) {
throw new \Exception('Failed to generate TTS audio');
}
$fileName = 'tts/' . now()->format('YmdHis') . '.wav';
Storage::disk('public')->put($fileName, $audioContent);
$announcement->message = $request->tts_text;
$announcement->audio_path = $fileName;
$announcement->voice = $request->tts_voice;
$announcement->speed = $request->tts_speed;
$announcement->relay_state = self::RELAY_OFF; // Default untuk TTS
}
$announcement->sent_at = now();
$announcement->status = 'pending';
if (!$announcement->save()) {
throw new \Exception('Failed to save announcement');
}
$existingRuangan = Ruangan::whereIn('id', $request->ruangans)->pluck('id');
if ($existingRuangan->count() != count($request->ruangans)) {
throw new \Exception('Some selected ruangan not found');
}
$announcement->ruangans()->sync($existingRuangan);
$this->publishAnnouncement($announcement);
return redirect()->route('announcement.index')
->with('success', 'Pengumuman berhasil dikirim');
} catch (\Exception $e) {
Log::error('Announcement Error: ' . $e->getMessage());
if (isset($announcement) && $announcement->exists) {
$announcement->delete();
}
return redirect()->back()
->with('error', 'Gagal: ' . $e->getMessage())
->withInput();
}
}
protected function publishAnnouncement(Announcement $announcement)
{
$payload = [
'mode' => $announcement->mode,
'announcement_id' => $announcement->id,
'ruangans' => $announcement->ruangans->pluck('nama_ruangan')->toArray(),
'timestamp' => now()->toDateTimeString()
];
if ($announcement->mode === self::MODE_REGULER) {
$payload['relay_state'] = $announcement->relay_state;
// Kirim perintah relay ke masing-masing ruangan
foreach ($announcement->ruangans as $ruangan) {
$topic = $ruangan->mqtt_topic ?? "ruangan/{$ruangan->id}/relay/control";
$this->mqttService->publish(
$topic,
json_encode([
'state' => $announcement->relay_state,
'announcement_id' => $announcement->id
]),
1 // QoS level
);
// Update status relay di database
$ruangan->update(['relay_state' => $announcement->relay_state]);
}
} else {
$payload['message'] = $announcement->message;
$payload['audio_url'] = asset('storage/' . $announcement->audio_path);
$payload['voice'] = $announcement->voice;
$payload['speed'] = $announcement->speed;
}
// Publis ke topic announcement umum
$this->mqttService->publish(
$this->mqttConfig['topics']['commands']['announcement'],
json_encode($payload),
1
);
}
protected function generateTTS($text, $voice, $speed)
{
$response = Http::get(self::TTS_API_URL, [
'key' => self::TTS_API_KEY,
'hl' => $voice,
'src' => $text,
'r' => $speed,
'c' => self::TTS_DEFAULT_FORMAT,
'f' => '8khz_8bit_mono'
]);
if ($response->successful()) {
return $response->body();
}
Log::error('TTS API Error: ' . $response->body());
return null;
}
protected function handleAnnouncementAck(string $message)
{
try {
$data = json_decode($message, true);
if (isset($data['announcement_id'])) {
Announcement::where('id', $data['announcement_id'])
->update(['status' => 'delivered']);
Log::info('Announcement delivered', $data);
}
} catch (\Exception $e) {
Log::error('ACK Handler Error: ' . $e->getMessage());
}
$ruangans = Ruangan::orderBy('nama_ruangan')->get();
$mqttStatus = $this->checkMqttConnection();
return view('admin.announcement.index', compact('ruangans', 'mqttStatus'));
}
protected function handleAnnouncementError(string $message)
public function history()
{
$announcements = Announcement::with('ruangans')
->when(request('mode'), function($query, $mode) {
return $query->where('mode', $mode);
})
->when(request('date'), function($query, $date) {
return $query->whereDate('sent_at', $date);
})
->orderBy('sent_at', 'desc')
->paginate(10);
return view('admin.announcement.history', compact('announcements'));
}
private function checkMqttConnection()
{
try {
$data = json_decode($message, true);
if (isset($data['announcement_id'])) {
Announcement::where('id', $data['announcement_id'])
->update([
'status' => 'failed',
'error_message' => $data['error'] ?? 'Unknown error'
]);
Log::error('Announcement failed', $data);
}
$mqtt = MQTT::connection();
return $mqtt->isConnected();
} catch (\Exception $e) {
Log::error('Error Handler Error: ' . $e->getMessage());
return false;
}
}
protected function handleRelayStatusUpdate(string $message)
public function store(StoreAnnouncementRequest $request)
{
$announcementData = [
'mode' => $request->mode,
'sent_at' => now(),
];
// Hanya tambahkan message jika mode TTS
if ($request->mode === 'tts') {
$announcementData['message'] = $request->message;
}
$announcement = Announcement::create($announcementData);
$announcement->ruangans()->sync($request->ruangans);
try {
$data = json_decode($message, true);
if (isset($data['ruangan_id'], $data['state'])) {
Ruangan::where('id', $data['ruangan_id'])
->update(['relay_state' => $data['state']]);
Log::info('Relay status updated', $data);
if ($request->mode === 'tts') {
MQTT::publish('control/relay', json_encode([
'mode' => 'tts',
'ruang' => $request->ruangans
]));
MQTT::publish('tts/play', json_encode([
'ruang' => $request->ruangans,
'teks' => $request->message
]));
} else {
MQTT::publish('control/relay', json_encode([
'mode' => 'reguler',
'ruang' => $request->ruangans
]));
}
} catch (\Exception $e) {
Log::error('Relay Status Handler Error: ' . $e->getMessage());
return response()->json([
'message' => 'Gagal mengirim ke perangkat: ' . $e->getMessage()
], 500);
}
return response()->json(['success' => true]);
}
public function ttsPreview(Request $request)
public function details($id)
{
$validator = Validator::make($request->all(), [
'text' => 'required|string|max:1000',
'voice' => 'required|string',
'speed' => 'required|integer|min:-10|max:10'
$announcement = Announcement::with('ruangans')->findOrFail($id);
return response()->json([
'mode' => $announcement->mode,
'formatted_sent_at' => $announcement->formatted_sent_at,
'message' => $announcement->message,
'ruangans' => $announcement->ruangans->map(function($ruangan) {
return ['nama_ruangan' => $ruangan->nama_ruangan];
})
]);
}
public function destroy($id)
{
$announcement = Announcement::findOrFail($id);
$announcement->delete();
return response()->json(['success' => true]);
}
public function checkMqtt()
{
return response()->json([
'connected' => $this->checkMqttConnection()
]);
}
public function controlRelay(Request $request)
{
$request->validate([
'ruangans' => 'required|array|min:1',
'ruangans.*' => 'exists:ruangan,id',
'action' => 'required|in:activate,deactivate',
'mode' => 'required|in:manual,tts'
]);
if ($validator->fails()) {
return response()->json([
'error' => $validator->errors()->first()
], 400);
}
$ruangans = Ruangan::whereIn('id', $request->ruangans)->get();
$state = $request->action === 'activate' ? 'on' : 'off';
try {
$audioContent = $this->generateTTS(
$request->text,
$request->voice,
$request->speed
);
// Kirim perintah ke ESP32 via MQTT
MQTT::publish('control/relay', json_encode([
'action' => $request->action,
'ruang' => $request->ruangans,
'mode' => $request->mode
]));
if (!$audioContent) {
throw new \Exception('Failed to generate TTS audio');
// Update status relay di database
Ruangan::whereIn('id', $request->ruangans)->update(['relay_state' => $state]);
// Jika mengaktifkan relay, simpan sebagai pengumuman manual
if ($request->action === 'activate' && $request->mode === 'manual') {
$announcement = Announcement::create([
'mode' => 'manual',
'message' => 'Pengumuman via microphone manual',
'sent_at' => now()
]);
$announcement->ruangans()->sync($request->ruangans);
}
$fileName = 'tts/previews/' . uniqid() . '.wav';
Storage::disk('public')->put($fileName, $audioContent);
return response()->json([
'audio_url' => asset('storage/' . $fileName)
]);
return response()->json(['success' => true]);
} catch (\Exception $e) {
Log::error('TTS Preview Error: ' . $e->getMessage());
return response()->json([
'error' => 'Failed to generate preview'
'message' => 'Gagal mengontrol relay: ' . $e->getMessage()
], 500);
}
}
public function history(Request $request)
public function relayStatus()
{
$search = $request->input('search');
$mode = $request->input('mode');
$relayState = $request->input('relay_state');
$announcements = Announcement::with(['ruangans'])
->when($search, function($query) use ($search) {
return $query->where('message', 'like', "%{$search}%")
->orWhereHas('ruangans', function($q) use ($search) {
$q->where('nama_ruangan', 'like', "%{$search}%");
});
})
->when($mode, function($query) use ($mode) {
return $query->where('mode', $mode);
})
->when($relayState, function($query) use ($relayState) {
return $query->where('relay_state', $relayState);
})
->latest()
->paginate(10);
return view('admin.announcement.history', [
'announcements' => $announcements,
'search' => $search,
'mode' => $mode,
'relay_state' => $relayState,
'modes' => [self::MODE_REGULER, self::MODE_TTS],
'relayStates' => [self::RELAY_ON, self::RELAY_OFF]
]);
$ruangans = Ruangan::select('id', 'relay_state')->get();
return response()->json($ruangans);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreAnnouncementRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'message' => 'required|string|max:500',
'mode' => 'required|in:tts,manual',
'ruangans' => 'required|array|min:1',
'ruangans.*' => 'exists:ruangan,id',
];
}
public function messages()
{
return [
'ruangans.required' => 'Pilih minimal satu ruangan',
'ruangans.min' => 'Pilih minimal satu ruangan',
];
}
}

View File

@ -4,121 +4,37 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Announcement extends Model
{
use HasFactory;
protected $table = 'announcements'; // Pastikan konsisten
protected $fillable = [
'mode',
'message',
'audio_path',
'voice',
'speed',
'is_active',
'status',
'error_message',
'sent_at',
'relay_state' // Tambahkan ini
];
protected $attributes = [
'is_active' => true,
'status' => 'pending',
'relay_state' => 'OFF' // Default value
];
protected $fillable = ['message', 'mode', 'sent_at'];
protected $casts = [
'is_active' => 'boolean',
'sent_at' => 'datetime'
'sent_at' => 'datetime',
];
// Tambahkan aksesor untuk relay
public function getRelayStateDescriptionAttribute()
{
return $this->relay_state === 'ON' ? 'Relay Menyala' : 'Relay Mati';
}
/**
* Relationship with Ruangan (many-to-many)
*/
public function ruangans()
{
return $this->belongsToMany(Ruangan::class, 'announcement_ruangan')
->withTimestamps();
return $this->belongsToMany(Ruangan::class);
}
/**
* Scope for regular announcements
*/
public function scopeReguler($query)
{
return $query->where('mode', 'reguler');
}
/**
* Scope for TTS announcements
*/
public function scopeTts($query)
{
return $query->where('mode', 'tts');
}
/**
* Scope for delivered announcements
*/
public function scopeDelivered($query)
{
return $query->where('status', 'delivered');
}
/**
* Scope for failed announcements
*/
public function scopeFailed($query)
{
return $query->where('status', 'failed');
}
/**
* Accessor for audio URL
*/
public function getAudioUrlAttribute()
{
return $this->audio_path ? asset('storage/' . $this->audio_path) : null;
}
/**
* Accessor for formatted sent time
*/
public function getFormattedSentAtAttribute()
{
return $this->sent_at->format('d M Y H:i:s');
return $this->sent_at
? $this->sent_at->format('d/m/Y H:i:s')
: 'Belum dikirim';
}
/**
* Accessor untuk pesan aktivasi
*/
public function getActivationMessageAttribute()
// Accessor for short message
public function getShortMessageAttribute()
{
return $this->is_active ? 'Aktivasi Ruangan' : 'Deaktivasi Ruangan';
}
/**
* Cek apakah pengumuman reguler
*/
public function isReguler()
{
return $this->mode === 'reguler';
}
/**
* Cek apakah pengumuman TTS
*/
public function isTts()
{
return $this->mode === 'tts';
if ($this->mode === 'manual') {
return 'Relay Control';
}
return Str::limit($this->message, 30);
}
}

View File

@ -15,15 +15,13 @@ class Ruangan extends Model
'nama_ruangan',
'id_kelas',
'id_jurusan',
'relay_state', // Ubah dari status_relay menjadi relay_state
'mqtt_topic' // Tambahkan kolom untuk custom MQTT topic
'relay_state'
];
protected $casts = [
'relay_state' => 'string' // Ubah menjadi string untuk menyimpan 'ON'/'OFF'
'relay_state' => 'string'
];
/**
* Relationship with Kelas
*/
@ -56,19 +54,4 @@ public function getNamaRuanganAttribute($value)
return strtoupper($value);
}
/**
* Scope for active relay status
*/
public function scopeRelayActive($query)
{
return $query->where('status_relay', true);
}
/**
* Scope for inactive relay status
*/
public function scopeRelayInactive($query)
{
return $query->where('status_relay', false);
}
}

View File

@ -13,8 +13,7 @@ public function up(): void
$table->string('nama_ruangan');
$table->foreignId('id_kelas')->constrained('kelas')->onDelete('cascade');
$table->foreignId('id_jurusan')->constrained('jurusan')->onDelete('cascade');
$table->string('relay_state')->default('OFF'); // Ubah tipe data
$table->string('mqtt_topic')->nullable(); // Untuk custom topic per ruangan
$table->enum('relay_state', ['on', 'off'])->default('off');
$table->timestamps();
$table->index('nama_ruangan');

View File

@ -10,32 +10,16 @@ public function up()
{
Schema::create('announcements', function (Blueprint $table) {
$table->id();
$table->string('mode');
$table->text('message');
$table->string('audio_path')->nullable();
$table->string('voice')->nullable();
$table->integer('speed')->nullable();
$table->boolean('is_active')->default(true);
$table->string('status')->default('pending');
$table->text('error_message')->nullable();
$table->string('relay_state')->default('OFF'); // Tambahkan kolom ini
$table->timestamp('sent_at')->useCurrent();
$table->text('message')->nullable();
$table->string('mode')->default('tts');
$table->timestamp('sent_at')->nullable();
$table->timestamps();
$table->index('mode');
$table->index('sent_at');
$table->index('relay_state'); // Tambahkan index
});
// Perbaikan utama: Explicitly specify table names
Schema::create('announcement_ruangan', function (Blueprint $table) {
$table->id();
$table->foreignId('announcement_id')->constrained('announcements')->onDelete('cascade');
$table->foreignId('ruangan_id')->constrained('ruangan')->onDelete('cascade');
$table->string('relay_state_at_time')->nullable(); // State saat pengumuman dikirim
$table->timestamps();
$table->unique(['announcement_id', 'ruangan_id']);
$table->primary(['announcement_id', 'ruangan_id']);
});
}

View File

@ -63,4 +63,16 @@ .badge-green {
.badge-gray {
@apply px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded-full;
}
.tab-content {
transition: opacity 0.3s ease;
}
.shadow-sm {
transition: box-shadow 0.2s ease;
}
.hover\:shadow-lg:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}

View File

@ -1,269 +1,301 @@
@extends('layouts.dashboard')
@section('title', 'Riwayat Pengumuman')
@section('title', 'Riwayat Pengumuman - Smart School')
@section('content')
<div class="container mx-auto px-4 py-8">
<!-- Header Section -->
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8">
<div>
<h1 class="text-3xl font-bold text-gray-800">Riwayat Pengumuman</h1>
<p class="text-gray-600 mt-2">Daftar seluruh pengumuman yang pernah dikirim</p>
</div>
<div class="mt-4 md:mt-0 flex space-x-3">
<a href="{{ route('announcement.index') }}"
class="flex items-center px-5 py-2.5 bg-indigo-50 text-indigo-700 rounded-lg hover:bg-indigo-100 transition duration-300 shadow-sm">
<i class="fas fa-arrow-left mr-2"></i> Kembali
</a>
</div>
<h1 class="text-2xl font-bold text-gray-800">Riwayat Pengumuman</h1>
<a href="{{ route('admin.announcement.index') }}"
class="flex items-center mt-4 md:mt-0 px-4 py-2 bg-white border border-blue-500 text-blue-600 rounded-lg hover:bg-blue-50 transition-colors">
<i class="fas fa-plus-circle mr-2"></i> Buat Pengumuman Baru
</a>
</div>
<!-- Filter Section -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-6 border border-gray-100">
<div class="p-6">
<form action="{{ route('admin.announcement.history') }}" method="GET">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Search Input -->
<div>
<label for="search" class="block text-gray-700 font-medium mb-2">Cari</label>
<div class="relative">
<input type="text" name="search" id="search" value="{{ request('search') }}"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-200 pr-10"
placeholder="Cari pengumuman atau ruangan...">
<button type="submit" class="absolute right-3 top-3 text-gray-400 hover:text-gray-600">
<i class="fas fa-search"></i>
</button>
<!-- Enhanced Filter Card -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 mb-6 overflow-hidden">
<div class="p-4 border-b border-gray-200 bg-gray-50">
<h2 class="text-lg font-medium text-gray-700 flex items-center">
<i class="fas fa-filter mr-2 text-blue-500"></i> Filter Riwayat
</h2>
</div>
<div class="p-4">
<form id="filterForm" method="GET" action="{{ route('admin.announcement.history') }}" class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label for="mode" class="block text-sm font-medium text-gray-700 mb-1">Mode Pengumuman</label>
<div class="relative">
<select id="mode" name="mode" class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
<option value="">Semua Mode</option>
<option value="tts" {{ request('mode') == 'tts' ? 'selected' : '' }}>Text-to-Speech</option>
<option value="manual" {{ request('mode') == 'manual' ? 'selected' : '' }}>Manual</option>
</select>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<i class="fas fa-caret-down text-gray-400"></i>
</div>
</div>
<!-- Mode Filter -->
<div>
<label for="mode" class="block text-gray-700 font-medium mb-2">Jenis Pengumuman</label>
<div class="relative">
<select name="mode" id="mode"
class="appearance-none w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-200 pr-10 bg-white">
<option value="">Semua Jenis</option>
<option value="reguler" {{ request('mode') == 'reguler' ? 'selected' : '' }}>Aktivasi Ruangan</option>
<option value="tts" {{ request('mode') == 'tts' ? 'selected' : '' }}>Pengumuman Suara (TTS)</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-700">
<i class="fas fa-chevron-down"></i>
</div>
</div>
<div>
<label for="start_date" class="block text-sm font-medium text-gray-700 mb-1">Dari Tanggal</label>
<div class="relative">
<input type="date" id="start_date" name="start_date" value="{{ request('start_date') }}"
class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<i class="fas fa-calendar-alt text-gray-400"></i>
</div>
</div>
<!-- Reset Button -->
<div class="flex items-end">
<a href="{{ route('admin.announcement.history') }}"
class="px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition duration-300 flex items-center">
<i class="fas fa-sync-alt mr-2"></i> Reset Filter
</a>
</div>
<div>
<label for="end_date" class="block text-sm font-medium text-gray-700 mb-1">Sampai Tanggal</label>
<div class="relative">
<input type="date" id="end_date" name="end_date" value="{{ request('end_date') }}"
class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<i class="fas fa-calendar-alt text-gray-400"></i>
</div>
</div>
</div>
<div class="flex items-end space-x-3">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 h-[42px] transition-colors">
<i class="fas fa-search mr-2"></i> Terapkan Filter
</button>
@if(request()->has('mode') || request()->has('start_date') || request()->has('end_date'))
<a href="{{ route('admin.announcement.history') }}" class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 h-[42px] transition-colors">
<i class="fas fa-sync-alt mr-2"></i> Reset
</a>
@endif
</div>
</form>
</div>
</div>
<!-- Announcements Table -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-gray-100">
<!-- Table Header -->
<div class="bg-gradient-to-r from-gray-50 to-gray-100 px-6 py-4 border-b border-gray-200">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center">
<h2 class="text-lg font-semibold text-gray-800">Daftar Pengumuman</h2>
<div class="mt-2 md:mt-0 text-sm">
Menampilkan {{ $announcements->count() }} dari {{ $announcements->total() }} pengumuman
</div>
</div>
</div>
<!-- Table Body -->
<!-- History Table -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Jenis
#
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Konten
Waktu Pengiriman
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Mode
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Isi Pengumuman
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ruangan Tujuan
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Waktu
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Aksi
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($announcements as $announcement)
<tr class="hover:bg-gray-50 transition duration-150">
<!-- Jenis -->
@forelse($announcements as $index => $announcement)
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $index + 1 + (($announcements->currentPage() - 1) * $announcements->perPage()) }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
@if($announcement->mode === 'reguler')
<div class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center mr-3">
<i class="fas fa-door-open text-blue-600 text-sm"></i>
</div>
<span class="text-sm font-medium text-gray-900">Aktivasi</span>
@else
<div class="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center mr-3">
<i class="fas fa-volume-up text-purple-600 text-sm"></i>
</div>
<span class="text-sm font-medium text-gray-900">TTS</span>
@endif
</div>
</td>
<!-- Konten -->
<td class="px-6 py-4">
<div class="text-sm text-gray-900 max-w-xs truncate">
@if($announcement->mode === 'tts')
{{ $announcement->message }}
@else
{{ $announcement->is_active ? 'Aktivasi ruangan' : 'Deaktivasi ruangan' }}
<div class="text-xs text-gray-500 mt-1">
Status: {{ $announcement->is_active ? 'AKTIF' : 'NONAKTIF' }}
</div>
@endif
</div>
@if($announcement->mode === 'tts')
<div class="mt-1">
<audio controls class="h-8">
<source src="{{ asset('storage/' . $announcement->audio_path) }}" type="audio/wav">
</audio>
</div>
@endif
</td>
<!-- Ruangan -->
<td class="px-6 py-4">
<div class="text-sm text-gray-500">
{{ $announcement->ruangans->count() }} ruangan
</div>
<div class="text-xs text-gray-400 mt-1">
@foreach($announcement->ruangans->take(3) as $ruangan)
{{ $ruangan->nama_ruangan }}@if(!$loop->last), @endif
@endforeach
@if($announcement->ruangans->count() > 3)
+{{ $announcement->ruangans->count() - 3 }} lainnya
@endif
</div>
</td>
<!-- Waktu -->
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">
{{ $announcement->sent_at->format('d M Y') }}
<div class="text-sm font-semibold text-gray-900">
{{ $announcement->formatted_sent_at }}
</div>
<div class="text-xs text-gray-500">
{{ $announcement->sent_at->format('H:i') }}
{{ $announcement->sent_at->diffForHumans() }}
</div>
</td>
<!-- Status -->
<td class="px-6 py-4 whitespace-nowrap">
@if($announcement->status === 'delivered')
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
<i class="fas fa-check-circle mr-1"></i> Terkirim
</span>
@elseif($announcement->status === 'failed')
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
<i class="fas fa-times-circle mr-1"></i> Gagal
@if($announcement->mode === 'tts')
<span class="px-2.5 py-0.5 inline-flex text-xs leading-4 font-medium rounded-full bg-green-100 text-green-800">
<i class="fas fa-robot mr-1"></i> TTS
</span>
@else
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
<i class="fas fa-clock mr-1"></i> Proses
<span class="px-2.5 py-0.5 inline-flex text-xs leading-4 font-medium rounded-full bg-blue-100 text-blue-800">
<i class="fas fa-microphone-alt mr-1"></i> Manual
</span>
@endif
</td>
<!-- Aksi -->
<td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900">{{ $announcement->short_message }}</div>
<div class="text-xs text-gray-500 mt-1">{{ Str::limit($announcement->message, 80) }}</div>
</td>
<td class="px-6 py-4">
<div class="flex flex-wrap gap-1">
@foreach($announcement->ruangans as $ruangan)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
<span class="w-1.5 h-1.5 rounded-full mr-1 {{ $announcement->mode === 'tts' ? 'bg-green-500' : 'bg-blue-500' }}"></span>
{{ $ruangan->nama_ruangan }}
</span>
@endforeach
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="dropdown relative inline-block">
<button class="dropdown-toggle p-1 rounded-full hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition duration-200">
<i class="fas fa-ellipsis-v"></i>
<div class="flex items-center justify-end space-x-2">
<!-- Detail Button -->
<button onclick="showAnnouncementDetails('{{ $announcement->id }}')"
class="inline-flex items-center px-3 py-1.5 border border-blue-500 text-sm font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
<i class="fas fa-eye mr-1"></i> Detail
</button>
<!-- Delete Button -->
<button onclick="confirmDelete('{{ $announcement->id }}')"
class="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors">
<i class="fas fa-trash-alt mr-1"></i> Hapus
</button>
<div class="dropdown-menu absolute right-0 mt-1 w-40 bg-white rounded-md shadow-lg py-1 z-10 hidden border border-gray-200">
<a href="{{ route('admin.announcement.show', $announcement->id) }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-eye mr-2"></i> Detail
</a>
<form action="{{ route('admin.announcement.destroy', $announcement->id) }}" method="POST" class="block w-full">
@csrf
@method('DELETE')
<button type="button" onclick="confirmDelete(this)" class="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
<i class="fas fa-trash-alt mr-2"></i> Hapus
</button>
</form>
</div>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-6 py-12 text-center">
<div class="mx-auto w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<i class="fas fa-bullhorn text-3xl text-gray-400"></i>
<td colspan="6" class="px-6 py-8 text-center">
<div class="flex flex-col items-center justify-center">
<i class="fas fa-inbox text-3xl text-gray-300 mb-3"></i>
<p class="text-gray-500 text-sm">Tidak ada riwayat pengumuman</p>
@if(request()->has('mode') || request()->has('start_date') || request()->has('end_date'))
<a href="{{ route('admin.announcement.history') }}" class="text-blue-600 hover:text-blue-800 mt-2 text-sm flex items-center">
<i class="fas fa-sync-alt mr-1"></i> Reset filter
</a>
@endif
</div>
<h3 class="text-lg font-medium text-gray-700">Belum Ada Pengumuman</h3>
<p class="text-gray-500 mt-1">Tidak ada riwayat pengumuman yang ditemukan</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
@if($announcements->hasPages())
<div class="bg-gray-50 px-6 py-4 border-t border-gray-200">
{{ $announcements->appends(request()->query())->links() }}
<div class="flex flex-col md:flex-row items-center justify-between">
<div class="mb-4 md:mb-0">
<p class="text-sm text-gray-700">
Menampilkan <span class="font-medium">{{ $announcements->firstItem() }}</span>
sampai <span class="font-medium">{{ $announcements->lastItem() }}</span>
dari <span class="font-medium">{{ $announcements->total() }}</span> hasil
</p>
</div>
<div>
{{ $announcements->appends(request()->query())->links() }}
</div>
</div>
</div>
@endif
</div>
</div>
<!-- SweetAlert CDN -->
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Custom Script -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Confirm delete function
function confirmDelete(form) {
// Show announcement details in modal
function showAnnouncementDetails(id) {
$.get(`/admin/announcement/${id}/details`, function(data) {
Swal.fire({
title: 'Hapus Pengumuman?',
text: "Anda tidak akan bisa mengembalikan data ini!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#6366f1',
cancelButtonColor: '#d33',
confirmButtonText: 'Ya, Hapus!',
cancelButtonText: 'Batal',
backdrop: 'rgba(99, 102, 241, 0.1)'
}).then((result) => {
if (result.isConfirmed) {
form.closest('form').submit();
title: 'Detail Pengumuman',
html: `
<div class="text-left space-y-3">
<div class="flex items-center">
<span class="font-medium w-24">Mode:</span>
<span class="px-2.5 py-0.5 inline-flex text-xs leading-4 font-medium rounded-full ${data.mode === 'tts' ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800'}">
${data.mode === 'tts' ? 'Text-to-Speech' : 'Manual'}
</span>
</div>
<div>
<span class="font-medium w-24">Waktu:</span>
<span>${data.formatted_sent_at}</span>
</div>
<div>
<span class="font-medium w-24">Ruangan:</span>
<div class="flex flex-wrap gap-1 mt-1">
${data.ruangans.map(r => `
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
<span class="w-1.5 h-1.5 rounded-full mr-1 ${data.mode === 'tts' ? 'bg-green-500' : 'bg-blue-500'}"></span>
${r.nama_ruangan}
</span>
`).join('')}
</div>
</div>
<div class="mt-4 p-3 bg-gray-50 rounded border border-gray-200">
<p class="font-medium text-sm">Isi Pengumuman:</p>
<p class="mt-1 text-sm">${data.message}</p>
</div>
</div>
`,
confirmButtonText: 'Tutup',
width: '600px',
customClass: {
confirmButton: 'px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium'
}
});
}
});
}
// Dropdown menu handler
document.addEventListener('click', function(e) {
if (!e.target.closest('.dropdown')) {
document.querySelectorAll('.dropdown-menu').forEach(menu => {
menu.classList.add('hidden');
// Confirm deletion
function confirmDelete(id) {
Swal.fire({
title: 'Hapus Pengumuman?',
text: "Data yang dihapus tidak dapat dikembalikan!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ya, Hapus!',
cancelButtonText: 'Batal',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
$.ajax({
url: `/admin/announcement/${id}`,
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
success: function() {
Swal.fire({
title: 'Terhapus!',
text: 'Pengumuman telah dihapus.',
icon: 'success',
timer: 1500,
timerProgressBar: true,
showConfirmButton: false,
willClose: () => {
location.reload();
}
});
},
error: function() {
Swal.fire({
title: 'Gagal!',
text: 'Terjadi kesalahan saat menghapus pengumuman.',
icon: 'error',
confirmButtonText: 'Tutup'
});
}
});
} else {
const dropdown = e.target.closest('.dropdown');
const menu = dropdown.querySelector('.dropdown-menu');
menu.classList.toggle('hidden');
}
});
}
// Date range validation
document.getElementById('filterForm').addEventListener('submit', function(e) {
const startDate = new Date(document.getElementById('start_date').value);
const endDate = new Date(document.getElementById('end_date').value);
if (document.getElementById('start_date').value && document.getElementById('end_date').value && startDate > endDate) {
e.preventDefault();
Swal.fire({
title: 'Tanggal Tidak Valid',
text: 'Tanggal akhir tidak boleh sebelum tanggal awal',
icon: 'error',
confirmButtonText: 'Tutup'
});
}
});
</script>
@endsection

File diff suppressed because it is too large Load Diff

View File

@ -1,80 +0,0 @@
@extends('layouts.dashboard')
@section('content')
<div class="container-fluid">
<div class="row mb-4">
<div class="col-md-12">
<h2 class="fw-bold">Detail Pengumuman</h2>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Informasi Pengumuman</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<strong>Waktu:</strong> {{ $announcement->sent_at->format('d M Y H:i:s') }}
</div>
<div class="col-md-6">
<strong>Mode:</strong>
<span class="badge bg-{{ $announcement->mode === 'reguler' ? 'primary' : 'success' }}">
{{ strtoupper($announcement->mode) }}
</span>
</div>
</div>
<div class="mb-3">
<strong>Ruangan Tujuan:</strong>
<div class="mt-2">
@foreach($announcement->ruangans as $ruangan)
<span class="badge bg-secondary mb-1">{{ $ruangan->nama_ruangan }}</span>
@endforeach
</div>
</div>
<div class="mb-3">
<strong>Isi Pengumuman:</strong>
<div class="p-3 bg-light rounded mt-2">
@if($announcement->mode === 'tts')
<p>{{ $announcement->message }}</p>
@if($announcement->audio_path)
<audio controls class="w-100 mt-3">
<source src="{{ $announcement->audio_url }}" type="audio/wav">
Browser Anda tidak mendukung pemutar audio.
</audio>
@endif
<div class="mt-2">
<small class="text-muted">
<strong>Suara:</strong> {{ $announcement->voice }} |
<strong>Kecepatan:</strong> {{ $announcement->speed }}
</small>
</div>
@else
<p>{{ $announcement->message }}</p>
@endif
</div>
</div>
<div class="row">
<div class="col-md-6">
<strong>Dibuat Oleh:</strong> {{ $announcement->user->name }}
</div>
<div class="col-md-6">
<strong>Dibuat Pada:</strong> {{ $announcement->created_at->format('d M Y H:i:s') }}
</div>
</div>
</div>
<div class="card-footer text-end">
<a href="{{ route('admin.announcement.index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i> Kembali
</a>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -41,21 +41,6 @@ class="bg-gradient-to-r from-blue-600 to-blue-800 hover:from-blue-700 hover:to-b
</div>
</div>
</div>
<!-- Jurusan Aktif -->
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-green-500 hover:shadow-md transition duration-300">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-sm font-medium text-gray-500">Jurusan Aktif</h3>
<p class="text-2xl font-bold text-gray-700">{{ $jurusan->count() }}</p>
</div>
</div>
</div>
</div>
<!-- Main Content Card -->
@ -123,15 +108,9 @@ class="block w-full md:w-64 rounded-md border-gray-300 shadow-sm focus:border-bl
</div>
<div>
<div class="text-sm font-semibold text-gray-900">{{ $item->nama_jurusan }}</div>
<!-- <div class="text-xs text-gray-500">Kode: {{ $item->kode_jurusan ?? '-' }}</div> -->
</div>
</div>
</td>
<!-- <td class="px-6 py-4 whitespace-nowrap">
<span class="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
{{ $item->ruangan_count ?? 0 }} Ruangan
</span>
</td> -->
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex justify-end space-x-3">
<a href="{{ route('admin.jurusan.edit', $item->id) }}"

View File

@ -43,36 +43,6 @@ class="bg-gradient-to-r from-blue-600 to-blue-800 hover:from-blue-700 hover:to-b
</div>
</div>
</div>
<!-- Kelas Aktif -->
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-green-500 hover:shadow-md transition duration-300">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-sm font-medium text-gray-500">Kelas Aktif</h3>
<p class="text-2xl font-bold text-gray-700">{{ $kelas->count() }}</p>
</div>
</div>
</div>
<!-- Kelas per Jurusan -->
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-purple-500 hover:shadow-md transition duration-300">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-sm font-medium text-gray-500">Jurusan Terdaftar</h3>
<p class="text-2xl font-bold text-gray-700">{{ $kelas->unique('jurusan_id')->count() }}</p>
</div>
</div>
</div>
</div>
<!-- Main Content Card -->

View File

@ -41,36 +41,6 @@ class="bg-gradient-to-r from-blue-600 to-blue-800 hover:from-blue-700 hover:to-b
</div>
</div>
</div>
<!-- Ruangan Aktif -->
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-green-500 hover:shadow-md transition duration-300">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-sm font-medium text-gray-500">Ruangan Tersedia</h3>
<p class="text-2xl font-bold text-gray-700">{{ $ruangan->count() }}</p>
</div>
</div>
</div>
<!-- Ruangan per Jurusan -->
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-purple-500 hover:shadow-md transition duration-300">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-sm font-medium text-gray-500">Jurusan Terdaftar</h3>
<p class="text-2xl font-bold text-gray-700">{{ $ruangan->unique('jurusan_id')->count() }}</p>
</div>
</div>
</div>
</div>
<!-- Main Content Card -->
@ -138,8 +108,7 @@ class="block w-full md:w-64 rounded-md border-gray-300 shadow-sm focus:border-bl
</svg>
</div>
<div>
<div class="text-sm font-semibold text-gray-900">{{ $item->nama_ruangan }}</div>
<div class="text-xs text-gray-500">Kapasitas: 30 siswa</div>
<div class="text-sm font-semibold text-gray-900">ruangan {{ $item->nama_ruangan }}</div>
</div>
</div>
</td>

View File

@ -106,11 +106,20 @@
});
});
// Announcement System
// Announcement System
Route::prefix('announcement')->controller(AnnouncementController::class)->group(function () {
Route::get('/', 'index')->name('admin.announcement.index');
Route::get('/history', 'history')->name('admin.announcement.history');
Route::post('/', 'store')->name('admin.announcement.store');
Route::get('/{id}/details', 'details')->name('admin.announcement.details');
Route::delete('/{id}', 'destroy')->name('admin.announcement.destroy');
// MQTT & Relay
Route::get('/check/mqtt', 'checkMqtt')->name('admin.check.mqtt');
Route::post('/control-relay', 'controlRelay')->name('admin.announcement.control-relay');
Route::get('/relay-status', 'relayStatus')->name('admin.announcement.relay-status');
});
});
});