From 5d76037f9411ece66313f0d4652b052d332f46a3 Mon Sep 17 00:00:00 2001 From: rendygaafk Date: Sat, 26 Apr 2025 03:27:15 +0700 Subject: [PATCH] Pengumuman Controller,model,migrate,index --- .../Controllers/AnnouncementController.php | 615 +++++++--------- app/Models/Announcement.php | 81 +- app/Models/Ruangan.php | 9 + ... 2025_03_20_091252_create_cache_table.php} | 0 .../2025_04_20_135559_create_rooms_table.php | 21 - ...4_20_135609_create_announcements_table.php | 28 - ...025_04_21_051528_create_ruangan_table.php} | 0 ...4_22_135609_create_announcements_table.php | 43 ++ .../admin/announcement/history.blade.php | 117 +++ .../views/admin/announcement/index.blade.php | 692 ++++++++++++++++++ .../views/admin/announcement/show.blade.php | 80 ++ .../admin/announcements/history.blade.php | 395 ---------- .../views/admin/announcements/index.blade.php | 543 -------------- .../views/admin/component/sidebar.blade.php | 2 +- routes/web.php | 14 +- 15 files changed, 1301 insertions(+), 1339 deletions(-) rename database/migrations/{2025_03_21_091252_create_cache_table.php => 2025_03_20_091252_create_cache_table.php} (100%) delete mode 100644 database/migrations/2025_04_20_135559_create_rooms_table.php delete mode 100644 database/migrations/2025_04_20_135609_create_announcements_table.php rename database/migrations/{2025_04_23_051528_create_ruangan_table.php => 2025_04_21_051528_create_ruangan_table.php} (100%) create mode 100644 database/migrations/2025_04_22_135609_create_announcements_table.php create mode 100644 resources/views/admin/announcement/history.blade.php create mode 100644 resources/views/admin/announcement/index.blade.php create mode 100644 resources/views/admin/announcement/show.blade.php delete mode 100644 resources/views/admin/announcements/history.blade.php delete mode 100644 resources/views/admin/announcements/index.blade.php diff --git a/app/Http/Controllers/AnnouncementController.php b/app/Http/Controllers/AnnouncementController.php index 83a8765..c677d6f 100644 --- a/app/Http/Controllers/AnnouncementController.php +++ b/app/Http/Controllers/AnnouncementController.php @@ -2,386 +2,339 @@ namespace App\Http\Controllers; +use App\Models\Announcement; use App\Models\Ruangan; -use App\Services\MqttService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Storage; +use PhpMqtt\Client\Facades\MQTT; use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Cache; -use App\Models\Announcement; use Illuminate\Support\Facades\Validator; +use Illuminate\Support\Facades\Storage; class AnnouncementController extends Controller { - protected $mqttService; + // Mode constants + const MODE_REGULER = 'reguler'; + const MODE_TTS = 'tts'; - public function __construct(MqttService $mqttService) - { - $this->mqttService = $mqttService; - } + // 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; // -10 to 10 + const TTS_DEFAULT_FORMAT = 'wav'; /** - * Display the announcement interface + * Show the announcement form */ public function index() { - $ruangans = Ruangan::pluck('nama_ruangan')->toArray(); - $activeAnnouncements = Announcement::where('is_active', true)->get(); - - if (empty($ruangans)) { - $ruangans = ['ruang1', 'ruang2', 'ruang3']; - } - - return view('admin.announcements.index', compact('ruangans', 'activeAnnouncements')); - } - - /** - * Display announcement history page - */ - public function history() - { - return view('admin.announcements.history'); - } - - /** - * Get MQTT connection status - */ - public function mqttStatus() - { - return response()->json([ - 'connected' => $this->mqttService->isConnected(), - 'status' => Cache::get('mqtt_status', 'disconnected') - ]); - } - - /** - * Check for active announcements - */ - public function checkActive() - { - $active = Cache::get('active_announcement', false); + $ruangan = Ruangan::with(['kelas', 'jurusan'])->get(); + $announcements = Announcement::with(['user', 'ruangans']) + ->latest() + ->paginate(10); - return response()->json([ - 'active' => $active, - 'type' => $active ? Cache::get('active_announcement_type') : null, - 'remaining' => $active ? (Cache::get('active_announcement_end') - time()) : null + return view('admin.announcement.index', [ + 'ruangan' => $ruangan, + 'announcements' => $announcements, + 'modes' => [self::MODE_REGULER, self::MODE_TTS] ]); } /** - * Get active announcements + * Handle the announcement request */ - public function activeAnnouncements() + public function store(Request $request) { - $announcements = Announcement::with('ruangans') - ->where('is_active', true) - ->orderBy('sent_at', 'desc') - ->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 - ]; - }); + $validator = $this->validateRequest($request); - return response()->json($announcements); - } - - /** - * Get announcement history (for API) - */ - public function getHistory(Request $request) - { - $perPage = 10; - $query = Announcement::where('is_active', false) - ->orderBy('sent_at', 'desc'); - - // Filter by type - if ($request->filter && in_array($request->filter, ['tts', 'manual'])) { - $query->where('type', $request->filter); + if ($validator->fails()) { + return redirect()->back() + ->withErrors($validator) + ->withInput(); } - // Filter by status - if ($request->status && in_array($request->status, ['completed', 'stopped'])) { - $query->where('status', $request->status); - } + $mode = $request->input('mode'); - // 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() - ]); - } - - /** - * Get announcement details - */ - public function announcementDetails($id) - { - $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 - ]); - } - - /** - * Send announcement to MQTT - */ - public function send(Request $request) - { - // Validasi input - $validated = $request->validate([ - 'type' => 'required|in:tts,manual', - 'content' => 'nullable|required_if:type,tts|string|max:500', - '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 { - // Persiapkan data awal - $announcementData = [ - 'type' => $validated['type'], - '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'; + if ($mode === self::MODE_REGULER) { + $this->handleRegularAnnouncement($request); + $message = 'Pengumuman reguler berhasil dikirim ke ruangan terpilih.'; + } elseif ($mode === self::MODE_TTS) { + $this->handleTTSAnnouncement($request); + $message = 'Pengumuman TTS berhasil diproses dan dikirim.'; } - - // Buat pengumuman - $announcement = Announcement::create($announcementData); - - // Siapkan payload MQTT - $payload = [ - 'type' => $validated['type'], - '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'] = $audioUrl; // Gunakan variabel yang sudah digenerate - } - - // Kirim via MQTT - $this->mqttService->sendAnnouncement($payload); - - return response()->json([ - 'success' => true, - 'message' => 'Pengumuman berhasil dikirim!', - 'audio_url' => $audioUrl ?? null // Sertakan audio_url dalam response - ]); - + + return redirect()->route('admin.announcement.index')->with('success', $message); } catch (\Exception $e) { Log::error('Announcement error: ' . $e->getMessage()); - - // Hapus record jika gagal setelah create - if (isset($announcement)) { - $announcement->delete(); - } + return redirect()->back() + ->with('error', 'Terjadi kesalahan saat memproses pengumuman: ' . $e->getMessage()) + ->withInput(); + } + } + - return response()->json([ - 'success' => false, - 'message' => 'Gagal mengirim pengumuman: ' . $e->getMessage(), - ], 500); + /** + * Validate the announcement request + */ + private function validateRequest(Request $request) + { + $rules = [ + 'mode' => 'required|in:reguler,tts', + ]; + + if ($request->input('mode') === self::MODE_REGULER) { + $rules['ruangan'] = 'required|array'; + $rules['ruangan.*'] = 'exists:ruangan,id'; + // Remove message requirement for regular mode + } elseif ($request->input('mode') === self::MODE_TTS) { + $rules['tts_text'] = 'required|string|max:1000'; + $rules['tts_voice'] = 'nullable|string'; + $rules['tts_speed'] = 'nullable|integer|min:-10|max:10'; + $rules['tts_ruangan'] = 'required|array'; + $rules['tts_ruangan.*'] = 'exists:ruangan,id'; } + + return Validator::make($request->all(), $rules); } /** - * Stop manual audio routing + * Handle regular announcement */ - public function stopManual() + private function handleRegularAnnouncement(Request $request) { - try { - Announcement::where('is_active', true) - ->where('type', 'manual') - ->update([ - 'is_active' => false, - 'status' => 'completed' - ]); - - $payload = [ - 'type' => 'manual', - 'action' => 'deactivate_relay', - 'timestamp' => now()->toDateTimeString(), - ]; - - $this->mqttService->publish('bel/sekolah/pengumuman', json_encode($payload), 1, false); - - // Clear active announcement cache - Cache::forget('active_announcement'); - Cache::forget('active_announcement_type'); - - return response()->json([ - 'success' => true, - 'message' => 'Relay ruangan berhasil dimatikan!', - ]); - } catch (\Exception $e) { - Log::error('Stop manual error: ' . $e->getMessage()); - return response()->json([ - 'success' => false, - 'message' => 'Gagal mematikan relay: ' . $e->getMessage(), - ], 500); - } - } - - /** - * Stop specific announcement - */ - public function stopAnnouncement(Request $request) - { - try { - $announcement = Announcement::findOrFail($request->id); - $announcement->update([ - 'is_active' => false, - 'status' => 'stopped' - ]); - - if ($announcement->type === 'manual') { - $payload = [ - 'type' => 'manual', - 'action' => 'deactivate_relay', - 'announcement_id' => $announcement->id, - 'timestamp' => now()->toDateTimeString(), - ]; - $this->mqttService->publish('bel/sekolah/pengumuman', json_encode($payload), 1, false); - } - - return response()->json([ - 'success' => true, - 'message' => 'Pengumuman berhasil dihentikan!', - ]); - } catch (\Exception $e) { - Log::error('Stop announcement error: ' . $e->getMessage()); - return response()->json([ - 'success' => false, - 'message' => 'Gagal menghentikan pengumuman: ' . $e->getMessage(), - ], 500); - } - } - - /** - * Generate TTS audio using VoiceRSS - */ - private function generateTtsAudio($text) - { - $apiKey = config('services.voicerss.key'); + $ruanganIds = $request->input('ruangan'); - // Validate API key configuration - if (empty($apiKey)) { - Log::error('VoiceRSS API Key not configured'); - return $this->getTtsFallback($text); // Use fallback instead of throwing exception + $ruangan = Ruangan::whereIn('id', $ruanganIds)->get(); + + $payload = [ + 'type' => 'reguler', + 'action' => 'activate', // Add action type + 'ruangan' => $ruangan->pluck('nama_ruangan')->toArray(), + 'timestamp' => now()->toDateTimeString() + ]; + + $this->publishToMQTT($payload); + + // Simpan ke database tanpa message + $announcement = Announcement::create([ + 'mode' => self::MODE_REGULER, + 'message' => 'Aktivasi ruangan', // Default message or empty + 'ruangan' => $ruangan->pluck('nama_ruangan')->toArray(), + 'user_id' => auth()->id, + 'sent_at' => now() + ]); + + // Attach ruangan ke pengumuman + $announcement->ruangans()->attach($ruanganIds); + + Log::info('Regular announcement sent', [ + 'announcement_id' => $announcement->id, + 'ruangan' => $ruanganIds + ]); + } + + private function handleTTSAnnouncement(Request $request) + { + $text = $request->input('tts_text'); + $ruanganIds = $request->input('tts_ruangan'); + $voice = $request->input('tts_voice', self::TTS_DEFAULT_VOICE); + $speed = $request->input('tts_speed', self::TTS_DEFAULT_SPEED); + + $audioContent = $this->generateTTS($text, $voice, $speed); + + if (!$audioContent) { + throw new \Exception('Gagal menghasilkan audio TTS'); } + + // Simpan file audio + $fileName = 'tts/' . now()->format('YmdHis') . '.wav'; + Storage::disk('public')->put($fileName, $audioContent); + + $ruangan = Ruangan::whereIn('id', $ruanganIds)->get(); + + $payload = [ + 'type' => 'tts', + 'audio_url' => asset('storage/' . $fileName), + 'ruangan' => $ruangan->pluck('nama_ruangan')->toArray(), + 'timestamp' => now()->toDateTimeString() + ]; + + $this->publishToMQTT($payload); + + // Simpan ke database + $announcement = Announcement::create([ + 'mode' => self::MODE_TTS, + 'message' => $text, + 'audio_path' => $fileName, + 'voice' => $voice, + 'speed' => $speed, + 'ruangan' => $ruangan->pluck('nama_ruangan')->toArray(), + 'user_id' => auth()->id, + 'sent_at' => now() + ]); + + // Attach ruangan ke pengumuman + $announcement->ruangans()->attach($ruanganIds); + + Log::info('TTS announcement sent', [ + 'announcement_id' => $announcement->id, // Diubah dari id() ke id + 'ruangan' => $ruanganIds, + 'text' => $text, + 'voice' => $voice, + 'speed' => $speed + ]); + } + /** + * Generate TTS audio using VoiceRSS API + */ + private function generateTTS($text, $voice, $speed) + { 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'); - - $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()); - - // 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() + $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' // Lower quality for faster transmission ]); - return $this->getTtsFallback($text); + + if ($response->successful()) { + return $response->body(); + } + + Log::error('TTS API Error: ' . $response->body()); + return null; + } catch (\Exception $e) { + Log::error('TTS Generation Error: ' . $e->getMessage()); + return null; } } - private function getTtsFallback($text) + /** + * Publish message to MQTT broker + */ + private function publishToMQTT(array $payload) { 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}"); + $mqtt = MQTT::connection(); + $mqtt->publish('announcement/channel', json_encode($payload), 0); + $mqtt->disconnect(); + } catch (\Exception $e) { + Log::error('MQTT Publish Error: ' . $e->getMessage()); + throw new \Exception('Gagal mengirim pesan ke MQTT broker'); + } + } + + /** + * Get available TTS voices + */ + public function getTTSVoices() + { + return [ + 'id-id' => 'Indonesian', + 'en-us' => 'English (US)', + 'en-gb' => 'English (UK)', + 'ja-jp' => 'Japanese', + 'es-es' => 'Spanish', + 'fr-fr' => 'French', + 'de-de' => 'German' + ]; + } + + /** + * Show announcement history + */ + public function history(Request $request) + { + $search = $request->input('search'); + $mode = $request->input('mode'); + + $announcements = Announcement::with(['user', '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}%"); + }) + ->orWhereHas('user', function($q) use ($search) { + $q->where('name', 'like', "%{$search}%"); + }); + }) + ->when($mode, function($query) use ($mode) { + return $query->where('mode', $mode); + }) + ->latest() + ->paginate(10); + + return view('announcement.history', [ + 'announcements' => $announcements, + 'search' => $search, + 'mode' => $mode, + 'modes' => [self::MODE_REGULER, self::MODE_TTS] + ]); + } + + /** + * Show announcement detail + */ + public function show(Announcement $announcement) + { + return view('announcement.show', [ + 'announcement' => $announcement->load(['user', 'ruangans']) + ]); + } + + /** + * Delete an announcement + */ + public function destroy(Announcement $announcement) + { + try { + // Hapus file audio jika ada + if ($announcement->audio_path && Storage::disk('public')->exists($announcement->audio_path)) { + Storage::disk('public')->delete($announcement->audio_path); } - return Storage::url($filename); + $announcement->delete(); + + return redirect()->route('announcement.history') + ->with('success', 'Pengumuman berhasil dihapus'); } catch (\Exception $e) { - Log::error('Fallback TTS failed', ['error' => $e->getMessage()]); - return null; // Return null if both main and fallback fail + Log::error('Error deleting announcement: ' . $e->getMessage()); + return redirect()->back() + ->with('error', 'Gagal menghapus pengumuman: ' . $e->getMessage()); } } + + /** + * TTS Preview endpoint + */ + public function ttsPreview(Request $request) + { + $validated = $request->validate([ + 'text' => 'required|string|max:1000', + 'voice' => 'nullable|string', + 'speed' => 'nullable|integer|min:-10|max:10' + ]); + + $audioContent = $this->generateTTS( + $validated['text'], + $validated['voice'] ?? self::TTS_DEFAULT_VOICE, + $validated['speed'] ?? self::TTS_DEFAULT_SPEED + ); + + if (!$audioContent) { + return response()->json(['message' => 'Gagal menghasilkan audio'], 500); + } + + $fileName = 'tts/previews/' . uniqid() . '.wav'; + Storage::disk('public')->put($fileName, $audioContent); + + return response()->json([ + 'audio_url' => asset('storage/' . $fileName) + ]); + } } \ No newline at end of file diff --git a/app/Models/Announcement.php b/app/Models/Announcement.php index 70c4099..db4b09d 100644 --- a/app/Models/Announcement.php +++ b/app/Models/Announcement.php @@ -9,28 +9,83 @@ class Announcement extends Model { use HasFactory; + protected $table = 'announcements'; + protected $fillable = [ - 'type', - 'content', - 'target_ruangans', // Menggunakan 'ruangan' bukan 'target_ruangans' - 'duration', - 'sent_at', - 'is_active', - 'status', - 'audio_url' + 'mode', + 'message', + 'audio_path', + 'voice', + 'speed', + 'ruangan', + 'user_id', + 'sent_at' ]; protected $casts = [ - 'target_ruangans' => 'array', - 'sent_at' => 'datetime', - 'is_active' => 'boolean', + 'ruangan' => 'array', + 'sent_at' => 'datetime' ]; /** - * Relasi ke model Ruangan + * Relasi ke user yang membuat pengumuman + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Relasi many-to-many ke ruangan */ public function ruangans() { - return Ruangan::whereIn('nama', $this->ruangan)->get(); + return $this->belongsToMany(Ruangan::class, 'announcement_ruangan'); + } + + /** + * Scope untuk pengumuman reguler + */ + public function scopeReguler($query) + { + return $query->where('mode', 'reguler'); + } + + /** + * Scope untuk pengumuman TTS + */ + public function scopeTts($query) + { + return $query->where('mode', 'tts'); + } + + /** + * Scope untuk pencarian + */ + public function scopeSearch($query, $search) + { + return $query->where('message', 'like', "%{$search}%") + ->orWhereHas('ruangans', function($q) use ($search) { + $q->where('nama_ruangan', 'like', "%{$search}%"); + }) + ->orWhereHas('user', function($q) use ($search) { + $q->where('name', 'like', "%{$search}%"); + }); + } + + /** + * Accessor untuk audio URL + */ + public function getAudioUrlAttribute() + { + return $this->audio_path ? asset('storage/' . $this->audio_path) : null; + } + + /** + * Format tanggal pengiriman + */ + public function getFormattedSentAtAttribute() + { + return $this->sent_at->format('d M Y H:i:s'); } } \ No newline at end of file diff --git a/app/Models/Ruangan.php b/app/Models/Ruangan.php index 47f9616..14224e2 100644 --- a/app/Models/Ruangan.php +++ b/app/Models/Ruangan.php @@ -33,6 +33,15 @@ public function jurusan() return $this->belongsTo(Jurusan::class, 'id_jurusan'); } + // Di app/Models/Ruangan.php + + /** + * Relasi many-to-many ke announcements + */ + public function announcements() + { + return $this->belongsToMany(Announcement::class, 'announcement_ruangan'); + } /** * Accessor untuk nama ruangan diff --git a/database/migrations/2025_03_21_091252_create_cache_table.php b/database/migrations/2025_03_20_091252_create_cache_table.php similarity index 100% rename from database/migrations/2025_03_21_091252_create_cache_table.php rename to database/migrations/2025_03_20_091252_create_cache_table.php diff --git a/database/migrations/2025_04_20_135559_create_rooms_table.php b/database/migrations/2025_04_20_135559_create_rooms_table.php deleted file mode 100644 index d7ff82c..0000000 --- a/database/migrations/2025_04_20_135559_create_rooms_table.php +++ /dev/null @@ -1,21 +0,0 @@ -id(); - $table->string('name')->unique(); // Nama ruangan harus unik - $table->timestamps(); - }); - } - - public function down() - { - Schema::dropIfExists('rooms'); - } -} \ No newline at end of file diff --git a/database/migrations/2025_04_20_135609_create_announcements_table.php b/database/migrations/2025_04_20_135609_create_announcements_table.php deleted file mode 100644 index df256c4..0000000 --- a/database/migrations/2025_04_20_135609_create_announcements_table.php +++ /dev/null @@ -1,28 +0,0 @@ -id(); - $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(); - }); - } - - public function down() - { - Schema::dropIfExists('announcements'); - } -}; \ No newline at end of file diff --git a/database/migrations/2025_04_23_051528_create_ruangan_table.php b/database/migrations/2025_04_21_051528_create_ruangan_table.php similarity index 100% rename from database/migrations/2025_04_23_051528_create_ruangan_table.php rename to database/migrations/2025_04_21_051528_create_ruangan_table.php diff --git a/database/migrations/2025_04_22_135609_create_announcements_table.php b/database/migrations/2025_04_22_135609_create_announcements_table.php new file mode 100644 index 0000000..0e248fa --- /dev/null +++ b/database/migrations/2025_04_22_135609_create_announcements_table.php @@ -0,0 +1,43 @@ +id(); + $table->string('mode'); // 'reguler' atau 'tts' + $table->text('message'); + $table->string('audio_path')->nullable(); + $table->string('voice')->nullable(); + $table->integer('speed')->nullable(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->timestamp('sent_at')->useCurrent(); + $table->timestamps(); + + $table->index('mode'); + $table->index('sent_at'); + $table->index('user_id'); + }); + + // 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->timestamps(); + + $table->unique(['announcement_id', 'ruangan_id']); + }); + } + + public function down() + { + Schema::dropIfExists('announcement_ruangan'); + Schema::dropIfExists('announcements'); + } +}; \ No newline at end of file diff --git a/resources/views/admin/announcement/history.blade.php b/resources/views/admin/announcement/history.blade.php new file mode 100644 index 0000000..745cc23 --- /dev/null +++ b/resources/views/admin/announcement/history.blade.php @@ -0,0 +1,117 @@ +@extends('layouts.dashboard') + +@section('content') +
+
+
+

Riwayat Pengumuman

+
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + @forelse($announcements as $announcement) + + + + + + + + + + @empty + + + + @endforelse + +
#WaktuModeRuangan TujuanIsi PengumumanPengirimAksi
{{ $loop->iteration }}{{ $announcement->sent_at->format('d M Y H:i') }} + + {{ strtoupper($announcement->mode) }} + + + @foreach($announcement->ruangans as $ruangan) + {{ $ruangan->nama_ruangan }} + @endforeach + + @if($announcement->mode === 'tts') + + TTS: {{ Str::limit($announcement->message, 50) }} + @if($announcement->audio_path) + + + + @endif + @else + {{ Str::limit($announcement->message, 50) }} + @endif + {{ $announcement->user->name }} + + + +
+ @csrf + @method('DELETE') + +
+
Belum ada pengumuman
+
+
+ +
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/admin/announcement/index.blade.php b/resources/views/admin/announcement/index.blade.php new file mode 100644 index 0000000..27feeaf --- /dev/null +++ b/resources/views/admin/announcement/index.blade.php @@ -0,0 +1,692 @@ +@extends('layouts.dashboard') + +@section('title', 'Manajemen Pengumuman') + +@section('content') +
+ +
+
+

Sistem Pengumuman Digital

+

Kelola dan kirim pengumuman ke ruangan terpilih

+
+
+ + Riwayat + + +
+
+ + +
+ +
+

Formulir Pengumuman Baru

+
+ + +
+
+ @csrf + + +
+ +
+ + +
+
+ + +
+
+ +
+ +
+ 0/500 +
+
+
+
+ + + + + +
+
+ +
+ + +
+
+ +
+
+ @foreach($ruangan as $room) + + @endforeach +
+
+
+ + +
+ +
+
+
+
+ + +
+ +
+
+

Pengumuman Terakhir

+
+ Menampilkan {{ $announcements->count() }} dari {{ $announcements->total() }} pengumuman +
+
+
+ + +
+ @if($announcements->isEmpty()) +
+
+ +
+

Belum Ada Pengumuman

+

Mulailah dengan membuat pengumuman baru

+
+ @else +
+ @foreach($announcements as $announcement) +
+
+
+
+ @if($announcement->mode === 'reguler') +
+ +
+ @else +
+ +
+ @endif +
+
+

+ {{ $announcement->message }} +

+
+
+ + {{ $announcement->user->name }} +
+
+ + {{ $announcement->sent_at->diffForHumans() }} +
+
+ + {{ count($announcement->ruangan) }} ruangan +
+
+
+
+ +
+
+ @endforeach +
+ + +
+ {{ $announcements->links('vendor.pagination.tailwind') }} +
+ @endif +
+
+
+ + + + + + + + + +@endsection \ No newline at end of file diff --git a/resources/views/admin/announcement/show.blade.php b/resources/views/admin/announcement/show.blade.php new file mode 100644 index 0000000..fb53039 --- /dev/null +++ b/resources/views/admin/announcement/show.blade.php @@ -0,0 +1,80 @@ +@extends('layouts.dashboard') + +@section('content') +
+
+
+

Detail Pengumuman

+
+
+ +
+
+
+
+
Informasi Pengumuman
+
+
+
+
+ Waktu: {{ $announcement->sent_at->format('d M Y H:i:s') }} +
+
+ Mode: + + {{ strtoupper($announcement->mode) }} + +
+
+ +
+ Ruangan Tujuan: +
+ @foreach($announcement->ruangans as $ruangan) + {{ $ruangan->nama_ruangan }} + @endforeach +
+
+ +
+ Isi Pengumuman: +
+ @if($announcement->mode === 'tts') +

{{ $announcement->message }}

+ @if($announcement->audio_path) + + @endif +
+ + Suara: {{ $announcement->voice }} | + Kecepatan: {{ $announcement->speed }} + +
+ @else +

{{ $announcement->message }}

+ @endif +
+
+ +
+
+ Dibuat Oleh: {{ $announcement->user->name }} +
+
+ Dibuat Pada: {{ $announcement->created_at->format('d M Y H:i:s') }} +
+
+
+ +
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/admin/announcements/history.blade.php b/resources/views/admin/announcements/history.blade.php deleted file mode 100644 index 98bed7a..0000000 --- a/resources/views/admin/announcements/history.blade.php +++ /dev/null @@ -1,395 +0,0 @@ -@extends('layouts.dashboard') - -@section('content') -
- -
-
-
- - - -
-
-

Riwayat Pengumuman

-

Daftar semua pengumuman yang pernah dikirim

-
-
- -
- - -
-
- -
- -
- -
- - - -
-
-
- - -
- - -
- - -
- - -
-
-
- - -
-
- - - - - - - - - - - - - - - - - - -
IDWaktuJenisIsiRuanganStatusAksi
- Memuat data... -
-
-
-
- Menampilkan 0 sampai 0 dari 0 entri -
-
- - -
-
-
-
- - - - - - -@endsection \ No newline at end of file diff --git a/resources/views/admin/announcements/index.blade.php b/resources/views/admin/announcements/index.blade.php deleted file mode 100644 index 5fbd71b..0000000 --- a/resources/views/admin/announcements/index.blade.php +++ /dev/null @@ -1,543 +0,0 @@ -@extends('layouts.dashboard') - -@section('content') -
- -
-
-
- - - -
-
-

Sistem Pengumuman

-

Kelola pengumuman untuk seluruh ruangan

-
-
-
-
- - Memeriksa koneksi... -
- - - - - Riwayat - -
-
- - -
- @if(session('success')) -
- {{ session('success') }} -
- @endif - - @if(session('error')) -
- {{ session('error') }} -
- @endif -
- - -
- -
-
-
-

- - - - Buat Pengumuman Baru -

-
-
-
- @csrf - - -
- -
- - -
-
- - -
-
- - -
-
- @foreach($ruangans as $ruangan) - - @endforeach -
-
- - -
- -
- -
- 0/500 -
-
-
- - -
- -
-
-
-
-
- - -
- -
-
-

- - - - Status Sistem -

-
-
- -
-
-
-
-
-

Tidak ada pengumuman aktif

-

Siap menerima pengumuman baru

-
-
- - - -
-
- - - -
-
-
- - - - - - -@endsection \ No newline at end of file diff --git a/resources/views/admin/component/sidebar.blade.php b/resources/views/admin/component/sidebar.blade.php index 709cfcf..11ee1fc 100644 --- a/resources/views/admin/component/sidebar.blade.php +++ b/resources/views/admin/component/sidebar.blade.php @@ -114,7 +114,7 @@ class="dropdown-btn w-full text-left p-2 bg-gray-100 rounded-lg flex justify-bet
  • Bel
  • -
  • Pengumuman
  • diff --git a/routes/web.php b/routes/web.php index d10cb8c..45bc8bc 100644 --- a/routes/web.php +++ b/routes/web.php @@ -101,13 +101,13 @@ }); // Announcement System - 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.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'); + Route::prefix('announcement')->controller(AnnouncementController::class)->group(function () { + Route::get('/', 'index')->name('announcement.index'); + Route::post('/', 'store')->name('announcement.store'); + Route::get('/history', 'history')->name('announcement.history'); + Route::get('/{announcement}', 'show')->name('announcement.show'); + Route::delete('/{announcement}', 'destroy')->name('announcement.destroy'); + Route::post('/tts-preview', 'ttsPreview')->name('announcement.ttsPreview'); }); }); }); \ No newline at end of file