relasi ruangan-pengumuman,history pengumuman

This commit is contained in:
rendygaafk 2025-04-24 03:33:41 +07:00
parent 4a66651e95
commit ce4412bf2f
17 changed files with 939 additions and 844 deletions

View File

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Models\Room;
use App\Models\Ruangan;
use App\Services\MqttService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
@ -26,17 +26,22 @@ public function __construct(MqttService $mqttService)
*/
public function index()
{
$rooms = Room::pluck('name')->toArray();
$ruangans = Ruangan::pluck('nama_ruangan')->toArray();
$activeAnnouncements = Announcement::where('is_active', true)->get();
$announcementHistory = Announcement::where('is_active', false)
->orderBy('sent_at', 'desc')
->paginate(10);
if (empty($rooms)) {
$rooms = ['ruang1', 'ruang2', 'ruang3'];
if (empty($ruangans)) {
$ruangans = ['ruang1', 'ruang2', 'ruang3'];
}
return view('admin.announcements.index', compact('rooms', 'activeAnnouncements', 'announcementHistory'));
return view('admin.announcements.index', compact('ruangans', 'activeAnnouncements'));
}
/**
* Display announcement history page
*/
public function history()
{
return view('admin.announcements.history');
}
/**
@ -60,7 +65,6 @@ public function checkActive()
return response()->json([
'active' => $active,
'type' => $active ? Cache::get('active_announcement_type') : null,
'duration' => $active ? Cache::get('active_announcement_duration') : null,
'remaining' => $active ? (Cache::get('active_announcement_end') - time()) : null
]);
}
@ -70,34 +74,59 @@ public function checkActive()
*/
public function activeAnnouncements()
{
$announcements = Announcement::where('is_active', true)
$announcements = Announcement::with('ruangans')
->where('is_active', true)
->orderBy('sent_at', 'desc')
->get();
->get()
->map(function($item) {
return [
'id' => $item->id,
'type' => $item->type,
'content' => $item->content,
'target_ruangans' => $item->target_ruangans,
'sent_at' => $item->sent_at,
'status' => $item->status,
'ruangans' => $item->ruangans
];
});
return response()->json($announcements);
}
/**
* Get announcement history
* Get announcement history (for API)
*/
public function announcementHistory(Request $request)
public function getHistory(Request $request)
{
$perPage = 10;
$query = Announcement::where('is_active', false)
->orderBy('sent_at', 'desc');
if ($request->has('search')) {
$query->where('content', 'like', '%'.$request->search.'%');
}
if ($request->has('filter') && $request->filter != 'all') {
// Filter by type
if ($request->filter && in_array($request->filter, ['tts', 'manual'])) {
$query->where('type', $request->filter);
}
if ($request->has('sort')) {
$query->orderBy('sent_at', $request->sort == 'oldest' ? 'asc' : 'desc');
// Filter by status
if ($request->status && in_array($request->status, ['completed', 'stopped'])) {
$query->where('status', $request->status);
}
return response()->json($query->paginate(10));
// Search
if ($request->search) {
$query->where('content', 'like', '%'.$request->search.'%');
}
$paginated = $query->paginate($perPage);
return response()->json([
'data' => $paginated->items(),
'total' => $paginated->total(),
'from' => $paginated->firstItem(),
'to' => $paginated->lastItem(),
'current_page' => $paginated->currentPage(),
'last_page' => $paginated->lastPage()
]);
}
/**
@ -105,8 +134,18 @@ public function announcementHistory(Request $request)
*/
public function announcementDetails($id)
{
$announcement = Announcement::findOrFail($id);
return response()->json($announcement);
$announcement = Announcement::with('ruangans')->findOrFail($id);
return response()->json([
'id' => $announcement->id,
'type' => $announcement->type,
'content' => $announcement->content,
'target_ruangans' => $announcement->target_ruangans,
'sent_at' => $announcement->sent_at,
'status' => $announcement->status,
'audio_url' => $announcement->audio_url,
'ruangans' => $announcement->ruangans
]);
}
/**
@ -114,57 +153,78 @@ public function announcementDetails($id)
*/
public function send(Request $request)
{
// Validasi input
$validated = $request->validate([
'type' => 'required|in:tts,manual',
'content' => 'nullable|required_if:type,tts|string|max:500',
'rooms' => 'required|array|min:1',
'rooms.*' => 'string',
'duration' => 'nullable|integer|min:5|max:300',
'ruangans' => 'required|array|min:1',
'ruangans.*' => 'string',
]);
if ($validated['type'] === 'tts' && empty(config('services.voicerss.key'))) {
return response()->json([
'success' => false,
'message' => 'Layanan Text-to-Speech tidak tersedia saat ini.'
], 400);
}
try {
$announcement = Announcement::create([
// Persiapkan data awal
$announcementData = [
'type' => $validated['type'],
'content' => $validated['type'] === 'tts' ? $validated['content'] : null,
'target_rooms' => $validated['rooms'],
'duration' => $validated['type'] === 'manual' ? $validated['duration'] : null,
'target_ruangans' => $validated['ruangans'],
'sent_at' => now(),
'is_active' => true,
'status' => 'processing'
]);
];
// Handle TTS
if ($validated['type'] === 'tts') {
$announcementData['content'] = $validated['content'];
// Generate audio dan simpan URL
$audioUrl = $this->generateTtsAudio($validated['content']);
$announcementData['audio_url'] = $audioUrl;
// TTS langsung dianggap selesai
$announcementData['is_active'] = false;
$announcementData['status'] = 'completed';
}
// Buat pengumuman
$announcement = Announcement::create($announcementData);
// Siapkan payload MQTT
$payload = [
'type' => $validated['type'],
'target_rooms' => $validated['rooms'],
'target_ruangans' => $validated['ruangans'],
'timestamp' => now()->toDateTimeString(),
'announcement_id' => $announcement->id
];
// Tambahkan data khusus TTS
if ($validated['type'] === 'tts') {
$payload['content'] = $validated['content'];
$payload['audio_url'] = $this->generateTtsAudio($validated['content']);
$payload['auto_stop'] = true;
} else {
$payload['duration'] = $validated['duration'] ?? 60;
$payload['audio_url'] = $audioUrl; // Gunakan variabel yang sudah digenerate
}
// Kirim via MQTT
$this->mqttService->sendAnnouncement($payload);
// Update cache for active announcement
if ($validated['type'] === 'manual') {
Cache::put('active_announcement', true);
Cache::put('active_announcement_type', 'manual');
Cache::put('active_announcement_duration', $validated['duration']);
Cache::put('active_announcement_end', time() + $validated['duration']);
}
return response()->json([
'success' => true,
'message' => 'Pengumuman berhasil dikirim!',
'announcement_id' => $announcement->id,
'audio_url' => $audioUrl ?? null // Sertakan audio_url dalam response
]);
} catch (\Exception $e) {
Log::error('Announcement error: ' . $e->getMessage());
// Hapus record jika gagal setelah create
if (isset($announcement)) {
$announcement->delete();
}
return response()->json([
'success' => false,
'message' => 'Gagal mengirim pengumuman: ' . $e->getMessage(),
@ -179,13 +239,15 @@ public function stopManual()
{
try {
Announcement::where('is_active', true)
->where('type', 'manual')
->update([
'is_active' => false,
'status' => 'completed'
]);
$payload = [
'type' => 'stop_manual',
'type' => 'manual',
'action' => 'deactivate_relay',
'timestamp' => now()->toDateTimeString(),
];
@ -194,18 +256,16 @@ public function stopManual()
// Clear active announcement cache
Cache::forget('active_announcement');
Cache::forget('active_announcement_type');
Cache::forget('active_announcement_duration');
Cache::forget('active_announcement_end');
return response()->json([
'success' => true,
'message' => 'Routing audio berhasil diputus!',
'message' => 'Relay ruangan berhasil dimatikan!',
]);
} catch (\Exception $e) {
Log::error('Stop manual error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Gagal memutus routing audio: ' . $e->getMessage(),
'message' => 'Gagal mematikan relay: ' . $e->getMessage(),
], 500);
}
}
@ -224,7 +284,9 @@ public function stopAnnouncement(Request $request)
if ($announcement->type === 'manual') {
$payload = [
'type' => 'stop_manual',
'type' => 'manual',
'action' => 'deactivate_relay',
'announcement_id' => $announcement->id,
'timestamp' => now()->toDateTimeString(),
];
$this->mqttService->publish('bel/sekolah/pengumuman', json_encode($payload), 1, false);
@ -243,91 +305,83 @@ public function stopAnnouncement(Request $request)
}
}
/**
* Manage rooms (add, edit, delete)
*/
public function manageRoom(Request $request)
{
$validator = Validator::make($request->all(), [
'action' => 'required|in:add,edit,delete',
'room_name' => 'required|string|max:50',
'old_room' => 'nullable|string|max:50'
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => $validator->errors()->first()
], 400);
}
try {
switch ($request->action) {
case 'add':
Room::create(['name' => $request->room_name]);
break;
case 'edit':
$room = Room::where('name', $request->old_room)->firstOrFail();
$room->update(['name' => $request->room_name]);
// Update existing announcements that use this room
Announcement::whereJsonContains('target_rooms', $request->old_room)
->each(function($announcement) use ($request) {
$updatedRooms = array_map(function($room) use ($request) {
return $room == $request->old_room ? $request->room_name : $room;
}, $announcement->target_rooms);
$announcement->update(['target_rooms' => $updatedRooms]);
});
break;
case 'delete':
$room = Room::where('name', $request->room_name)->firstOrFail();
$room->delete();
break;
}
return response()->json([
'success' => true,
'message' => 'Ruangan berhasil di' . ($request->action == 'add' ? 'tambah' : ($request->action == 'edit' ? 'edit' : 'hapus')),
'rooms' => Room::pluck('name')->toArray()
]);
} catch (\Exception $e) {
Log::error('Room management error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Gagal mengelola ruangan: ' . $e->getMessage()
], 500);
}
}
/**
* Generate TTS audio using VoiceRSS
*/
private function generateTtsAudio($text)
{
$apiKey = config('services.voicerss.api_key');
$apiKey = config('services.voicerss.key');
if (!$apiKey) {
throw new \Exception('VoiceRSS API Key tidak dikonfigurasi');
// Validate API key configuration
if (empty($apiKey)) {
Log::error('VoiceRSS API Key not configured');
return $this->getTtsFallback($text); // Use fallback instead of throwing exception
}
$response = Http::timeout(10)->get('https://api.voicerss.org', [
'key' => $apiKey,
'hl' => 'id-id',
'src' => $text,
'r' => '0',
'c' => 'mp3',
'f' => '44khz_16bit_stereo',
]);
try {
// Generate unique filename
$filename = 'tts/'.md5($text.'_'.microtime()).'.mp3';
$storagePath = Storage::disk('public')->path($filename);
// Create directory if not exists
Storage::disk('public')->makeDirectory('tts');
if ($response->successful()) {
$filename = 'tts/' . uniqid() . '.mp3';
$response = Http::timeout(20)
->retry(3, 500)
->get('https://api.voicerss.org/', [
'key' => $apiKey,
'hl' => 'id-id',
'src' => $text,
'r' => '0',
'c' => 'mp3',
'f' => '44khz_16bit_stereo',
]);
if (!$response->successful()) {
Log::error('VoiceRSS API Error', [
'status' => $response->status(),
'response' => $response->body()
]);
return $this->getTtsFallback($text);
}
// Save audio file
Storage::disk('public')->put($filename, $response->body());
return Storage::url($filename);
}
throw new \Exception('Gagal menghasilkan audio TTS');
// Verify file was saved
if (!Storage::disk('public')->exists($filename)) {
Log::error('Failed to save TTS file', ['path' => $filename]);
return $this->getTtsFallback($text);
}
return Storage::url($filename);
} catch (\Exception $e) {
Log::error('TTS Generation Failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return $this->getTtsFallback($text);
}
}
private function getTtsFallback($text)
{
try {
// Create simple fallback audio
$filename = 'tts/fallback_'.md5($text).'.mp3';
$path = Storage::disk('public')->path($filename);
// Generate basic audio file using shell_exec or other method
if (!Storage::disk('public')->exists($filename)) {
$command = "text2wave -o {$path} -eval '(language_indonesian)'";
shell_exec("echo \"{$text}\" | {$command}");
}
return Storage::url($filename);
} catch (\Exception $e) {
Log::error('Fallback TTS failed', ['error' => $e->getMessage()]);
return null; // Return null if both main and fallback fail
}
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -11,24 +12,25 @@ class Announcement extends Model
protected $fillable = [
'type',
'content',
'target_rooms',
'target_ruangans', // Menggunakan 'ruangan' bukan 'target_ruangans'
'duration',
'sent_at',
'is_active',
'status'
'status',
'audio_url'
];
protected $casts = [
'target_rooms' => 'array', // Kolom ini akan disimpan sebagai JSON
'target_ruangans' => 'array',
'sent_at' => 'datetime',
'is_active' => 'boolean'
'is_active' => 'boolean',
];
/**
* Relationship with Room (Many-to-Many)
* Relasi ke model Ruangan
*/
public function rooms()
public function ruangans()
{
return $this->belongsToMany(Room::class, 'announcement_room', 'announcement_id', 'room_name');
return Ruangan::whereIn('nama', $this->ruangan)->get();
}
}

View File

@ -13,17 +13,61 @@ class Ruangan extends Model
protected $fillable = [
'nama_ruangan',
'id_kelas',
'id_jurusan',
'kelas_id',
'jurusan_id'
];
/**
* Relasi ke model Kelas
*/
public function kelas()
{
return $this->belongsTo(Kelas::class, 'id_kelas');
return $this->belongsTo(Kelas::class, 'kelas_id');
}
/**
* Relasi ke model Jurusan
*/
public function jurusan()
{
return $this->belongsTo(Jurusan::class, 'id_jurusan');
return $this->belongsTo(Jurusan::class, 'jurusan_id');
}
}
/**
* Relasi ke model Announcement (pengumuman)
*/
public function announcements()
{
return $this->hasMany(Announcement::class, 'ruangan_id');
}
/**
* Accessor untuk nama ruangan
*/
public function getNamaRuanganAttribute($value)
{
return strtoupper($value);
}
/**
* Mutator untuk nama ruangan
*/
public function setNamaRuanganAttribute($value)
{
$this->attributes['nama_ruangan'] = strtolower($value);
}
/**
* Scope untuk pencarian ruangan
*/
public function scopeSearch($query, $term)
{
return $query->where('nama_ruangan', 'like', "%{$term}%")
->orWhereHas('kelas', function($q) use ($term) {
$q->where('nama_kelas', 'like', "%{$term}%");
})
->orWhereHas('jurusan', function($q) use ($term) {
$q->where('nama_jurusan', 'like', "%{$term}%");
});
}
}

View File

@ -297,21 +297,14 @@ public function sendAnnouncement($payload)
// Jika TTS, kirim perintah stop setelah delay
if ($payload['type'] === 'tts' && $payload['auto_stop'] ?? false) {
$duration = $this->estimateTtsDuration($payload['content']);
sleep($duration + 2); // Tambah buffer 2 detik
$stopPayload = [
'type' => 'stop_tts',
'target_rooms' => $payload['target_rooms'],
'target_ruangans' => $payload['target_ruangans'],
'timestamp' => now()->toDateTimeString()
];
$this->publish($topic, json_encode($stopPayload), 1, false);
}
}
private function estimateTtsDuration($text)
{
// Estimasi 0.3 detik per karakter (termasuk jeda)
return min(300, ceil(strlen($text) * 0.3)); // Max 5 menit
}
}

View File

@ -1,21 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAnnouncementsTable extends Migration
return new class extends Migration
{
public function up()
{
Schema::create('announcements', function (Blueprint $table) {
$table->id();
$table->enum('type', ['tts', 'manual']); // Jenis pengumuman: TTS atau Manual
$table->text('content')->nullable(); // Konten pengumuman (untuk TTS)
$table->json('target_rooms'); // Daftar ruangan target (disimpan sebagai JSON)
$table->integer('duration')->nullable(); // Durasi pengumuman (untuk manual)
$table->timestamp('sent_at')->nullable(); // Waktu pengiriman
$table->boolean('is_active')->default(false); // Status aktif/tidak
$table->enum('status', ['processing', 'completed', 'stopped'])->default('processing'); // Status pengumuman
$table->string('type'); // 'tts' or 'manual'
$table->text('content')->nullable();
$table->json('target_ruangans')->nullable();
$table->timestamp('sent_at');
$table->boolean('is_active')->default(false);
$table->string('status')->default('pending');
$table->string('audio_url')->nullable();
$table->timestamps();
});
}
@ -24,4 +25,4 @@ public function down()
{
Schema::dropIfExists('announcements');
}
}
};

View File

@ -1,29 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAnnouncementRoomTable extends Migration
{
public function up()
{
Schema::create('announcement_room', function (Blueprint $table) {
$table->unsignedBigInteger('announcement_id'); // ID pengumuman
$table->string('room_name'); // Nama ruangan
// Primary key gabungan
$table->primary(['announcement_id', 'room_name']);
// Foreign keys
$table->foreign('announcement_id')->references('id')->on('announcements')->onDelete('cascade');
$table->foreign('room_name')->references('name')->on('rooms')->onDelete('cascade');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('announcement_room');
}
}

View File

@ -6,26 +6,31 @@
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('ruangan', function (Blueprint $table) {
$table->id();
$table->string('nama_ruangan');
$table->timestamps();
// Foreign keys
$table->foreignId('kelas_id')->constrained('kelas')->onDelete('cascade');
$table->foreignId('jurusan_id')->constrained('jurusan')->onDelete('cascade');
$table->timestamps();
// Tambahkan index untuk pencarian
$table->index('nama_ruangan');
$table->index(['kelas_id', 'jurusan_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('ruangan', function (Blueprint $table) {
$table->dropForeign(['kelas_id']);
$table->dropForeign(['jurusan_id']);
});
Schema::dropIfExists('ruangan');
}
};
};

View File

@ -0,0 +1,48 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Ruangan;
use App\Models\Kelas;
use App\Models\Jurusan;
class RuanganSeeder extends Seeder
{
public function run(): void
{
// Hapus semua data ruangan sebelum menyisipkan data baru
Ruangan::truncate();
// Ambil semua ID kelas dan jurusan
$kelasIds = Kelas::pluck('id')->toArray();
$jurusanIds = Jurusan::pluck('id')->toArray();
$data = [];
foreach ($kelasIds as $kelasId) {
foreach ($jurusanIds as $jurusanId) {
// Ekstrak angka dari nama kelas
$kelasNumber = (int) preg_replace('/[^0-9]/', '', Kelas::find($kelasId)->nama_kelas);
$jurusanNumber = array_search($jurusanId, $jurusanIds) + 1;
$nama_ruangan = "{$kelasNumber}.{$jurusanNumber}";
// Pastikan kombinasi kelas_id dan jurusan_id unik
if (!Ruangan::where('kelas_id', $kelasId)
->where('jurusan_id', $jurusanId)
->exists()) {
$data[] = [
'nama_ruangan' => $nama_ruangan,
'kelas_id' => $kelasId,
'jurusan_id' => $jurusanId,
];
}
}
}
// Masukkan data ke tabel ruangan
foreach ($data as $item) {
Ruangan::create($item);
}
}
}

View File

@ -4,7 +4,6 @@
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Status;
class StatusSeeder extends Seeder // Nama kelas harus sama dengan nama file
{

View File

@ -1,193 +0,0 @@
@extends('layouts.dashboard')
@section('content')
<div class="container mx-auto p-6">
<!-- Judul Halaman -->
<h1 class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-purple-600 mb-8 text-center">
Tambah Pengumuman
</h1>
<!-- Card Container -->
<div class="bg-white shadow-xl rounded-lg overflow-hidden max-w-3xl mx-auto">
<div class="p-6">
<!-- Form -->
<form id="announcement-form" action="{{ route('announcements.store') }}" method="POST" class="space-y-6">
@csrf
<!-- Judul Pengumuman -->
<div>
<label for="title" class="block text-sm font-medium text-gray-700">
Judul Pengumuman <span class="text-red-500">*</span>
</label>
<input type="text" id="title" name="title"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
placeholder="Masukkan judul pengumuman..."
required>
</div>
<!-- Tipe Pengumuman -->
<div>
<label for="type" class="block text-sm font-medium text-gray-700">
Tipe Pengumuman <span class="text-red-500">*</span>
</label>
<select id="type" name="type"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
required>
<option value="tts">TTS (Text-to-Speech)</option>
<option value="manual">Manual Mic</option>
</select>
</div>
<!-- Isi Pengumuman (Hanya untuk TTS) -->
<div id="content-field" style="display: none;">
<label for="content" class="block text-sm font-medium text-gray-700">
Isi Pengumuman <span class="text-red-500">*</span>
</label>
<textarea id="content" name="content" rows="4"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
placeholder="Masukkan teks pengumuman..."
minlength="5" maxlength="200"></textarea>
<span id="text-error" class="text-sm text-red-500 mt-1 hidden">Teks minimal 5 karakter dan maksimal 200 karakter.</span>
</div>
<!-- Pilih Bahasa (Hanya untuk TTS) -->
<div id="language-field" style="display: none;">
<label for="language" class="block text-sm font-medium text-gray-700">
Bahasa <span class="text-red-500">*</span>
</label>
<select id="language" name="language"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="id">Indonesia</option>
<option value="en">English</option>
</select>
</div>
<!-- Pilih Ruangan -->
<div>
<label class="block text-sm font-medium text-gray-700 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M4 4a2 2 0 012-2h8a2 2 0 012 2v8a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 00-1 1v3a1 1 0 001 1h8a1 1 0 001-1v-3a1 1 0 00-1-1H6z"
clip-rule="evenodd" />
</svg>
Pilih Ruangan <span class="text-red-500">*</span>
</label>
<!-- Checkbox Semua -->
<div class="flex items-center mt-2">
<input type="checkbox" id="select-all-rooms"
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="select-all-rooms" class="ml-2 text-sm text-gray-700">Pilih Semua</label>
</div>
<!-- Daftar Checkbox Ruangan -->
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 mt-4">
@foreach($rooms as $room)
<div class="flex items-center">
<input type="checkbox" id="room-{{ $room->id }}" name="rooms[]"
value="{{ $room->name }}"
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded room-checkbox">
<label for="room-{{ $room->id }}"
class="ml-2 text-sm text-gray-700">{{ $room->name }}</label>
</div>
@endforeach
</div>
</div>
<!-- Tombol Aksi -->
<div class="flex justify-between items-center">
<a href="{{ route('announcements.index') }}"
class="text-gray-500 hover:text-gray-700 font-medium transition duration-300 ease-in-out flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm-3.707-8.293a1 1 0 011.414 0L10 11.586l2.293-2.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
Kembali ke Daftar Pengumuman
</a>
<button type="submit" id="submit-btn"
class="relative inline-flex items-center justify-center px-8 py-3 font-medium text-white rounded-lg transition duration-300 ease-in-out hover:scale-105 shadow-md bg-gradient-to-r from-blue-600 to-purple-600">
<svg id="spinner" class="hidden animate-spin mr-2 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
</svg>
Simpan Pengumuman
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Script -->
<script>
const form = document.getElementById('announcement-form');
const submitBtn = document.getElementById('submit-btn');
const spinner = document.getElementById('spinner');
const contentField = document.getElementById('content-field');
const languageField = document.getElementById('language-field');
const typeField = document.getElementById('type');
// Tampilkan/hilangkan field berdasarkan tipe pengumuman
typeField.addEventListener('change', () => {
if (typeField.value === 'tts') {
contentField.style.display = 'block';
languageField.style.display = 'block';
} else {
contentField.style.display = 'none';
languageField.style.display = 'none';
}
});
// Checkbox "Pilih Semua"
const selectAll = document.getElementById('select-all-rooms');
const roomCheckboxes = document.querySelectorAll('.room-checkbox');
selectAll.addEventListener('change', () => {
roomCheckboxes.forEach(cb => cb.checked = selectAll.checked);
});
roomCheckboxes.forEach(cb => {
cb.addEventListener('change', () => {
if (!cb.checked) selectAll.checked = false;
else selectAll.checked = Array.from(roomCheckboxes).every(c => c.checked);
});
});
// Validasi panjang teks
const contentTextarea = document.getElementById('content');
const errorSpan = document.getElementById('text-error');
if (contentTextarea) {
contentTextarea.addEventListener('input', function () {
const value = contentTextarea.value.trim();
if (value.length < 5 || value.length > 200) {
errorSpan.classList.remove('hidden');
} else {
errorSpan.classList.add('hidden');
}
});
}
// Handle form submission
form.addEventListener('submit', function () {
// Tampilkan loading SweetAlert
Swal.fire({
title: 'Memproses Pengumuman...',
text: 'Mohon tunggu sebentar.',
allowOutsideClick: false,
showConfirmButton: false,
willOpen: () => Swal.showLoading()
});
// Tampilkan spinner dan disable tombol
spinner.classList.remove('hidden');
submitBtn.setAttribute('disabled', true);
});
</script>
@endsection

View File

@ -1,104 +0,0 @@
@extends('layouts.dashboard')
@section('content')
<div class="container mx-auto p-6">
<!-- Judul Halaman -->
<h1 class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-red-600 to-yellow-600 mb-8 text-center">
Mode Darurat
</h1>
<!-- Card Container -->
<div class="bg-white shadow-xl rounded-lg overflow-hidden max-w-3xl mx-auto">
<div class="p-6">
<!-- Form -->
<form id="emergency-form" action="{{ route('announcements.emergency') }}" method="POST" class="space-y-6">
@csrf
<!-- Pilih Jenis Darurat -->
<div>
<label for="emergency-type" class="block text-sm font-medium text-gray-700">
Jenis Darurat <span class="text-red-500">*</span>
</label>
<select id="emergency-type" name="emergency_type"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
required>
<option value="fire">Kebakaran</option>
<option value="earthquake">Gempa Bumi</option>
<option value="evacuation">Evakuasi</option>
<option value="other">Lainnya</option>
</select>
</div>
<!-- Isi Pesan Darurat (Hanya untuk "Lainnya") -->
<div id="custom-message-field" style="display: none;">
<label for="custom-message" class="block text-sm font-medium text-gray-700">
Pesan Darurat <span class="text-red-500">*</span>
</label>
<textarea id="custom-message" name="custom_message" rows="4"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
placeholder="Masukkan pesan darurat..."></textarea>
</div>
<!-- Tombol Aksi -->
<div class="flex justify-between items-center">
<a href="{{ route('announcements.index') }}"
class="text-gray-500 hover:text-gray-700 font-medium transition duration-300 ease-in-out flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm-3.707-8.293a1 1 0 011.414 0L10 11.586l2.293-2.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
Kembali ke Daftar Pengumuman
</a>
<button type="submit" id="submit-btn"
class="relative inline-flex items-center justify-center px-8 py-3 font-medium text-white rounded-lg transition duration-300 ease-in-out hover:scale-105 shadow-md bg-gradient-to-r from-red-600 to-yellow-600">
<svg id="spinner" class="hidden animate-spin mr-2 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
</svg>
Aktifkan Mode Darurat
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Script -->
<script>
const emergencyTypeField = document.getElementById('emergency-type');
const customMessageField = document.getElementById('custom-message-field');
// Tampilkan/hilangkan field pesan khusus berdasarkan jenis darurat
emergencyTypeField.addEventListener('change', () => {
if (emergencyTypeField.value === 'other') {
customMessageField.style.display = 'block';
} else {
customMessageField.style.display = 'none';
}
});
const form = document.getElementById('emergency-form');
const submitBtn = document.getElementById('submit-btn');
const spinner = document.getElementById('spinner');
// Handle form submission
form.addEventListener('submit', function () {
// Tampilkan loading SweetAlert
Swal.fire({
title: 'Mengaktifkan Mode Darurat...',
text: 'Mohon tunggu sebentar.',
allowOutsideClick: false,
showConfirmButton: false,
willOpen: () => Swal.showLoading()
});
// Tampilkan spinner dan disable tombol
spinner.classList.remove('hidden');
submitBtn.setAttribute('disabled', true);
});
</script>
@endsection

View File

@ -0,0 +1,395 @@
@extends('layouts.dashboard')
@section('content')
<div class="p-4 sm:p-6">
<!-- Header Section -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4">
<div class="flex items-center space-x-3">
<div class="p-3 rounded-xl bg-blue-100/80 shadow-inner">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h1 class="text-2xl font-bold text-gray-800">Riwayat Pengumuman</h1>
<p class="text-sm text-gray-500">Daftar semua pengumuman yang pernah dikirim</p>
</div>
</div>
<div class="flex items-center space-x-2">
<a href="{{ route('announcements.index') }}" class="px-3 py-1 rounded-lg text-sm font-medium bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z" />
</svg>
Kembali ke Pengumuman
</a>
</div>
</div>
<!-- Filters Section -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 mb-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Search Box -->
<div>
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">Cari Pengumuman</label>
<div class="relative">
<input type="text" id="search" name="search" placeholder="Kata kunci..." class="block w-full pl-4 pr-10 py-2 border border-gray-300 rounded-xl shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 transition duration-150 ease-in-out">
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
</div>
<!-- Type Filter -->
<div>
<label for="type-filter" class="block text-sm font-medium text-gray-700 mb-1">Jenis Pengumuman</label>
<select id="type-filter" class="block w-full pl-3 pr-10 py-2 border border-gray-300 rounded-xl shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 transition duration-150 ease-in-out">
<option value="">Semua Jenis</option>
<option value="tts">Text-to-Speech</option>
<option value="manual">Manual</option>
</select>
</div>
<!-- Status Filter -->
<div>
<label for="status-filter" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select id="status-filter" class="block w-full pl-3 pr-10 py-2 border border-gray-300 rounded-xl shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 transition duration-150 ease-in-out">
<option value="">Semua Status</option>
<option value="completed">Selesai</option>
<option value="stopped">Dihentikan</option>
</select>
</div>
</div>
</div>
<!-- Announcement History Table -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 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">ID</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">Jenis</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Isi</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ruangan</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 id="history-table-body" class="bg-white divide-y divide-gray-200">
<!-- Data will be loaded via AJAX -->
<tr>
<td colspan="7" class="px-6 py-4 text-center text-sm text-gray-500">
Memuat data...
</td>
</tr>
</tbody>
</table>
</div>
<div class="bg-white px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<div class="text-sm text-gray-500" id="pagination-info">
Menampilkan 0 sampai 0 dari 0 entri
</div>
<div class="flex space-x-2" id="pagination-controls">
<button id="prev-page" disabled class="px-3 py-1 rounded-lg text-sm font-medium bg-gray-100 text-gray-500 cursor-not-allowed">
Sebelumnya
</button>
<button id="next-page" disabled class="px-3 py-1 rounded-lg text-sm font-medium bg-gray-100 text-gray-500 cursor-not-allowed">
Selanjutnya
</button>
</div>
</div>
</div>
</div>
<!-- Details Modal -->
<div id="details-modal" class="hidden fixed inset-0 overflow-y-auto z-50">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white rounded-2xl shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full">
<div class="bg-gradient-to-r from-blue-500 to-blue-600 px-6 py-4 rounded-t-2xl">
<h3 class="text-lg font-semibold text-white" id="modal-title">Detail Pengumuman</h3>
</div>
<div class="px-6 py-4">
<div class="space-y-4">
<div>
<h4 class="text-sm font-medium text-gray-500">ID Pengumuman</h4>
<p class="mt-1 text-sm text-gray-900" id="detail-id">-</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Waktu Pengiriman</h4>
<p class="mt-1 text-sm text-gray-900" id="detail-time">-</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Jenis</h4>
<p class="mt-1 text-sm text-gray-900" id="detail-type">-</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Status</h4>
<p class="mt-1 text-sm text-gray-900" id="detail-status">-</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Isi Pengumuman</h4>
<p class="mt-1 text-sm text-gray-900" id="detail-content">-</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Ruangan Tujuan</h4>
<div class="mt-1 flex flex-wrap gap-2" id="detail-rooms">
<!-- Room badges will appear here -->
</div>
</div>
<div id="audio-section" class="hidden">
<h4 class="text-sm font-medium text-gray-500">Audio</h4>
<audio controls class="mt-2 w-full" id="audio-player">
Your browser does not support the audio element.
</audio>
</div>
</div>
</div>
<div class="bg-gray-50 px-6 py-4 rounded-b-2xl flex justify-end">
<button type="button" id="close-modal" class="px-4 py-2 bg-white border border-gray-300 rounded-xl text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Tutup
</button>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
const baseUrl = '/admin/pengumuman';
let currentPage = 1;
let lastPage = 1;
let loading = false;
// Initialize the page
function init() {
loadHistory();
setupEventListeners();
}
// Set up event listeners
function setupEventListeners() {
// Filter changes
$('#search, #type-filter, #status-filter').on('change keyup', function() {
currentPage = 1;
loadHistory();
});
// Pagination controls
$('#prev-page').click(function() {
if (currentPage > 1) {
currentPage--;
loadHistory();
}
});
$('#next-page').click(function() {
if (currentPage < lastPage) {
currentPage++;
loadHistory();
}
});
// Modal close button
$('#close-modal').click(function() {
$('#details-modal').addClass('hidden');
});
}
// Load history data
function loadHistory() {
if (loading) return;
loading = true;
// Show loading state
$('#history-table-body').html(`
<tr>
<td colspan="7" class="px-6 py-4 text-center text-sm text-gray-500">
Memuat data...
</td>
</tr>
`);
const filters = {
search: $('#search').val(),
filter: $('#type-filter').val(),
status: $('#status-filter').val(),
page: currentPage
};
$.get(`${baseUrl}/history`, filters, function(response) {
renderTable(response);
updatePagination(response);
}).fail(function() {
$('#history-table-body').html(`
<tr>
<td colspan="7" class="px-6 py-4 text-center text-sm text-red-500">
Gagal memuat data. Silakan coba lagi.
</td>
</tr>
`);
}).always(function() {
loading = false;
});
}
// Render table data
function renderTable(data) {
if (data.data.length === 0) {
$('#history-table-body').html(`
<tr>
<td colspan="7" class="px-6 py-4 text-center text-sm text-gray-500">
Tidak ada data yang ditemukan
</td>
</tr>
`);
return;
}
let html = '';
data.data.forEach(item => {
// Format time
const sentAt = new Date(item.sent_at);
const timeString = sentAt.toLocaleString('id-ID', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
// Format type
const typeBadge = item.type === 'tts' ?
'<span class="px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800">TTS</span>' :
'<span class="px-2 py-1 text-xs rounded-full bg-amber-100 text-amber-800">Manual</span>';
// Format status
let statusBadge;
if (item.status === 'completed') {
statusBadge = '<span class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800">Selesai</span>';
} else {
statusBadge = '<span class="px-2 py-1 text-xs rounded-full bg-red-100 text-red-800">Dihentikan</span>';
}
// Truncate content if too long
const content = item.content ?
(item.content.length > 50 ? item.content.substring(0, 50) + '...' : item.content) :
'-';
// Room badges (show first 2 only)
let roomsHtml = '';
if (item.target_ruangans && item.target_ruangans.length > 0) {
const roomsToShow = item.target_ruangans.slice(0, 2);
roomsToShow.forEach(room => {
roomsHtml += `<span class="inline-block px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800 mr-1">${room}</span>`;
});
if (item.target_ruangans.length > 2) {
roomsHtml += `<span class="inline-block px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800">+${item.target_ruangans.length - 2}</span>`;
}
}
html += `
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${item.id}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${timeString}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${typeBadge}</td>
<td class="px-6 py-4 text-sm text-gray-500">${content}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${roomsHtml || '-'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${statusBadge}</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button onclick="showDetails(${item.id})" class="text-blue-600 hover:text-blue-900 mr-3">Detail</button>
</td>
</tr>
`;
});
$('#history-table-body').html(html);
}
// Update pagination controls
function updatePagination(data) {
lastPage = data.last_page;
// Update pagination info
$('#pagination-info').text(
`Menampilkan ${data.from || 0} sampai ${data.to || 0} dari ${data.total || 0} entri`
);
// Update previous button
$('#prev-page').prop('disabled', currentPage === 1);
if (currentPage === 1) {
$('#prev-page').addClass('bg-gray-100 text-gray-500 cursor-not-allowed')
.removeClass('bg-white text-gray-700 hover:bg-gray-50');
} else {
$('#prev-page').addClass('bg-white text-gray-700 hover:bg-gray-50')
.removeClass('bg-gray-100 text-gray-500 cursor-not-allowed');
}
// Update next button
$('#next-page').prop('disabled', currentPage === lastPage);
if (currentPage === lastPage) {
$('#next-page').addClass('bg-gray-100 text-gray-500 cursor-not-allowed')
.removeClass('bg-white text-gray-700 hover:bg-gray-50');
} else {
$('#next-page').addClass('bg-white text-gray-700 hover:bg-gray-50')
.removeClass('bg-gray-100 text-gray-500 cursor-not-allowed');
}
}
// Show announcement details
window.showDetails = function(id) {
$.get(`${baseUrl}/${id}`, function(response) {
// Format time
const sentAt = new Date(response.sent_at);
const timeString = sentAt.toLocaleString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
// Set basic info
$('#detail-id').text(response.id);
$('#detail-time').text(timeString);
$('#detail-type').text(response.type === 'tts' ? 'Text-to-Speech' : 'Manual');
$('#detail-status').text(response.status === 'completed' ? 'Selesai' : 'Dihentikan');
$('#detail-content').text(response.content || '-');
// Set rooms
let roomsHtml = '';
if (response.target_ruangans && response.target_ruangans.length > 0) {
response.target_ruangans.forEach(room => {
roomsHtml += `<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">${room}</span>`;
});
}
$('#detail-rooms').html(roomsHtml || '-');
// Handle audio for TTS
if (response.type === 'tts' && response.audio_url) {
$('#audio-section').removeClass('hidden');
$('#audio-player').attr('src', response.audio_url);
} else {
$('#audio-section').addClass('hidden');
}
// Show modal
$('#details-modal').removeClass('hidden');
}).fail(function() {
alert('Gagal memuat detail pengumuman');
});
}
// Initialize the page
init();
});
</script>
@endsection

View File

@ -20,6 +20,12 @@
<span class="w-2 h-2 rounded-full bg-gray-400 mr-2"></span>
<span>Memeriksa koneksi...</span>
</div>
<a href="{{ route('announcements.history') }}" class="px-3 py-1 rounded-lg text-sm font-medium bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Riwayat
</a>
</div>
</div>
@ -77,8 +83,8 @@
<div class="flex items-center">
<div class="flex-shrink-0 h-5 w-5 rounded-full border-2 border-gray-300 peer-checked:border-blue-500 peer-checked:bg-blue-500 peer-checked:border-4 transition-all duration-200 mr-3"></div>
<div>
<h3 class="font-medium text-gray-800">Mikrofon Manual</h3>
<p class="text-sm text-gray-500">Gunakan mikrofon langsung</p>
<h3 class="font-medium text-gray-800">Pengumuman Manual</h3>
<p class="text-sm text-gray-500">Aktifkan relay audio di ruangan terpilih</p>
</div>
</div>
</div>
@ -93,11 +99,11 @@
<button type="button" id="select-all" class="text-xs text-blue-600 hover:text-blue-800 font-medium transition-colors duration-200">Pilih Semua</button>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
@foreach($rooms as $room)
@foreach($ruangans as $ruangan)
<label class="relative cursor-pointer">
<input type="checkbox" name="rooms[]" value="{{ $room }}" class="peer absolute opacity-0">
<input type="checkbox" name="ruangans[]" value="{{ $ruangan }}" class="peer absolute opacity-0">
<div class="px-3 py-2 border border-gray-200 rounded-lg text-center transition-all duration-150 peer-checked:bg-blue-50 peer-checked:border-blue-300 peer-checked:text-blue-700 peer-checked:font-medium hover:bg-gray-50">
{{ $room }}
{{ $ruangan }}
</div>
</label>
@endforeach
@ -115,16 +121,6 @@
</div>
</div>
<!-- Manual Mic Duration -->
<div id="manual-fields" class="hidden mb-6 transition-all duration-300">
<label class="block text-sm font-medium text-gray-700 mb-2">Durasi (detik)</label>
<div class="flex items-center space-x-2">
<input type="range" name="duration" min="5" max="300" value="60" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<span id="duration-value" class="text-sm font-medium text-gray-700 w-12 text-center">60</span>
</div>
<p class="mt-1 text-xs text-gray-500">Durasi: 5-300 detik (5 menit)</p>
</div>
<!-- Submit Button -->
<div class="flex justify-end pt-4">
<button type="submit" id="submit-btn" class="px-5 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl text-sm font-medium text-white hover:from-blue-600 hover:to-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 shadow-sm transition-all duration-300 flex items-center justify-center transform hover:scale-105">
@ -141,19 +137,55 @@
<!-- Right Sidebar -->
<div class="space-y-6">
<!-- Audio Routing Control Card -->
<div id="stop-manual-section" class="hidden bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden h-full flex flex-col">
<!-- System Status Card -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="bg-gradient-to-r from-green-500 to-green-600 px-6 py-4">
<h2 class="text-xl font-semibold text-white flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Status Sistem
</h2>
</div>
<div class="p-6 space-y-4">
<!-- Active Announcement Indicator -->
<div id="active-indicator" class="flex items-start">
<div class="flex-shrink-0 mt-1">
<div class="h-3 w-3 rounded-full bg-gray-300"></div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-700">Tidak ada pengumuman aktif</p>
<p class="text-xs text-gray-500">Siap menerima pengumuman baru</p>
</div>
</div>
<!-- Active Rooms Indicator -->
<div id="active-rooms-indicator" class="hidden">
<div class="flex items-center justify-between mb-2">
<p class="text-sm font-medium text-gray-700">Ruangan Aktif</p>
<span class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">
<span id="active-count">0</span> aktif
</span>
</div>
<div id="active-rooms-badges" class="flex flex-wrap gap-2">
<!-- Badge ruangan akan muncul di sini -->
</div>
</div>
</div>
</div>
<!-- Manual Control Card -->
<div id="stop-manual-section" class="hidden bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="bg-gradient-to-r from-amber-500 to-orange-500 px-6 py-4">
<h2 class="text-xl font-semibold text-white flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" clip-rule="evenodd" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Kontrol Routing Audio
Kontrol Ruangan
</h2>
</div>
<div class="p-6 flex-grow">
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 rounded-lg mb-4 animate-pulse">
<div class="p-6">
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 rounded-lg mb-4">
<div class="flex">
<div class="flex-shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-amber-500" viewBox="0 0 20 20" fill="currentColor">
@ -162,62 +194,19 @@
</div>
<div class="ml-3">
<p class="text-sm text-amber-700">
<span class="font-medium">Perhatian!</span> Mikrofon aktif sedang di-routing ke speaker ruangan.
<span class="font-medium">Perhatian!</span> Relay audio sedang aktif di ruangan terpilih.
</p>
</div>
</div>
</div>
<div class="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg mb-4">
<div class="flex-shrink-0 p-2 bg-blue-100 rounded-lg text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
</svg>
</div>
<div>
<p class="text-sm font-medium text-gray-800">Status Mikrofon</p>
<p class="text-sm text-gray-500">Sedang aktif dan terhubung</p>
</div>
</div>
<div id="timer-display" class="hidden text-center mb-4">
<div class="inline-flex items-center justify-center">
<div class="relative">
<svg class="w-16 h-16" viewBox="0 0 36 36">
<circle cx="18" cy="18" r="16" fill="none" class="stroke-gray-200" stroke-width="2"></circle>
<circle cx="18" cy="18" r="16" fill="none" class="stroke-blue-500" stroke-width="2" stroke-dasharray="100" stroke-dashoffset="0" id="countdown-circle"></circle>
</svg>
<span id="countdown" class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-lg font-bold text-gray-800">60</span>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">Sisa waktu pengumuman</p>
</div>
</div>
<div class="p-6 pt-0">
<button id="stop-routing-btn" class="w-full px-4 py-3 bg-gradient-to-r from-amber-500 to-orange-500 rounded-xl text-sm font-medium text-white hover:from-amber-600 hover:to-orange-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-amber-500 shadow-sm transition-all duration-300 flex items-center justify-center transform hover:scale-105">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" clip-rule="evenodd" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Putuskan Routing Audio
Matikan Semua Ruangan
</button>
</div>
</div>
<!-- Active Announcements Card -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden transition-all duration-300 hover:shadow-md">
<div class="bg-gradient-to-r from-green-500 to-green-600 px-6 py-4">
<h2 class="text-xl font-semibold text-white flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Pengumuman Aktif
</h2>
</div>
<div class="p-6">
<div id="active-announcements-container">
<p class="text-sm text-gray-500">Memuat pengumuman aktif...</p>
</div>
</div>
</div>
</div>
</div>
</div>
@ -231,27 +220,23 @@
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
#countdown-circle {
transition: stroke-dashoffset 1s linear;
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
$(document).ready(function() {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
// Base URL for API endpoints
const baseUrl = '/admin/pengumuman';
// Initialize UI state
function initUI() {
// Set initial state for manual fields
$('input[name="duration"]').val(60);
$('#duration-value').text(60);
// Check MQTT status immediately
updateMqttStatus();
@ -260,6 +245,9 @@ function initUI() {
// Trigger change event to set initial view
$('input[name="type"]').trigger('change');
// Load active announcements
loadActiveAnnouncements();
}
// MQTT Status Indicator
@ -293,11 +281,6 @@ function updateMqttStatus() {
});
}
// Update duration value display
$('input[name="duration"]').on('input', function() {
$('#duration-value').text($(this).val());
});
// Character counter
$('textarea[name="content"]').on('input', function() {
const count = $(this).val().length;
@ -309,13 +292,17 @@ function updateMqttStatus() {
$('input[name="type"]').change(function() {
const isManual = $(this).val() === 'manual';
$('#tts-fields').toggleClass('hidden', isManual);
$('#manual-fields').toggleClass('hidden', !isManual);
$('#stop-manual-section').toggleClass('hidden', !isManual);
$('#submit-btn').html(`
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
${isManual ? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />' : '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />'}
</svg>
${isManual ? 'Hidupkan Ruangan' : 'Kirim Pengumuman'}
`);
});
// Select all rooms
$('#select-all').click(function() {
const checkboxes = $('input[name="rooms[]"]');
const checkboxes = $('input[name="ruangans[]"]');
const allChecked = checkboxes.length === checkboxes.filter(':checked').length;
checkboxes.prop('checked', !allChecked).trigger('change');
$(this).text(allChecked ? 'Pilih Semua' : 'Batalkan Semua');
@ -327,48 +314,32 @@ function updateMqttStatus() {
const formData = $(this).serializeArray();
const isManual = $('input[name="type"]:checked').val() === 'manual';
const duration = isManual ? parseInt($('input[name="duration"]').val()) : 0;
const selectedRooms = $('input[name="rooms[]"]:checked').map(function() {
return $(this).closest('label').text().trim();
const selectedRuangans = $('input[name="ruangans[]"]:checked').map(function() {
return $(this).val();
}).get();
if (selectedRooms.length === 0) {
Swal.fire({
icon: 'error',
title: 'Peringatan',
text: 'Silakan pilih minimal satu ruangan',
confirmButtonColor: '#3b82f6'
});
if (selectedRuangans.length === 0) {
showAlert('error', 'Peringatan', 'Silakan pilih minimal satu ruangan');
return;
}
Swal.fire({
title: 'Konfirmasi Pengiriman',
html: `<div class="text-left">
<p>Anda akan mengirim pengumuman <strong>${isManual ? 'manual' : 'TTS'}</strong> ke:</p>
<ul class="list-disc pl-5 mt-2 mb-2 max-h-40 overflow-y-auto">
${selectedRooms.map(room => `<li>${room}</li>`).join('')}
</ul>
${isManual ? `<p>Durasi: <strong>${duration} detik</strong></p>` : ''}
</div>`,
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Kirim Sekarang',
cancelButtonText: 'Batal',
reverseButtons: true,
customClass: {
confirmButton: 'px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md',
cancelButton: 'px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded-md'
}
}).then((result) => {
if (result.isConfirmed) {
const confirmMessage = isManual
? `Anda akan menghidupkan relay audio di ${selectedRuangans.length} ruangan`
: `Anda akan mengirim pengumuman TTS ke ${selectedRuangans.length} ruangan`;
showConfirmation(
'Konfirmasi',
confirmMessage,
'',
function() {
const submitBtn = $('#submit-btn');
const buttonText = isManual ? 'Menghidupkan...' : 'Mengirim...';
submitBtn.prop('disabled', true).html(`
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Mengirim...
${buttonText}
`);
$.ajax({
@ -376,58 +347,37 @@ function updateMqttStatus() {
method: 'POST',
data: formData,
success: function(response) {
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: response.message || 'Pengumuman berhasil dikirim',
timer: 2000,
showConfirmButton: false
});
showAlert('success', 'Berhasil!', response.message);
if (isManual) {
startCountdown(duration);
$('#stop-manual-section').removeClass('hidden');
updateActiveRooms(selectedRuangans);
}
// Refresh active announcements
loadActiveAnnouncements();
},
error: function(xhr) {
const errorMsg = xhr.responseJSON?.message || 'Terjadi kesalahan saat mengirim pengumuman';
Swal.fire({
icon: 'error',
title: 'Gagal!',
text: errorMsg,
timer: 2000,
showConfirmButton: false
});
},
error: handleAjaxError,
complete: function() {
submitBtn.prop('disabled', false).html(`
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
${isManual ? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />' : '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />'}
</svg>
Kirim Pengumuman
${isManual ? 'Hidupkan Ruangan' : 'Kirim Pengumuman'}
`);
}
});
}
});
);
});
// Stop routing button
$('#stop-routing-btn').click(function(e) {
e.preventDefault();
Swal.fire({
title: 'Konfirmasi',
text: 'Anda yakin ingin memutus routing audio?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya, Putuskan',
cancelButtonText: 'Batal',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
showConfirmation(
'Konfirmasi',
'Anda yakin ingin mematikan semua relay ruangan?',
'',
function() {
const stopBtn = $('#stop-routing-btn');
stopBtn.prop('disabled', true).html(`
<svg class="animate-spin -ml-1 mr-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
@ -436,77 +386,28 @@ function updateMqttStatus() {
</svg>
Memproses...
`);
$.post(`${baseUrl}/stop-manual`, function(response) {
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: response.message || 'Routing audio berhasil diputus',
timer: 2000,
showConfirmButton: false
});
$('#timer-display').hide();
// Refresh active announcements
showAlert('success', 'Berhasil!', response.message);
$('#stop-manual-section').addClass('hidden');
loadActiveAnnouncements();
}).fail(function(xhr) {
const errorMsg = xhr.responseJSON?.message || 'Gagal memutus routing audio';
Swal.fire({
icon: 'error',
title: 'Gagal!',
text: errorMsg,
timer: 2000,
showConfirmButton: false
});
}).always(function() {
}).fail(handleAjaxError).always(function() {
stopBtn.prop('disabled', false).html(`
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" clip-rule="evenodd" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Putuskan Routing Audio
Matikan Semua Ruangan
`);
});
}
});
);
});
// Countdown timer with circular progress
function startCountdown(seconds) {
$('#timer-display').show();
let counter = seconds;
const circle = $('#countdown-circle');
const circumference = 2 * Math.PI * 16;
const countdownElement = $('#countdown');
// Initialize circle
circle.css('stroke-dasharray', circumference);
circle.css('stroke-dashoffset', 0);
const interval = setInterval(() => {
countdownElement.text(counter);
// Update progress circle
const offset = circumference - (counter / seconds) * circumference;
circle.css('stroke-dashoffset', offset);
counter--;
if (counter < 0) {
clearInterval(interval);
$('#timer-display').hide();
}
}, 1000);
}
// Check if there's an active manual announcement
function checkActiveAnnouncement() {
$.get(`${baseUrl}/check-active`, function(response) {
if (response.active && response.type === 'manual') {
$('input[name="type"][value="manual"]').prop('checked', true).trigger('change');
$('input[name="duration"]').val(response.duration);
$('#duration-value').text(response.duration);
startCountdown(response.remaining);
$('#stop-manual-section').removeClass('hidden');
}
});
}
@ -514,92 +415,129 @@ function checkActiveAnnouncement() {
// Load active announcements
function loadActiveAnnouncements() {
$.get(`${baseUrl}/active-announcements`, function(response) {
const container = $('#active-announcements-container');
if (response.length === 0) {
container.html('<p class="text-sm text-gray-500">Tidak ada pengumuman aktif</p>');
return;
}
let html = '<div class="space-y-3">';
response.forEach(announcement => {
html += `
<div class="p-3 border border-gray-200 rounded-lg">
<div class="flex justify-between items-start">
<div>
<h3 class="font-medium text-gray-800">${announcement.room}</h3>
<p class="text-sm text-gray-500">${announcement.type === 'tts' ? 'TTS' : 'Manual'}</p>
</div>
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Active
</span>
</div>
<div class="mt-2 text-sm text-gray-500">
<p>Mulai: ${new Date(announcement.sent_at).toLocaleString()}</p>
${announcement.type === 'manual' ? `<p>Durasi: ${announcement.duration} detik</p>` : ''}
</div>
<div class="mt-2">
<button class="text-red-600 hover:text-red-900 text-sm font-medium stop-announcement" data-id="${announcement.id}">
Hentikan
</button>
</div>
</div>
`;
});
html += '</div>';
container.html(html);
updateActiveStatus(response);
});
}
// Stop announcement button handler
$(document).on('click', '.stop-announcement', function() {
const announcementId = $(this).data('id');
// Update active status UI
function updateActiveStatus(activeAnnouncements) {
const activeIndicator = $('#active-indicator');
const activeRoomsIndicator = $('#active-rooms-indicator');
const activeRoomsBadges = $('#active-rooms-badges');
if (activeAnnouncements.length === 0) {
activeIndicator.html(`
<div class="flex-shrink-0 mt-1">
<div class="h-3 w-3 rounded-full bg-gray-300"></div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-700">Tidak ada pengumuman aktif</p>
<p class="text-xs text-gray-500">Siap menerima pengumuman baru</p>
</div>
`);
activeRoomsIndicator.addClass('hidden');
return;
}
// Count unique active rooms
const allRooms = [];
activeAnnouncements.forEach(announcement => {
allRooms.push(...announcement.target_ruangans);
});
const uniqueRooms = [...new Set(allRooms)];
// Update main indicator
activeIndicator.html(`
<div class="flex-shrink-0 mt-1">
<div class="h-3 w-3 rounded-full bg-green-500 animate-pulse"></div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-700">${activeAnnouncements.length} pengumuman aktif</p>
<p class="text-xs text-gray-500">${uniqueRooms.length} ruangan terpengaruh</p>
</div>
`);
// Update active rooms badges
activeRoomsIndicator.removeClass('hidden');
$('#active-count').text(uniqueRooms.length);
let badgesHtml = '';
uniqueRooms.forEach(room => {
badgesHtml += `
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
${room}
</span>
`;
});
activeRoomsBadges.html(badgesHtml);
}
// Update active rooms display
function updateActiveRooms(ruangans) {
const activeRoomsIndicator = $('#active-rooms-indicator');
const activeRoomsBadges = $('#active-rooms-badges');
activeRoomsIndicator.removeClass('hidden');
$('#active-count').text(ruangans.length);
let badgesHtml = '';
ruangans.forEach(ruangan => {
badgesHtml += `
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
${ruangan}
</span>
`;
});
activeRoomsBadges.html(badgesHtml);
}
// Helper function to show alert
function showAlert(icon, title, text) {
Swal.fire({
title: 'Konfirmasi',
text: 'Anda yakin ingin menghentikan pengumuman ini?',
icon: 'warning',
icon: icon,
title: title,
text: text,
timer: 2000,
showConfirmButton: false
});
}
// Helper function to show confirmation dialog
function showConfirmation(title, text, html, confirmCallback) {
Swal.fire({
title: title,
text: text,
html: html,
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Hentikan',
confirmButtonText: 'Ya, Lanjutkan',
cancelButtonText: 'Batal',
reverseButtons: true
reverseButtons: true,
customClass: {
confirmButton: 'px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md',
cancelButton: 'px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded-md'
}
}).then((result) => {
if (result.isConfirmed) {
$.post(`${baseUrl}/stop-announcement`, {
id: announcementId,
_token: $('meta[name="csrf-token"]').attr('content')
}, function(response) {
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: response.message || 'Pengumuman berhasil dihentikan',
timer: 2000,
showConfirmButton: false
});
// Refresh active announcements
loadActiveAnnouncements();
}).fail(function(xhr) {
const errorMsg = xhr.responseJSON?.message || 'Gagal menghentikan pengumuman';
Swal.fire({
icon: 'error',
title: 'Gagal!',
text: errorMsg,
timer: 2000,
showConfirmButton: false
});
});
confirmCallback();
}
});
});
}
// Helper function to handle AJAX errors
function handleAjaxError(xhr) {
const errorMsg = xhr.responseJSON?.message || 'Terjadi kesalahan saat memproses permintaan';
showAlert('error', 'Gagal!', errorMsg);
}
// Initialize the UI
initUI();
// Load initial data
loadActiveAnnouncements();
// Polling for updates every 30 seconds
setInterval(function() {
updateMqttStatus();
loadActiveAnnouncements();
}, 30000);
});
</script>
@endsection

View File

@ -1,60 +0,0 @@
@extends('layouts.dashboard')
@section('content')
<div class="container mx-auto p-6">
<!-- Judul Halaman -->
<h1 class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-red-600 to-yellow-600 mb-8 text-center">
Hentikan Mic Manual
</h1>
<!-- Card Container -->
<div class="bg-white shadow-xl rounded-lg overflow-hidden max-w-md mx-auto">
<div class="p-6">
<!-- Deskripsi -->
<p class="text-gray-700 text-center mb-6">
Tekan tombol di bawah ini untuk menghentikan mic manual di semua ruangan.
</p>
<!-- Tombol Aksi -->
<form id="stop-manual-form" action="{{ route('announcements.stopManual') }}" method="POST" class="flex justify-center">
@csrf
<button type="submit" id="submit-btn"
class="relative inline-flex items-center justify-center px-8 py-3 font-medium text-white rounded-lg transition duration-300 ease-in-out hover:scale-105 shadow-md bg-gradient-to-r from-red-600 to-yellow-600">
<svg id="spinner" class="hidden animate-spin mr-2 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
</svg>
Hentikan Mic Manual
</button>
</form>
</div>
</div>
</div>
<!-- Script -->
<script>
const form = document.getElementById('stop-manual-form');
const submitBtn = document.getElementById('submit-btn');
const spinner = document.getElementById('spinner');
// Handle form submission
form.addEventListener('submit', function () {
// Tampilkan loading SweetAlert
Swal.fire({
title: 'Menghentikan Mic Manual...',
text: 'Mohon tunggu sebentar.',
allowOutsideClick: false,
showConfirmButton: false,
willOpen: () => Swal.showLoading()
});
// Tampilkan spinner dan disable tombol
spinner.classList.remove('hidden');
submitBtn.setAttribute('disabled', true);
});
</script>
@endsection

View File

@ -684,18 +684,18 @@ function getLiveStatus() {
Swal.close();
if (data.success) {
updateDeviceStatus(data.data);
Toast.fire({
icon: 'success',
title: 'Status perangkat diperbarui'
});
// Toast.fire({
// icon: 'success',
// title: 'Status perangkat diperbarui'
// });
}
})
.catch(error => {
Swal.fire({
icon: 'error',
title: 'Gagal memperbarui status',
text: error.message
});
// Swal.fire({
// icon: 'error',
// title: 'Gagal memperbarui status',
// text: error.message
// });
});
}
@ -844,6 +844,7 @@ function updateNextSchedule() {
// Initialize when page loads
document.addEventListener('DOMContentLoaded', function() {
updateNextSchedule();
getLiveStatus();
// Refresh every minute to stay accurate
setInterval(updateNextSchedule, 60000);

View File

@ -7,6 +7,7 @@
<!-- Add jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body class="bg-gray-100">

View File

@ -12,7 +12,6 @@
use App\Http\Controllers\PresensiController;
use App\Http\Controllers\SiswaController;
use App\Http\Controllers\AnnouncementController;
use App\Http\Controllers\MqttController;
use App\Http\Controllers\RuanganController;
// Public routes
@ -78,9 +77,10 @@
Route::prefix('pengumuman')->group(function () {
Route::get('/', [AnnouncementController::class, 'index'])->name('announcements.index');
Route::post('/send', [AnnouncementController::class, 'send'])->name('announcements.send');
Route::post('/stop-manual', [AnnouncementController::class, 'stopManual'])->name('announcements.stopManual');
Route::post('/stop-manual', [AnnouncementController::class, 'stopManual'])->name('announcements.stop-manual');
Route::get('/active', [AnnouncementController::class, 'checkActive'])->name('announcements.active');
Route::get('/mqtt-status', [AnnouncementController::class, 'mqttStatus'])->name('announcements.mqttStatus');
Route::get('/history', [AnnouncementController::class, 'history'])->name('announcements.history');
});
});
});