fix controller,model,migrate,views pengumuman and fix tampilan ruangan,kelas,jurusan
fix tampilan pengumuman
This commit is contained in:
parent
43a61488c0
commit
734bc685f3
|
@ -4,353 +4,158 @@
|
|||
|
||||
use App\Models\Announcement;
|
||||
use App\Models\Ruangan;
|
||||
use App\Services\MqttService;
|
||||
use App\Http\Requests\StoreAnnouncementRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Carbon\Carbon;
|
||||
use PhpMqtt\Client\Facades\MQTT;
|
||||
|
||||
class AnnouncementController extends Controller
|
||||
{
|
||||
// Mode constants
|
||||
const MODE_REGULER = 'reguler';
|
||||
const MODE_TTS = 'tts';
|
||||
|
||||
// Relay constants
|
||||
const RELAY_ON = 'ON';
|
||||
const RELAY_OFF = 'OFF';
|
||||
|
||||
// TTS API constants
|
||||
const TTS_API_URL = 'http://api.voicerss.org/';
|
||||
const TTS_API_KEY = '90927de8275148d79080facd20fb486c';
|
||||
const TTS_DEFAULT_VOICE = 'id-id';
|
||||
const TTS_DEFAULT_SPEED = 0;
|
||||
const TTS_DEFAULT_FORMAT = 'wav';
|
||||
|
||||
protected $mqttService;
|
||||
protected $mqttConfig;
|
||||
|
||||
public function __construct(MqttService $mqttService)
|
||||
{
|
||||
$this->mqttService = $mqttService;
|
||||
$this->mqttConfig = config('mqtt');
|
||||
$this->initializeMqttSubscriptions();
|
||||
}
|
||||
|
||||
protected function initializeMqttSubscriptions()
|
||||
{
|
||||
try {
|
||||
$this->mqttService->subscribe(
|
||||
$this->mqttConfig['topics']['responses']['announcement_ack'],
|
||||
function (string $topic, string $message) {
|
||||
$this->handleAnnouncementAck($message);
|
||||
}
|
||||
);
|
||||
|
||||
$this->mqttService->subscribe(
|
||||
$this->mqttConfig['topics']['responses']['announcement_error'],
|
||||
function (string $topic, string $message) {
|
||||
$this->handleAnnouncementError($message);
|
||||
}
|
||||
);
|
||||
|
||||
$this->mqttService->subscribe(
|
||||
$this->mqttConfig['topics']['responses']['relay_status'],
|
||||
function (string $topic, string $message) {
|
||||
$this->handleRelayStatusUpdate($message);
|
||||
}
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('MQTT Subscription Error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$ruangan = Ruangan::with(['kelas', 'jurusan'])->get();
|
||||
$announcements = Announcement::with(['ruangans'])
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
try {
|
||||
$mqttStatus = $this->mqttService->isConnected() ? 'Connected' : 'Disconnected';
|
||||
} catch (\Exception $e) {
|
||||
$mqttStatus = 'Disconnected';
|
||||
Log::error('MQTT check failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return view('admin.announcement.index', [
|
||||
'ruangans' => $ruangan,
|
||||
'announcements' => $announcements,
|
||||
'modes' => [self::MODE_REGULER, self::MODE_TTS],
|
||||
'relayStates' => [self::RELAY_ON, self::RELAY_OFF],
|
||||
'mqttStatus' => $mqttStatus
|
||||
]);
|
||||
$ruangans = Ruangan::orderBy('nama_ruangan')->get();
|
||||
$mqttStatus = $this->checkMqttConnection();
|
||||
return view('admin.announcement.index', compact('ruangans', 'mqttStatus'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
public function history()
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'mode' => 'required|in:reguler,tts',
|
||||
'ruangans' => 'required|array',
|
||||
'ruangans.*' => 'exists:ruangan,id',
|
||||
'relay_action' => 'required_if:mode,reguler|in:ON,OFF',
|
||||
'tts_text' => 'required_if:mode,tts|string|max:1000',
|
||||
'tts_voice' => 'required_if:mode,tts',
|
||||
'tts_speed' => 'required_if:mode,tts|integer|min:-10|max:10',
|
||||
]);
|
||||
$announcements = Announcement::with('ruangans')
|
||||
->when(request('mode'), function($query, $mode) {
|
||||
return $query->where('mode', $mode);
|
||||
})
|
||||
->when(request('date'), function($query, $date) {
|
||||
return $query->whereDate('sent_at', $date);
|
||||
})
|
||||
->orderBy('sent_at', 'desc')
|
||||
->paginate(10);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->back()
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
return view('admin.announcement.history', compact('announcements'));
|
||||
}
|
||||
|
||||
private function checkMqttConnection()
|
||||
{
|
||||
try {
|
||||
$announcement = new Announcement();
|
||||
$announcement->mode = $request->mode;
|
||||
|
||||
if ($request->mode === self::MODE_REGULER) {
|
||||
$announcement->message = $request->relay_action === self::RELAY_ON
|
||||
? 'Aktivasi Relay Ruangan'
|
||||
: 'Deaktivasi Relay Ruangan';
|
||||
$announcement->is_active = $request->relay_action === self::RELAY_ON;
|
||||
$announcement->relay_state = $request->relay_action;
|
||||
} else {
|
||||
$audioContent = $this->generateTTS(
|
||||
$request->tts_text,
|
||||
$request->tts_voice,
|
||||
$request->tts_speed
|
||||
);
|
||||
|
||||
if (!$audioContent) {
|
||||
throw new \Exception('Failed to generate TTS audio');
|
||||
}
|
||||
|
||||
$fileName = 'tts/' . now()->format('YmdHis') . '.wav';
|
||||
Storage::disk('public')->put($fileName, $audioContent);
|
||||
|
||||
$announcement->message = $request->tts_text;
|
||||
$announcement->audio_path = $fileName;
|
||||
$announcement->voice = $request->tts_voice;
|
||||
$announcement->speed = $request->tts_speed;
|
||||
$announcement->relay_state = self::RELAY_OFF; // Default untuk TTS
|
||||
}
|
||||
|
||||
$announcement->sent_at = now();
|
||||
$announcement->status = 'pending';
|
||||
|
||||
if (!$announcement->save()) {
|
||||
throw new \Exception('Failed to save announcement');
|
||||
}
|
||||
|
||||
$existingRuangan = Ruangan::whereIn('id', $request->ruangans)->pluck('id');
|
||||
if ($existingRuangan->count() != count($request->ruangans)) {
|
||||
throw new \Exception('Some selected ruangan not found');
|
||||
}
|
||||
|
||||
$announcement->ruangans()->sync($existingRuangan);
|
||||
|
||||
$this->publishAnnouncement($announcement);
|
||||
|
||||
return redirect()->route('announcement.index')
|
||||
->with('success', 'Pengumuman berhasil dikirim');
|
||||
|
||||
$mqtt = MQTT::connection();
|
||||
return $mqtt->isConnected();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Announcement Error: ' . $e->getMessage());
|
||||
if (isset($announcement) && $announcement->exists) {
|
||||
$announcement->delete();
|
||||
}
|
||||
return redirect()->back()
|
||||
->with('error', 'Gagal: ' . $e->getMessage())
|
||||
->withInput();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function publishAnnouncement(Announcement $announcement)
|
||||
public function store(StoreAnnouncementRequest $request)
|
||||
{
|
||||
$payload = [
|
||||
'mode' => $announcement->mode,
|
||||
'announcement_id' => $announcement->id,
|
||||
'ruangans' => $announcement->ruangans->pluck('nama_ruangan')->toArray(),
|
||||
'timestamp' => now()->toDateTimeString()
|
||||
$announcementData = [
|
||||
'mode' => $request->mode,
|
||||
'sent_at' => now(),
|
||||
];
|
||||
|
||||
if ($announcement->mode === self::MODE_REGULER) {
|
||||
$payload['relay_state'] = $announcement->relay_state;
|
||||
|
||||
// Kirim perintah relay ke masing-masing ruangan
|
||||
foreach ($announcement->ruangans as $ruangan) {
|
||||
$topic = $ruangan->mqtt_topic ?? "ruangan/{$ruangan->id}/relay/control";
|
||||
|
||||
$this->mqttService->publish(
|
||||
$topic,
|
||||
json_encode([
|
||||
'state' => $announcement->relay_state,
|
||||
'announcement_id' => $announcement->id
|
||||
]),
|
||||
1 // QoS level
|
||||
);
|
||||
|
||||
// Update status relay di database
|
||||
$ruangan->update(['relay_state' => $announcement->relay_state]);
|
||||
}
|
||||
} else {
|
||||
$payload['message'] = $announcement->message;
|
||||
$payload['audio_url'] = asset('storage/' . $announcement->audio_path);
|
||||
$payload['voice'] = $announcement->voice;
|
||||
$payload['speed'] = $announcement->speed;
|
||||
// Hanya tambahkan message jika mode TTS
|
||||
if ($request->mode === 'tts') {
|
||||
$announcementData['message'] = $request->message;
|
||||
}
|
||||
|
||||
// Publis ke topic announcement umum
|
||||
$this->mqttService->publish(
|
||||
$this->mqttConfig['topics']['commands']['announcement'],
|
||||
json_encode($payload),
|
||||
1
|
||||
);
|
||||
$announcement = Announcement::create($announcementData);
|
||||
$announcement->ruangans()->sync($request->ruangans);
|
||||
|
||||
try {
|
||||
if ($request->mode === 'tts') {
|
||||
MQTT::publish('control/relay', json_encode([
|
||||
'mode' => 'tts',
|
||||
'ruang' => $request->ruangans
|
||||
]));
|
||||
|
||||
MQTT::publish('tts/play', json_encode([
|
||||
'ruang' => $request->ruangans,
|
||||
'teks' => $request->message
|
||||
]));
|
||||
} else {
|
||||
MQTT::publish('control/relay', json_encode([
|
||||
'mode' => 'reguler',
|
||||
'ruang' => $request->ruangans
|
||||
]));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'message' => 'Gagal mengirim ke perangkat: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
protected function generateTTS($text, $voice, $speed)
|
||||
public function details($id)
|
||||
{
|
||||
$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'
|
||||
$announcement = Announcement::with('ruangans')->findOrFail($id);
|
||||
return response()->json([
|
||||
'mode' => $announcement->mode,
|
||||
'formatted_sent_at' => $announcement->formatted_sent_at,
|
||||
'message' => $announcement->message,
|
||||
'ruangans' => $announcement->ruangans->map(function($ruangan) {
|
||||
return ['nama_ruangan' => $ruangan->nama_ruangan];
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$announcement = Announcement::findOrFail($id);
|
||||
$announcement->delete();
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function checkMqtt()
|
||||
{
|
||||
return response()->json([
|
||||
'connected' => $this->checkMqttConnection()
|
||||
]);
|
||||
}
|
||||
|
||||
public function controlRelay(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'ruangans' => 'required|array|min:1',
|
||||
'ruangans.*' => 'exists:ruangan,id',
|
||||
'action' => 'required|in:activate,deactivate',
|
||||
'mode' => 'required|in:manual,tts'
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return $response->body();
|
||||
}
|
||||
$ruangans = Ruangan::whereIn('id', $request->ruangans)->get();
|
||||
$state = $request->action === 'activate' ? 'on' : 'off';
|
||||
|
||||
Log::error('TTS API Error: ' . $response->body());
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function handleAnnouncementAck(string $message)
|
||||
{
|
||||
try {
|
||||
$data = json_decode($message, true);
|
||||
// Kirim perintah ke ESP32 via MQTT
|
||||
MQTT::publish('control/relay', json_encode([
|
||||
'action' => $request->action,
|
||||
'ruang' => $request->ruangans,
|
||||
'mode' => $request->mode
|
||||
]));
|
||||
|
||||
if (isset($data['announcement_id'])) {
|
||||
Announcement::where('id', $data['announcement_id'])
|
||||
->update(['status' => 'delivered']);
|
||||
// Update status relay di database
|
||||
Ruangan::whereIn('id', $request->ruangans)->update(['relay_state' => $state]);
|
||||
|
||||
Log::info('Announcement delivered', $data);
|
||||
// Jika mengaktifkan relay, simpan sebagai pengumuman manual
|
||||
if ($request->action === 'activate' && $request->mode === 'manual') {
|
||||
$announcement = Announcement::create([
|
||||
'mode' => 'manual',
|
||||
'message' => 'Pengumuman via microphone manual',
|
||||
'sent_at' => now()
|
||||
]);
|
||||
|
||||
$announcement->ruangans()->sync($request->ruangans);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('ACK Handler Error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleAnnouncementError(string $message)
|
||||
{
|
||||
try {
|
||||
$data = json_decode($message, true);
|
||||
|
||||
if (isset($data['announcement_id'])) {
|
||||
Announcement::where('id', $data['announcement_id'])
|
||||
->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $data['error'] ?? 'Unknown error'
|
||||
]);
|
||||
|
||||
Log::error('Announcement failed', $data);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error Handler Error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleRelayStatusUpdate(string $message)
|
||||
{
|
||||
try {
|
||||
$data = json_decode($message, true);
|
||||
|
||||
if (isset($data['ruangan_id'], $data['state'])) {
|
||||
Ruangan::where('id', $data['ruangan_id'])
|
||||
->update(['relay_state' => $data['state']]);
|
||||
|
||||
Log::info('Relay status updated', $data);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Relay Status Handler Error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function ttsPreview(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'text' => 'required|string|max:1000',
|
||||
'voice' => 'required|string',
|
||||
'speed' => 'required|integer|min:-10|max:10'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'error' => $validator->errors()->first()
|
||||
], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$audioContent = $this->generateTTS(
|
||||
$request->text,
|
||||
$request->voice,
|
||||
$request->speed
|
||||
);
|
||||
|
||||
if (!$audioContent) {
|
||||
throw new \Exception('Failed to generate TTS audio');
|
||||
}
|
||||
|
||||
$fileName = 'tts/previews/' . uniqid() . '.wav';
|
||||
Storage::disk('public')->put($fileName, $audioContent);
|
||||
|
||||
return response()->json([
|
||||
'audio_url' => asset('storage/' . $fileName)
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('TTS Preview Error: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'error' => 'Failed to generate preview'
|
||||
'message' => 'Gagal mengontrol relay: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function history(Request $request)
|
||||
public function relayStatus()
|
||||
{
|
||||
$search = $request->input('search');
|
||||
$mode = $request->input('mode');
|
||||
$relayState = $request->input('relay_state');
|
||||
|
||||
$announcements = Announcement::with(['ruangans'])
|
||||
->when($search, function($query) use ($search) {
|
||||
return $query->where('message', 'like', "%{$search}%")
|
||||
->orWhereHas('ruangans', function($q) use ($search) {
|
||||
$q->where('nama_ruangan', 'like', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->when($mode, function($query) use ($mode) {
|
||||
return $query->where('mode', $mode);
|
||||
})
|
||||
->when($relayState, function($query) use ($relayState) {
|
||||
return $query->where('relay_state', $relayState);
|
||||
})
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
return view('admin.announcement.history', [
|
||||
'announcements' => $announcements,
|
||||
'search' => $search,
|
||||
'mode' => $mode,
|
||||
'relay_state' => $relayState,
|
||||
'modes' => [self::MODE_REGULER, self::MODE_TTS],
|
||||
'relayStates' => [self::RELAY_ON, self::RELAY_OFF]
|
||||
]);
|
||||
$ruangans = Ruangan::select('id', 'relay_state')->get();
|
||||
return response()->json($ruangans);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreAnnouncementRequest extends FormRequest
|
||||
{
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'message' => 'required|string|max:500',
|
||||
'mode' => 'required|in:tts,manual',
|
||||
'ruangans' => 'required|array|min:1',
|
||||
'ruangans.*' => 'exists:ruangan,id',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'ruangans.required' => 'Pilih minimal satu ruangan',
|
||||
'ruangans.min' => 'Pilih minimal satu ruangan',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -4,121 +4,37 @@
|
|||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Announcement extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'announcements'; // Pastikan konsisten
|
||||
|
||||
protected $fillable = [
|
||||
'mode',
|
||||
'message',
|
||||
'audio_path',
|
||||
'voice',
|
||||
'speed',
|
||||
'is_active',
|
||||
'status',
|
||||
'error_message',
|
||||
'sent_at',
|
||||
'relay_state' // Tambahkan ini
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'is_active' => true,
|
||||
'status' => 'pending',
|
||||
'relay_state' => 'OFF' // Default value
|
||||
];
|
||||
protected $fillable = ['message', 'mode', 'sent_at'];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'sent_at' => 'datetime'
|
||||
'sent_at' => 'datetime',
|
||||
];
|
||||
|
||||
// Tambahkan aksesor untuk relay
|
||||
public function getRelayStateDescriptionAttribute()
|
||||
{
|
||||
return $this->relay_state === 'ON' ? 'Relay Menyala' : 'Relay Mati';
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship with Ruangan (many-to-many)
|
||||
*/
|
||||
public function ruangans()
|
||||
{
|
||||
return $this->belongsToMany(Ruangan::class, 'announcement_ruangan')
|
||||
->withTimestamps();
|
||||
return $this->belongsToMany(Ruangan::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for regular announcements
|
||||
*/
|
||||
public function scopeReguler($query)
|
||||
{
|
||||
return $query->where('mode', 'reguler');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for TTS announcements
|
||||
*/
|
||||
public function scopeTts($query)
|
||||
{
|
||||
return $query->where('mode', 'tts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for delivered announcements
|
||||
*/
|
||||
public function scopeDelivered($query)
|
||||
{
|
||||
return $query->where('status', 'delivered');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for failed announcements
|
||||
*/
|
||||
public function scopeFailed($query)
|
||||
{
|
||||
return $query->where('status', 'failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for audio URL
|
||||
*/
|
||||
public function getAudioUrlAttribute()
|
||||
{
|
||||
return $this->audio_path ? asset('storage/' . $this->audio_path) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for formatted sent time
|
||||
*/
|
||||
public function getFormattedSentAtAttribute()
|
||||
{
|
||||
return $this->sent_at->format('d M Y H:i:s');
|
||||
return $this->sent_at
|
||||
? $this->sent_at->format('d/m/Y H:i:s')
|
||||
: 'Belum dikirim';
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor untuk pesan aktivasi
|
||||
*/
|
||||
public function getActivationMessageAttribute()
|
||||
// Accessor for short message
|
||||
public function getShortMessageAttribute()
|
||||
{
|
||||
return $this->is_active ? 'Aktivasi Ruangan' : 'Deaktivasi Ruangan';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah pengumuman reguler
|
||||
*/
|
||||
public function isReguler()
|
||||
{
|
||||
return $this->mode === 'reguler';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah pengumuman TTS
|
||||
*/
|
||||
public function isTts()
|
||||
{
|
||||
return $this->mode === 'tts';
|
||||
if ($this->mode === 'manual') {
|
||||
return 'Relay Control';
|
||||
}
|
||||
return Str::limit($this->message, 30);
|
||||
}
|
||||
}
|
|
@ -15,15 +15,13 @@ class Ruangan extends Model
|
|||
'nama_ruangan',
|
||||
'id_kelas',
|
||||
'id_jurusan',
|
||||
'relay_state', // Ubah dari status_relay menjadi relay_state
|
||||
'mqtt_topic' // Tambahkan kolom untuk custom MQTT topic
|
||||
'relay_state'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'relay_state' => 'string' // Ubah menjadi string untuk menyimpan 'ON'/'OFF'
|
||||
'relay_state' => 'string'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Relationship with Kelas
|
||||
*/
|
||||
|
@ -56,19 +54,4 @@ public function getNamaRuanganAttribute($value)
|
|||
return strtoupper($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for active relay status
|
||||
*/
|
||||
public function scopeRelayActive($query)
|
||||
{
|
||||
return $query->where('status_relay', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for inactive relay status
|
||||
*/
|
||||
public function scopeRelayInactive($query)
|
||||
{
|
||||
return $query->where('status_relay', false);
|
||||
}
|
||||
}
|
|
@ -13,8 +13,7 @@ public function up(): void
|
|||
$table->string('nama_ruangan');
|
||||
$table->foreignId('id_kelas')->constrained('kelas')->onDelete('cascade');
|
||||
$table->foreignId('id_jurusan')->constrained('jurusan')->onDelete('cascade');
|
||||
$table->string('relay_state')->default('OFF'); // Ubah tipe data
|
||||
$table->string('mqtt_topic')->nullable(); // Untuk custom topic per ruangan
|
||||
$table->enum('relay_state', ['on', 'off'])->default('off');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('nama_ruangan');
|
||||
|
|
|
@ -10,32 +10,16 @@ public function up()
|
|||
{
|
||||
Schema::create('announcements', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('mode');
|
||||
$table->text('message');
|
||||
$table->string('audio_path')->nullable();
|
||||
$table->string('voice')->nullable();
|
||||
$table->integer('speed')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->string('status')->default('pending');
|
||||
$table->text('error_message')->nullable();
|
||||
$table->string('relay_state')->default('OFF'); // Tambahkan kolom ini
|
||||
$table->timestamp('sent_at')->useCurrent();
|
||||
$table->text('message')->nullable();
|
||||
$table->string('mode')->default('tts');
|
||||
$table->timestamp('sent_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('mode');
|
||||
$table->index('sent_at');
|
||||
$table->index('relay_state'); // Tambahkan index
|
||||
});
|
||||
|
||||
// Perbaikan utama: Explicitly specify table names
|
||||
Schema::create('announcement_ruangan', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('announcement_id')->constrained('announcements')->onDelete('cascade');
|
||||
$table->foreignId('ruangan_id')->constrained('ruangan')->onDelete('cascade');
|
||||
$table->string('relay_state_at_time')->nullable(); // State saat pengumuman dikirim
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['announcement_id', 'ruangan_id']);
|
||||
$table->primary(['announcement_id', 'ruangan_id']);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -64,3 +64,15 @@ .badge-green {
|
|||
.badge-gray {
|
||||
@apply px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded-full;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.shadow-sm {
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.hover\:shadow-lg:hover {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
|
@ -1,218 +1,167 @@
|
|||
@extends('layouts.dashboard')
|
||||
|
||||
@section('title', 'Riwayat Pengumuman')
|
||||
@section('title', 'Riwayat Pengumuman - Smart School')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Header Section -->
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-800">Riwayat Pengumuman</h1>
|
||||
<p class="text-gray-600 mt-2">Daftar seluruh pengumuman yang pernah dikirim</p>
|
||||
</div>
|
||||
<div class="mt-4 md:mt-0 flex space-x-3">
|
||||
<a href="{{ route('announcement.index') }}"
|
||||
class="flex items-center px-5 py-2.5 bg-indigo-50 text-indigo-700 rounded-lg hover:bg-indigo-100 transition duration-300 shadow-sm">
|
||||
<i class="fas fa-arrow-left mr-2"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">Riwayat Pengumuman</h1>
|
||||
<a href="{{ route('admin.announcement.index') }}"
|
||||
class="flex items-center mt-4 md:mt-0 px-4 py-2 bg-white border border-blue-500 text-blue-600 rounded-lg hover:bg-blue-50 transition-colors">
|
||||
<i class="fas fa-plus-circle mr-2"></i> Buat Pengumuman Baru
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Filter Section -->
|
||||
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-6 border border-gray-100">
|
||||
<div class="p-6">
|
||||
<form action="{{ route('admin.announcement.history') }}" method="GET">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Search Input -->
|
||||
<div>
|
||||
<label for="search" class="block text-gray-700 font-medium mb-2">Cari</label>
|
||||
<div class="relative">
|
||||
<input type="text" name="search" id="search" value="{{ request('search') }}"
|
||||
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-200 pr-10"
|
||||
placeholder="Cari pengumuman atau ruangan...">
|
||||
<button type="submit" class="absolute right-3 top-3 text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
<!-- Enhanced Filter Card -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 mb-6 overflow-hidden">
|
||||
<div class="p-4 border-b border-gray-200 bg-gray-50">
|
||||
<h2 class="text-lg font-medium text-gray-700 flex items-center">
|
||||
<i class="fas fa-filter mr-2 text-blue-500"></i> Filter Riwayat
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<form id="filterForm" method="GET" action="{{ route('admin.announcement.history') }}" class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label for="mode" class="block text-sm font-medium text-gray-700 mb-1">Mode Pengumuman</label>
|
||||
<div class="relative">
|
||||
<select id="mode" name="mode" class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
|
||||
<option value="">Semua Mode</option>
|
||||
<option value="tts" {{ request('mode') == 'tts' ? 'selected' : '' }}>Text-to-Speech</option>
|
||||
<option value="manual" {{ request('mode') == 'manual' ? 'selected' : '' }}>Manual</option>
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<i class="fas fa-caret-down text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Filter -->
|
||||
<div>
|
||||
<label for="mode" class="block text-gray-700 font-medium mb-2">Jenis Pengumuman</label>
|
||||
<div class="relative">
|
||||
<select name="mode" id="mode"
|
||||
class="appearance-none w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-200 pr-10 bg-white">
|
||||
<option value="">Semua Jenis</option>
|
||||
<option value="reguler" {{ request('mode') == 'reguler' ? 'selected' : '' }}>Aktivasi Ruangan</option>
|
||||
<option value="tts" {{ request('mode') == 'tts' ? 'selected' : '' }}>Pengumuman Suara (TTS)</option>
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-700">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="start_date" class="block text-sm font-medium text-gray-700 mb-1">Dari Tanggal</label>
|
||||
<div class="relative">
|
||||
<input type="date" id="start_date" name="start_date" value="{{ request('start_date') }}"
|
||||
class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<i class="fas fa-calendar-alt text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset Button -->
|
||||
<div class="flex items-end">
|
||||
<a href="{{ route('admin.announcement.history') }}"
|
||||
class="px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition duration-300 flex items-center">
|
||||
<i class="fas fa-sync-alt mr-2"></i> Reset Filter
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<label for="end_date" class="block text-sm font-medium text-gray-700 mb-1">Sampai Tanggal</label>
|
||||
<div class="relative">
|
||||
<input type="date" id="end_date" name="end_date" value="{{ request('end_date') }}"
|
||||
class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<i class="fas fa-calendar-alt text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end space-x-3">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 h-[42px] transition-colors">
|
||||
<i class="fas fa-search mr-2"></i> Terapkan Filter
|
||||
</button>
|
||||
@if(request()->has('mode') || request()->has('start_date') || request()->has('end_date'))
|
||||
<a href="{{ route('admin.announcement.history') }}" class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 h-[42px] transition-colors">
|
||||
<i class="fas fa-sync-alt mr-2"></i> Reset
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Announcements Table -->
|
||||
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-gray-100">
|
||||
<!-- Table Header -->
|
||||
<div class="bg-gradient-to-r from-gray-50 to-gray-100 px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Daftar Pengumuman</h2>
|
||||
<div class="mt-2 md:mt-0 text-sm">
|
||||
Menampilkan {{ $announcements->count() }} dari {{ $announcements->total() }} pengumuman
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Body -->
|
||||
<!-- History Table -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Jenis
|
||||
#
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Konten
|
||||
Waktu Pengiriman
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Mode
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Isi Pengumuman
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Ruangan Tujuan
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Waktu
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Aksi
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse($announcements as $announcement)
|
||||
<tr class="hover:bg-gray-50 transition duration-150">
|
||||
<!-- Jenis -->
|
||||
@forelse($announcements as $index => $announcement)
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $index + 1 + (($announcements->currentPage() - 1) * $announcements->perPage()) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
@if($announcement->mode === 'reguler')
|
||||
<div class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center mr-3">
|
||||
<i class="fas fa-door-open text-blue-600 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900">Aktivasi</span>
|
||||
@else
|
||||
<div class="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center mr-3">
|
||||
<i class="fas fa-volume-up text-purple-600 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900">TTS</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Konten -->
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-900 max-w-xs truncate">
|
||||
@if($announcement->mode === 'tts')
|
||||
{{ $announcement->message }}
|
||||
@else
|
||||
{{ $announcement->is_active ? 'Aktivasi ruangan' : 'Deaktivasi ruangan' }}
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
Status: {{ $announcement->is_active ? 'AKTIF' : 'NONAKTIF' }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@if($announcement->mode === 'tts')
|
||||
<div class="mt-1">
|
||||
<audio controls class="h-8">
|
||||
<source src="{{ asset('storage/' . $announcement->audio_path) }}" type="audio/wav">
|
||||
</audio>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<!-- Ruangan -->
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $announcement->ruangans->count() }} ruangan
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
@foreach($announcement->ruangans->take(3) as $ruangan)
|
||||
{{ $ruangan->nama_ruangan }}@if(!$loop->last), @endif
|
||||
@endforeach
|
||||
@if($announcement->ruangans->count() > 3)
|
||||
+{{ $announcement->ruangans->count() - 3 }} lainnya
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Waktu -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $announcement->sent_at->format('d M Y') }}
|
||||
<div class="text-sm font-semibold text-gray-900">
|
||||
{{ $announcement->formatted_sent_at }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ $announcement->sent_at->format('H:i') }}
|
||||
{{ $announcement->sent_at->diffForHumans() }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Status -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if($announcement->status === 'delivered')
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
<i class="fas fa-check-circle mr-1"></i> Terkirim
|
||||
</span>
|
||||
@elseif($announcement->status === 'failed')
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
||||
<i class="fas fa-times-circle mr-1"></i> Gagal
|
||||
@if($announcement->mode === 'tts')
|
||||
<span class="px-2.5 py-0.5 inline-flex text-xs leading-4 font-medium rounded-full bg-green-100 text-green-800">
|
||||
<i class="fas fa-robot mr-1"></i> TTS
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
||||
<i class="fas fa-clock mr-1"></i> Proses
|
||||
<span class="px-2.5 py-0.5 inline-flex text-xs leading-4 font-medium rounded-full bg-blue-100 text-blue-800">
|
||||
<i class="fas fa-microphone-alt mr-1"></i> Manual
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<!-- Aksi -->
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $announcement->short_message }}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">{{ Str::limit($announcement->message, 80) }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@foreach($announcement->ruangans as $ruangan)
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
|
||||
<span class="w-1.5 h-1.5 rounded-full mr-1 {{ $announcement->mode === 'tts' ? 'bg-green-500' : 'bg-blue-500' }}"></span>
|
||||
{{ $ruangan->nama_ruangan }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="dropdown relative inline-block">
|
||||
<button class="dropdown-toggle p-1 rounded-full hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition duration-200">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<!-- Detail Button -->
|
||||
<button onclick="showAnnouncementDetails('{{ $announcement->id }}')"
|
||||
class="inline-flex items-center px-3 py-1.5 border border-blue-500 text-sm font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
|
||||
<i class="fas fa-eye mr-1"></i> Detail
|
||||
</button>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<button onclick="confirmDelete('{{ $announcement->id }}')"
|
||||
class="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors">
|
||||
<i class="fas fa-trash-alt mr-1"></i> Hapus
|
||||
</button>
|
||||
<div class="dropdown-menu absolute right-0 mt-1 w-40 bg-white rounded-md shadow-lg py-1 z-10 hidden border border-gray-200">
|
||||
<a href="{{ route('admin.announcement.show', $announcement->id) }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-eye mr-2"></i> Detail
|
||||
</a>
|
||||
<form action="{{ route('admin.announcement.destroy', $announcement->id) }}" method="POST" class="block w-full">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="button" onclick="confirmDelete(this)" class="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
|
||||
<i class="fas fa-trash-alt mr-2"></i> Hapus
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center">
|
||||
<div class="mx-auto w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||||
<i class="fas fa-bullhorn text-3xl text-gray-400"></i>
|
||||
<td colspan="6" class="px-6 py-8 text-center">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<i class="fas fa-inbox text-3xl text-gray-300 mb-3"></i>
|
||||
<p class="text-gray-500 text-sm">Tidak ada riwayat pengumuman</p>
|
||||
@if(request()->has('mode') || request()->has('start_date') || request()->has('end_date'))
|
||||
<a href="{{ route('admin.announcement.history') }}" class="text-blue-600 hover:text-blue-800 mt-2 text-sm flex items-center">
|
||||
<i class="fas fa-sync-alt mr-1"></i> Reset filter
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-700">Belum Ada Pengumuman</h3>
|
||||
<p class="text-gray-500 mt-1">Tidak ada riwayat pengumuman yang ditemukan</p>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
|
@ -221,49 +170,132 @@ class="px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transi
|
|||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($announcements->hasPages())
|
||||
<div class="bg-gray-50 px-6 py-4 border-t border-gray-200">
|
||||
{{ $announcements->appends(request()->query())->links() }}
|
||||
<div class="flex flex-col md:flex-row items-center justify-between">
|
||||
<div class="mb-4 md:mb-0">
|
||||
<p class="text-sm text-gray-700">
|
||||
Menampilkan <span class="font-medium">{{ $announcements->firstItem() }}</span>
|
||||
sampai <span class="font-medium">{{ $announcements->lastItem() }}</span>
|
||||
dari <span class="font-medium">{{ $announcements->total() }}</span> hasil
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{{ $announcements->appends(request()->query())->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SweetAlert CDN -->
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- Custom Script -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Confirm delete function
|
||||
function confirmDelete(form) {
|
||||
// Show announcement details in modal
|
||||
function showAnnouncementDetails(id) {
|
||||
$.get(`/admin/announcement/${id}/details`, function(data) {
|
||||
Swal.fire({
|
||||
title: 'Hapus Pengumuman?',
|
||||
text: "Anda tidak akan bisa mengembalikan data ini!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#6366f1',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Ya, Hapus!',
|
||||
cancelButtonText: 'Batal',
|
||||
backdrop: 'rgba(99, 102, 241, 0.1)'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
form.closest('form').submit();
|
||||
title: 'Detail Pengumuman',
|
||||
html: `
|
||||
<div class="text-left space-y-3">
|
||||
<div class="flex items-center">
|
||||
<span class="font-medium w-24">Mode:</span>
|
||||
<span class="px-2.5 py-0.5 inline-flex text-xs leading-4 font-medium rounded-full ${data.mode === 'tts' ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800'}">
|
||||
${data.mode === 'tts' ? 'Text-to-Speech' : 'Manual'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium w-24">Waktu:</span>
|
||||
<span>${data.formatted_sent_at}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium w-24">Ruangan:</span>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
${data.ruangans.map(r => `
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
|
||||
<span class="w-1.5 h-1.5 rounded-full mr-1 ${data.mode === 'tts' ? 'bg-green-500' : 'bg-blue-500'}"></span>
|
||||
${r.nama_ruangan}
|
||||
</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 p-3 bg-gray-50 rounded border border-gray-200">
|
||||
<p class="font-medium text-sm">Isi Pengumuman:</p>
|
||||
<p class="mt-1 text-sm">${data.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
confirmButtonText: 'Tutup',
|
||||
width: '600px',
|
||||
customClass: {
|
||||
confirmButton: 'px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Dropdown menu handler
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.dropdown')) {
|
||||
document.querySelectorAll('.dropdown-menu').forEach(menu => {
|
||||
menu.classList.add('hidden');
|
||||
// Confirm deletion
|
||||
function confirmDelete(id) {
|
||||
Swal.fire({
|
||||
title: 'Hapus Pengumuman?',
|
||||
text: "Data yang dihapus tidak dapat dikembalikan!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#3085d6',
|
||||
confirmButtonText: 'Ya, Hapus!',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajax({
|
||||
url: `/admin/announcement/${id}`,
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
},
|
||||
success: function() {
|
||||
Swal.fire({
|
||||
title: 'Terhapus!',
|
||||
text: 'Pengumuman telah dihapus.',
|
||||
icon: 'success',
|
||||
timer: 1500,
|
||||
timerProgressBar: true,
|
||||
showConfirmButton: false,
|
||||
willClose: () => {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function() {
|
||||
Swal.fire({
|
||||
title: 'Gagal!',
|
||||
text: 'Terjadi kesalahan saat menghapus pengumuman.',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'Tutup'
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const dropdown = e.target.closest('.dropdown');
|
||||
const menu = dropdown.querySelector('.dropdown-menu');
|
||||
menu.classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Date range validation
|
||||
document.getElementById('filterForm').addEventListener('submit', function(e) {
|
||||
const startDate = new Date(document.getElementById('start_date').value);
|
||||
const endDate = new Date(document.getElementById('end_date').value);
|
||||
|
||||
if (document.getElementById('start_date').value && document.getElementById('end_date').value && startDate > endDate) {
|
||||
e.preventDefault();
|
||||
Swal.fire({
|
||||
title: 'Tanggal Tidak Valid',
|
||||
text: 'Tanggal akhir tidak boleh sebelum tanggal awal',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'Tutup'
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
|
@ -1,491 +1,537 @@
|
|||
@extends('layouts.dashboard')
|
||||
|
||||
@section('title', 'Sistem Pengumuman Digital')
|
||||
@section('title', 'Pengumuman Sekolah - Smart School')
|
||||
|
||||
@section('content')
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- Header -->
|
||||
<div class="bg-white shadow-sm">
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">Sistem Pengumuman Digital</h1>
|
||||
<p class="text-gray-600 mt-1">Kontrol Terpusat untuk Ruangan dan Pengumuman</p>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Header Section -->
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
|
||||
<div>
|
||||
<div class="flex items-center mb-2">
|
||||
<h1 class="text-3xl font-bold text-gray-800">Sistem Pengumuman Sekolah</h1>
|
||||
<span class="ml-3 px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 flex items-center">
|
||||
<span class="w-2 h-2 rounded-full bg-blue-500 mr-2"></span>
|
||||
LIVE CONTROL
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-600">Kelola pengumuman manual dan TTS untuk seluruh ruangan sekolah</p>
|
||||
|
||||
<div class="flex items-center mt-4 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<span class="relative flex h-3 w-3 mr-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full {{ $mqttStatus ? 'bg-green-400' : 'bg-red-400' }} opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 {{ $mqttStatus ? 'bg-green-500' : 'bg-red-500' }}"></span>
|
||||
</span>
|
||||
<span class="text-sm font-medium {{ $mqttStatus ? 'text-green-700' : 'text-red-700' }}">
|
||||
MQTT: {{ $mqttStatus ? 'Connected' : 'Disconnected' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-4 md:mt-0 flex items-center space-x-4">
|
||||
<div class="flex items-center bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200">
|
||||
<div class="w-3 h-3 rounded-full mr-2 {{ $mqttStatus === 'Connected' ? 'bg-green-500' : 'bg-red-500' }}"></div>
|
||||
<span class="text-sm font-medium">MQTT: {{ $mqttStatus }}</span>
|
||||
</div>
|
||||
<button onclick="window.location.reload()" class="p-2 text-gray-500 hover:text-blue-600 transition-colors">
|
||||
<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="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<i class="fas fa-door-open mr-2 text-blue-500"></i>
|
||||
<span class="font-medium">{{ $ruangans->count() }}</span> Ruangan Terdaftar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('admin.announcement.history') }}"
|
||||
class="flex items-center px-5 py-2.5 bg-white border border-blue-500 text-blue-600 rounded-lg hover:bg-blue-50 transition-all duration-200 shadow-sm hover:shadow-md">
|
||||
<i class="fas fa-history mr-2"></i> Riwayat Pengumuman
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Mode Selection Card -->
|
||||
<div class="bg-white rounded-xl shadow-md overflow-hidden mb-8">
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="flex -mb-px">
|
||||
<button id="reguler-tab" class="tab-button active" data-tab="reguler">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Kontrol Relay
|
||||
</button>
|
||||
<button id="tts-tab" class="tab-button" data-tab="tts">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Pengumuman Suara
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- Reguler Tab Content -->
|
||||
<div id="reguler-content" class="tab-content active">
|
||||
<form id="reguler-form" action="{{ route('announcement.store') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="mode" value="reguler">
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Ruangan Selection -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Pilih Ruangan</h2>
|
||||
<button type="button" id="selectAllBtn" class="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline mr-1" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Pilih Semua
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-96 overflow-y-auto p-2">
|
||||
@foreach($ruangans as $ruangan)
|
||||
<div class="flex items-center p-3 hover:bg-gray-100 rounded-lg transition">
|
||||
<input id="ruangan-{{ $ruangan->id }}" name="ruangans[]" type="checkbox"
|
||||
value="{{ $ruangan->id }}"
|
||||
class="room-checkbox h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<label for="ruangan-{{ $ruangan->id }}" class="ml-3 flex-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="block text-gray-800 font-medium">{{ $ruangan->nama_ruangan }}</span>
|
||||
<span class="relay-status text-xs px-2 py-1 rounded-full
|
||||
{{ $ruangan->relay_state === 'ON' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' }}">
|
||||
{{ $ruangan->relay_state ?? 'OFF' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="block text-xs text-gray-500 mt-1">
|
||||
{{ $ruangan->kelas->nama_kelas ?? '-' }} •
|
||||
{{ $ruangan->jurusan->nama_jurusan ?? '-' }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@error('ruangans')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Relay Control Panel -->
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Kontrol Relay</h2>
|
||||
<div class="bg-white p-5 rounded-lg border border-gray-200 shadow-sm">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center p-3 rounded-lg bg-blue-50 border border-blue-100">
|
||||
<input id="relay-on" name="relay_action" type="radio" value="ON" checked
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500">
|
||||
<label for="relay-on" class="ml-3 flex items-center">
|
||||
<span class="w-3 h-3 rounded-full bg-green-500 mr-3"></span>
|
||||
<span class="text-gray-700 font-medium">Aktifkan Relay</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center p-3 rounded-lg bg-red-50 border border-red-100">
|
||||
<input id="relay-off" name="relay_action" type="radio" value="OFF"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500">
|
||||
<label for="relay-off" class="ml-3 flex items-center">
|
||||
<span class="w-3 h-3 rounded-full bg-red-500 mr-3"></span>
|
||||
<span class="text-gray-700 font-medium">Nonaktifkan Relay</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-5 border-t border-gray-200">
|
||||
<button type="submit"
|
||||
class="w-full flex justify-center items-center px-4 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-medium rounded-lg hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all shadow-md">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" />
|
||||
</svg>
|
||||
Eksekusi Perintah
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- TTS Tab Content -->
|
||||
<div id="tts-content" class="tab-content hidden">
|
||||
<form id="tts-form" action="{{ route('announcement.store') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="mode" value="tts">
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div class="lg:col-span-2">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Buat Pengumuman Suara</h2>
|
||||
|
||||
<!-- TTS Editor -->
|
||||
<div class="mb-6">
|
||||
<label for="tts_text" class="block text-gray-700 font-medium mb-2">Teks Pengumuman</label>
|
||||
<div class="relative">
|
||||
<textarea id="tts_text" name="tts_text" rows="6"
|
||||
class="w-full px-4 py-3 text-gray-700 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 shadow-sm"
|
||||
placeholder="Ketikkan teks pengumuman di sini..."></textarea>
|
||||
<div class="absolute bottom-3 right-3 text-xs text-gray-400" id="charCount">0/1000 karakter</div>
|
||||
</div>
|
||||
@error('tts_text')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Voice Settings -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<div>
|
||||
<label for="tts_voice" class="block text-gray-700 font-medium mb-2">Jenis Suara</label>
|
||||
<div class="relative">
|
||||
<select id="tts_voice" name="tts_voice"
|
||||
class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 rounded-lg shadow-sm">
|
||||
<option value="id-id">Bahasa Indonesia</option>
|
||||
<option value="en-us">English (US)</option>
|
||||
<option value="en-gb">English (UK)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tts_speed" class="block text-gray-700 font-medium mb-2">Kecepatan Bicara</label>
|
||||
<div class="px-2">
|
||||
<input type="range" id="tts_speed" name="tts_speed" min="-10" max="10" value="0"
|
||||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<div class="flex justify-between text-xs text-gray-500 mt-1 px-1">
|
||||
<span>Lambat</span>
|
||||
<span>Normal</span>
|
||||
<span>Cepat</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Section -->
|
||||
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="text-sm font-medium text-gray-700">Preview Suara</h3>
|
||||
<button type="button" id="previewBtn"
|
||||
class="inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Generate Preview
|
||||
</button>
|
||||
</div>
|
||||
<div id="previewContainer" class="hidden">
|
||||
<audio id="previewAudio" controls class="w-full mt-2"></audio>
|
||||
<div id="previewLoading" class="mt-3 text-center py-4 hidden">
|
||||
<svg class="animate-spin mx-auto h-5 w-5 text-blue-500" 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>
|
||||
<p class="mt-2 text-sm text-gray-500">Sedang memproses suara...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ruangan Selection -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Ruangan Tujuan</h2>
|
||||
<button type="button" id="selectAllTtsBtn" class="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline mr-1" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Pilih Semua
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200 max-h-96 overflow-y-auto">
|
||||
@foreach($ruangans as $ruangan)
|
||||
<div class="flex items-start mb-3">
|
||||
<div class="flex items-center h-5 mt-1">
|
||||
<input id="tts-ruangan-{{ $ruangan->id }}" name="ruangans[]" type="checkbox"
|
||||
value="{{ $ruangan->id }}"
|
||||
class="tts-room-checkbox h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
</div>
|
||||
<label for="tts-ruangan-{{ $ruangan->id }}" class="ml-3">
|
||||
<span class="block text-gray-800 font-medium">{{ $ruangan->nama_ruangan }}</span>
|
||||
<span class="block text-xs text-gray-500">
|
||||
{{ $ruangan->kelas->nama_kelas ?? '-' }} •
|
||||
{{ $ruangan->jurusan->nama_jurusan ?? '-' }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
@error('ruangans')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
|
||||
<div class="mt-6 pt-5 border-t border-gray-200">
|
||||
<button type="submit"
|
||||
class="w-full flex justify-center items-center px-4 py-3 bg-gradient-to-r from-purple-600 to-purple-700 text-white font-medium rounded-lg hover:from-purple-700 hover:to-purple-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all shadow-md">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" />
|
||||
</svg>
|
||||
Kirim Pengumuman
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Announcements -->
|
||||
<div class="mt-8 bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Riwayat Pengumuman Terakhir</h2>
|
||||
</div>
|
||||
<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">Waktu</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Mode</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Konten</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse($announcements as $announcement)
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $announcement->sent_at->format('d M Y') }}</div>
|
||||
<div class="text-sm text-gray-500">{{ $announcement->sent_at->format('H:i') }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2.5 py-0.5 inline-flex items-center text-xs font-medium rounded-full
|
||||
{{ $announcement->mode === 'reguler' ? 'bg-blue-100 text-blue-800' : 'bg-purple-100 text-purple-800' }}">
|
||||
{{ $announcement->mode === 'reguler' ? 'Kontrol Relay' : 'Pengumuman Suara' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
@if($announcement->mode === 'reguler')
|
||||
{{ $announcement->message }}
|
||||
<span class="text-xs font-normal ml-2 {{ $announcement->relay_state === 'ON' ? 'text-green-600' : 'text-red-600' }}">
|
||||
(Relay: {{ $announcement->relay_state }})
|
||||
</span>
|
||||
@else
|
||||
{{ Str::limit($announcement->message, 50) }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
Ruangan: {{ $announcement->ruangans->pluck('nama_ruangan')->implode(', ') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<span class="px-2.5 py-0.5 inline-flex text-xs font-medium rounded-full
|
||||
{{ $announcement->status === 'delivered' ? 'bg-green-100 text-green-800' : ($announcement->status === 'failed' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800') }}">
|
||||
{{ ucfirst($announcement->status) }}
|
||||
</span>
|
||||
@if($announcement->error_message)
|
||||
<button onclick="showErrorModal('{{ $announcement->error_message }}')" class="ml-2 text-gray-400 hover:text-gray-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-4 text-center text-sm text-gray-500">
|
||||
Belum ada riwayat pengumuman
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50">
|
||||
{{ $announcements->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Modal -->
|
||||
<div id="errorModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
|
||||
<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>
|
||||
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-red-600" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">Error Details</h3>
|
||||
<div class="mt-2">
|
||||
<p id="errorModalContent" class="text-sm text-gray-500"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button type="button" onclick="closeErrorModal()" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Tutup
|
||||
<!-- Main Card -->
|
||||
<div class="bg-white rounded-xl shadow-md overflow-hidden border border-gray-200">
|
||||
<!-- Tab Navigation -->
|
||||
<div class="border-b border-gray-200 bg-gradient-to-r from-blue-50 to-gray-50">
|
||||
<nav class="flex">
|
||||
<button id="manualTabBtn" class="px-6 py-4 font-medium text-sm border-b-2 border-blue-600 text-blue-600 focus:outline-none transition-colors flex items-center">
|
||||
<i class="fas fa-microphone-alt mr-2"></i> Mode Manual
|
||||
</button>
|
||||
<button id="ttsTabBtn" class="px-6 py-4 font-medium text-sm border-b-2 border-transparent text-gray-500 hover:text-gray-700 focus:outline-none transition-colors flex items-center">
|
||||
<i class="fas fa-robot mr-2"></i> Text-to-Speech
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="p-6">
|
||||
<!-- Manual Mode Tab -->
|
||||
<div id="manualTab" class="tab-content">
|
||||
<form id="manualForm" action="{{ route('admin.announcement.control-relay') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="mode" value="manual">
|
||||
<input type="hidden" id="actionType" name="action" value="activate">
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="p-2 rounded-lg bg-blue-100 text-blue-800 mr-3">
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-800">Kontrol Relay Manual</h3>
|
||||
<p class="text-gray-600 text-sm">Aktifkan atau nonaktifkan relay untuk ruangan tertentu</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="block text-sm font-medium text-gray-700">Pilih Ruangan <span class="text-red-500">*</span></label>
|
||||
<button type="button" id="selectAllManual" class="text-xs font-medium text-blue-600 hover:text-blue-800 transition-colors flex items-center">
|
||||
<i class="fas fa-check-circle mr-1"></i> Pilih Semua
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@foreach($ruangans as $ruangan)
|
||||
<div class="relative flex items-start p-3 rounded-lg border border-gray-200 hover:border-blue-300 transition-colors">
|
||||
<div class="flex items-center h-5 mt-1">
|
||||
<input id="manual-ruang-{{ $ruangan->id }}" name="ruangans[]"
|
||||
type="checkbox" value="{{ $ruangan->id }}"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
</div>
|
||||
<div class="ml-3 text-sm flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<label for="manual-ruang-{{ $ruangan->id }}" class="font-medium text-gray-700 flex items-center">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-blue-500 mr-2"></span>
|
||||
{{ $ruangan->nama_ruangan }}
|
||||
</label>
|
||||
<span id="status-ruang-{{ $ruangan->id }}" class="text-xs px-2 py-0.5 rounded-full
|
||||
{{ $ruangan->relay_state === 'on' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' }}">
|
||||
{{ $ruangan->relay_state === 'on' ? 'AKTIF' : 'NONAKTIF' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1 flex items-center">
|
||||
<i class="fas fa-map-marker-alt mr-1 text-gray-400"></i>
|
||||
{{ $ruangan->lokasi }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div id="ruanganError" class="mt-2 text-sm text-red-600 hidden flex items-center">
|
||||
<i class="fas fa-exclamation-circle mr-1"></i> Pilih minimal satu ruangan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-100">
|
||||
<button type="button" id="resetManual" class="inline-flex items-center px-4 py-2.5 border border-gray-300 shadow-sm text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200">
|
||||
<i class="fas fa-redo mr-2"></i> Reset
|
||||
</button>
|
||||
<button type="submit" id="toggleRelayBtn" class="inline-flex items-center px-6 py-2.5 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200">
|
||||
<i class="fas fa-broadcast-tower mr-2"></i> Aktifkan Relay
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- TTS Mode Tab -->
|
||||
<div id="ttsTab" class="tab-content hidden">
|
||||
<form id="ttsForm" action="{{ route('admin.announcement.store') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="mode" value="tts">
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="p-2 rounded-lg bg-green-100 text-green-800 mr-3">
|
||||
<i class="fas fa-robot"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-800">Pengumuman Suara (TTS)</h3>
|
||||
<p class="text-gray-600 text-sm">Masukkan teks yang akan diubah menjadi suara dan pilih ruangan tujuan</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="message" class="block text-sm font-medium text-gray-700 mb-2">Isi Pengumuman <span class="text-red-500">*</span></label>
|
||||
<div class="relative">
|
||||
<textarea id="message" name="message" rows="5"
|
||||
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm placeholder-gray-400"
|
||||
placeholder="Contoh: Selamat pagi siswa-siswi, harap berkumpul di lapangan upacara untuk mengikuti kegiatan hari ini">{{ old('message') }}</textarea>
|
||||
<div class="absolute bottom-3 right-3 bg-white px-2 py-1 rounded text-xs text-gray-500 border border-gray-200">
|
||||
<span id="charCount">0</span>/500
|
||||
</div>
|
||||
</div>
|
||||
<div id="messageError" class="mt-1 text-sm text-red-600 hidden flex items-center">
|
||||
<i class="fas fa-exclamation-circle mr-1"></i> Isi pengumuman diperlukan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Pilih Ruangan <span class="text-red-500">*</span></label>
|
||||
<button type="button" id="selectAllTTS" class="text-sm font-medium text-blue-600 hover:text-blue-800 transition-colors flex items-center">
|
||||
<i class="fas fa-check-circle mr-1"></i> Pilih Semua
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@foreach($ruangans as $ruangan)
|
||||
<div class="relative flex items-start p-3 rounded-lg border border-gray-200 hover:border-green-300 transition-colors">
|
||||
<div class="flex items-center h-5 mt-1">
|
||||
<input id="tts-ruang-{{ $ruangan->id }}" name="ruangans[]"
|
||||
type="checkbox" value="{{ $ruangan->id }}"
|
||||
class="focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300 rounded">
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="tts-ruang-{{ $ruangan->id }}" class="font-medium text-gray-700 flex items-center">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-green-500 mr-2"></span>
|
||||
{{ $ruangan->nama_ruangan }}
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 mt-1 flex items-center">
|
||||
<i class="fas fa-map-marker-alt mr-1 text-gray-400"></i>
|
||||
{{ $ruangan->lokasi }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div id="ttsRuanganError" class="mt-2 text-sm text-red-600 hidden flex items-center">
|
||||
<i class="fas fa-exclamation-circle mr-1"></i> Pilih minimal satu ruangan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-4 pt-4 border-t border-gray-100">
|
||||
<button type="button" id="resetTTS" class="bg-gray-100 hover:bg-gray-200 text-gray-800 py-2.5 px-6 rounded-lg text-sm font-medium transition-all duration-200 shadow-sm flex items-center">
|
||||
<i class="fas fa-redo mr-2"></i> Reset Form
|
||||
</button>
|
||||
<button type="submit" class="bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white py-2.5 px-6 rounded-lg text-sm font-medium transition-all duration-200 shadow-md hover:shadow-lg flex items-center">
|
||||
<i class="fas fa-play mr-2"></i> Kirim Pengumuman
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Tab Switching
|
||||
const tabs = document.querySelectorAll('.tab-button');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
// Update tab buttons
|
||||
tabs.forEach(t => t.classList.remove('active', 'border-blue-500', 'text-blue-600'));
|
||||
tab.classList.add('active', 'border-blue-500', 'text-blue-600');
|
||||
$(document).ready(function() {
|
||||
// Tab switching functionality
|
||||
function switchTab(activeTab, inactiveTab, activeBtn, inactiveBtn) {
|
||||
activeTab.classList.remove('hidden');
|
||||
inactiveTab.classList.add('hidden');
|
||||
activeBtn.classList.add('border-blue-600', 'text-blue-600');
|
||||
activeBtn.classList.remove('border-transparent', 'text-gray-500');
|
||||
inactiveBtn.classList.remove('border-blue-600', 'text-blue-600');
|
||||
inactiveBtn.classList.add('border-transparent', 'text-gray-500');
|
||||
}
|
||||
|
||||
// Update tab contents
|
||||
const tabId = tab.getAttribute('data-tab');
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.add('hidden');
|
||||
content.classList.remove('active');
|
||||
$('#manualTabBtn').click(function() {
|
||||
switchTab(document.getElementById('manualTab'), document.getElementById('ttsTab'),
|
||||
document.getElementById('manualTabBtn'), document.getElementById('ttsTabBtn'));
|
||||
});
|
||||
|
||||
$('#ttsTabBtn').click(function() {
|
||||
switchTab(document.getElementById('ttsTab'), document.getElementById('manualTab'),
|
||||
document.getElementById('ttsTabBtn'), document.getElementById('manualTabBtn'));
|
||||
});
|
||||
|
||||
// Character counter for TTS message
|
||||
$('#message').on('input', function() {
|
||||
const maxLength = 500;
|
||||
const currentLength = $(this).val().length;
|
||||
$('#charCount').text(currentLength);
|
||||
|
||||
if (currentLength > maxLength) {
|
||||
$(this).addClass('border-red-300');
|
||||
$('#charCount').addClass('text-red-600');
|
||||
} else {
|
||||
$(this).removeClass('border-red-300');
|
||||
$('#charCount').removeClass('text-red-600');
|
||||
}
|
||||
});
|
||||
|
||||
// Select all checkboxes
|
||||
$('#selectAllManual').click(function() {
|
||||
const allChecked = $('#manualTab input[type="checkbox"]').length === $('#manualTab input[type="checkbox"]:checked').length;
|
||||
$('#manualTab input[type="checkbox"]').prop('checked', !allChecked);
|
||||
|
||||
// Update button text
|
||||
$(this).html(`<i class="fas ${!allChecked ? 'fa-check-circle' : 'fa-times-circle'} mr-1"></i> ${!allChecked ? 'Pilih Semua' : 'Batal Pilih'}`);
|
||||
});
|
||||
|
||||
$('#selectAllTTS').click(function() {
|
||||
const allChecked = $('#ttsTab input[type="checkbox"]').length === $('#ttsTab input[type="checkbox"]:checked').length;
|
||||
$('#ttsTab input[type="checkbox"]').prop('checked', !allChecked);
|
||||
|
||||
// Update button text
|
||||
$(this).html(`<i class="fas ${!allChecked ? 'fa-check-circle' : 'fa-times-circle'} mr-1"></i> ${!allChecked ? 'Pilih Semua' : 'Batal Pilih'}`);
|
||||
});
|
||||
|
||||
// Reset forms
|
||||
$('#resetManual').click(function() {
|
||||
$('#manualForm')[0].reset();
|
||||
$('#ruanganError').addClass('hidden');
|
||||
$('#selectAllManual').html('<i class="fas fa-check-circle mr-1"></i> Pilih Semua');
|
||||
});
|
||||
|
||||
$('#resetTTS').click(function() {
|
||||
$('#ttsForm')[0].reset();
|
||||
$('#charCount').text('0');
|
||||
$('#messageError').addClass('hidden');
|
||||
$('#ttsRuanganError').addClass('hidden');
|
||||
$('#selectAllTTS').html('<i class="fas fa-check-circle mr-1"></i> Pilih Semua');
|
||||
});
|
||||
|
||||
// Form validation and submission
|
||||
$('#manualForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const form = this;
|
||||
const formData = new FormData(form);
|
||||
const rooms = formData.getAll('ruangans[]');
|
||||
|
||||
// Reset error states
|
||||
$('#ruanganError').addClass('hidden');
|
||||
|
||||
if (rooms.length === 0) {
|
||||
$('#ruanganError').removeClass('hidden');
|
||||
Swal.fire({
|
||||
title: 'Peringatan',
|
||||
text: 'Pilih minimal satu ruangan untuk mengontrol relay',
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'Mengerti',
|
||||
customClass: {
|
||||
confirmButton: 'px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'
|
||||
}
|
||||
});
|
||||
document.getElementById(`${tabId}-content`).classList.remove('hidden');
|
||||
document.getElementById(`${tabId}-content`).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Select All Rooms - Reguler
|
||||
document.getElementById('selectAllBtn').addEventListener('click', function() {
|
||||
const checkboxes = document.querySelectorAll('.room-checkbox');
|
||||
const allChecked = Array.from(checkboxes).every(checkbox => checkbox.checked);
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = !allChecked;
|
||||
});
|
||||
|
||||
this.innerHTML = allChecked ?
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline mr-1" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /></svg>Pilih Semua' :
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline mr-1" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>Batal Pilih';
|
||||
});
|
||||
|
||||
// Select All Rooms - TTS
|
||||
document.getElementById('selectAllTtsBtn').addEventListener('click', function() {
|
||||
const checkboxes = document.querySelectorAll('.tts-room-checkbox');
|
||||
const allChecked = Array.from(checkboxes).every(checkbox => checkbox.checked);
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = !allChecked;
|
||||
});
|
||||
|
||||
this.innerHTML = allChecked ?
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline mr-1" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /></svg>Pilih Semua' :
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline mr-1" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>Batal Pilih';
|
||||
});
|
||||
|
||||
// Character Count for TTS
|
||||
document.getElementById('tts_text').addEventListener('input', function() {
|
||||
const count = this.value.length;
|
||||
document.getElementById('charCount').textContent = `${count}/1000 karakter`;
|
||||
});
|
||||
|
||||
// TTS Preview
|
||||
document.getElementById('previewBtn').addEventListener('click', function() {
|
||||
const text = document.getElementById('tts_text').value;
|
||||
const voice = document.getElementById('tts_voice').value;
|
||||
const speed = document.getElementById('tts_speed').value;
|
||||
const previewContainer = document.getElementById('previewContainer');
|
||||
const previewAudio = document.getElementById('previewAudio');
|
||||
const previewLoading = document.getElementById('previewLoading');
|
||||
|
||||
if (!text) {
|
||||
alert('Masukkan teks terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading
|
||||
previewContainer.classList.remove('hidden');
|
||||
previewAudio.classList.add('hidden');
|
||||
previewLoading.classList.remove('hidden');
|
||||
// Toggle relay state
|
||||
isRelayActive = !isRelayActive;
|
||||
formData.set('action', isRelayActive ? 'activate' : 'deactivate');
|
||||
updateButtonState();
|
||||
|
||||
fetch("{{ route('announcement.tts-preview') }}", {
|
||||
// Show loading state
|
||||
const submitBtn = $('#toggleRelayBtn');
|
||||
const originalBtnText = submitBtn.html();
|
||||
submitBtn.html('<i class="fas fa-circle-notch fa-spin mr-2"></i> Memproses...');
|
||||
submitBtn.prop('disabled', true);
|
||||
|
||||
// Submit data
|
||||
$.ajax({
|
||||
url: $(form).attr('action'),
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: text,
|
||||
voice: voice,
|
||||
speed: speed
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
previewContainer.classList.add('hidden');
|
||||
} else {
|
||||
previewAudio.src = data.audio_url;
|
||||
previewLoading.classList.add('hidden');
|
||||
previewAudio.classList.remove('hidden');
|
||||
previewAudio.play();
|
||||
success: function(response) {
|
||||
// Update room status indicators
|
||||
rooms.forEach(roomId => {
|
||||
const statusElement = $(`#status-ruang-${roomId}`);
|
||||
statusElement.text(isRelayActive ? 'AKTIF' : 'NONAKTIF');
|
||||
statusElement.removeClass('bg-gray-100 text-gray-800 bg-green-100 text-green-800')
|
||||
.addClass(isRelayActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800');
|
||||
});
|
||||
|
||||
// Show success notification
|
||||
const action = isRelayActive ? 'diaktifkan' : 'dinonaktifkan';
|
||||
Swal.fire({
|
||||
title: 'Berhasil!',
|
||||
text: `Relay berhasil ${action}`,
|
||||
icon: 'success',
|
||||
timer: 2000,
|
||||
timerProgressBar: true,
|
||||
showConfirmButton: false,
|
||||
position: 'top-end',
|
||||
toast: true,
|
||||
background: '#f0fdf4',
|
||||
iconColor: '#10b981'
|
||||
});
|
||||
},
|
||||
error: function(xhr) {
|
||||
// Revert state on error
|
||||
isRelayActive = !isRelayActive;
|
||||
updateButtonState();
|
||||
|
||||
let errorMessage = 'Terjadi kesalahan saat mengontrol relay';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
Swal.fire({
|
||||
title: 'Gagal!',
|
||||
text: errorMessage,
|
||||
icon: 'error',
|
||||
confirmButtonText: 'Tutup',
|
||||
customClass: {
|
||||
confirmButton: 'px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'
|
||||
}
|
||||
});
|
||||
},
|
||||
complete: function() {
|
||||
submitBtn.html(originalBtnText);
|
||||
submitBtn.prop('disabled', false);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Gagal membuat preview suara');
|
||||
previewContainer.classList.add('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize first tab
|
||||
document.querySelector('.tab-button').click();
|
||||
$('#ttsForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const form = this;
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Reset error states
|
||||
$('#messageError').addClass('hidden');
|
||||
$('#ttsRuanganError').addClass('hidden');
|
||||
|
||||
// Client-side validation
|
||||
if (!formData.get('message')) {
|
||||
$('#messageError').removeClass('hidden');
|
||||
Swal.fire({
|
||||
title: 'Peringatan',
|
||||
text: 'Isi pengumuman diperlukan untuk mode TTS',
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'Mengerti',
|
||||
customClass: {
|
||||
confirmButton: 'px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('#message').val().length > 500) {
|
||||
Swal.fire({
|
||||
title: 'Peringatan',
|
||||
text: 'Isi pengumuman melebihi 500 karakter',
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'Mengerti',
|
||||
customClass: {
|
||||
confirmButton: 'px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const rooms = formData.getAll('ruangans[]');
|
||||
if (rooms.length === 0) {
|
||||
$('#ttsRuanganError').removeClass('hidden');
|
||||
Swal.fire({
|
||||
title: 'Peringatan',
|
||||
text: 'Pilih minimal satu ruangan',
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'Mengerti',
|
||||
customClass: {
|
||||
confirmButton: 'px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const submitBtn = $(form).find('button[type="submit"]');
|
||||
const originalBtnText = submitBtn.html();
|
||||
submitBtn.html('<i class="fas fa-circle-notch fa-spin mr-2"></i> Mengirim...');
|
||||
submitBtn.prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: $(form).attr('action'),
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
Swal.fire({
|
||||
title: 'Berhasil!',
|
||||
text: 'Pengumuman TTS berhasil dikirim',
|
||||
icon: 'success',
|
||||
timer: 2000,
|
||||
timerProgressBar: true,
|
||||
showConfirmButton: false,
|
||||
position: 'top-end',
|
||||
toast: true,
|
||||
background: '#f0fdf4',
|
||||
iconColor: '#10b981',
|
||||
willClose: () => {
|
||||
window.location.href = "{{ route('admin.announcement.history') }}";
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let errorMessage = 'Pengumuman TTS gagal dikirim';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
Swal.fire({
|
||||
title: 'Gagal!',
|
||||
text: errorMessage,
|
||||
icon: 'error',
|
||||
confirmButtonText: 'Tutup',
|
||||
customClass: {
|
||||
confirmButton: 'px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'
|
||||
}
|
||||
});
|
||||
},
|
||||
complete: function() {
|
||||
submitBtn.html(originalBtnText);
|
||||
submitBtn.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Check relay status periodically
|
||||
function checkRelayStatus() {
|
||||
$.get("{{ route('admin.announcement.relay-status') }}", function(data) {
|
||||
data.forEach(room => {
|
||||
const statusElement = $(`#status-ruang-${room.id}`);
|
||||
if (statusElement.length) {
|
||||
statusElement.text(room.relay_state === 'on' ? 'AKTIF' : 'NONAKTIF');
|
||||
statusElement.removeClass('bg-gray-100 text-gray-800 bg-green-100 text-green-800')
|
||||
.addClass(room.relay_state === 'on' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Check MQTT connection status
|
||||
function checkMqttStatus() {
|
||||
$.get("{{ route('admin.check.mqtt') }}", function(data) {
|
||||
const statusElement = $('.mqtt-status-indicator');
|
||||
const textElement = $('.mqtt-status-text');
|
||||
|
||||
if (data.connected) {
|
||||
statusElement.removeClass('bg-red-500').addClass('bg-green-500');
|
||||
textElement.removeClass('text-red-600').addClass('text-green-600');
|
||||
textElement.text('MQTT: Connected');
|
||||
} else {
|
||||
statusElement.removeClass('bg-green-500').addClass('bg-red-500');
|
||||
textElement.removeClass('text-green-600').addClass('text-red-600');
|
||||
textElement.text('MQTT: Disconnected');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update button state based on relay status
|
||||
function updateButtonState() {
|
||||
const btn = $('#toggleRelayBtn');
|
||||
if (isRelayActive) {
|
||||
btn.html('<i class="fas fa-broadcast-tower mr-2"></i> Matikan Relay');
|
||||
btn.removeClass('from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800')
|
||||
.addClass('from-red-600 to-red-700 hover:from-red-700 hover:to-red-800');
|
||||
} else {
|
||||
btn.html('<i class="fas fa-broadcast-tower mr-2"></i> Aktifkan Relay');
|
||||
btn.removeClass('from-red-600 to-red-700 hover:from-red-700 hover:to-red-800')
|
||||
.addClass('from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
let isRelayActive = false;
|
||||
updateButtonState();
|
||||
setInterval(checkRelayStatus, 10000);
|
||||
checkRelayStatus();
|
||||
setInterval(checkMqttStatus, 30000);
|
||||
});
|
||||
|
||||
// Error Modal Functions
|
||||
function showErrorModal(message) {
|
||||
document.getElementById('errorModalContent').textContent = message;
|
||||
document.getElementById('errorModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeErrorModal() {
|
||||
document.getElementById('errorModal').classList.add('hidden');
|
||||
}
|
||||
</script>
|
||||
|
||||
@endsection
|
|
@ -1,80 +0,0 @@
|
|||
@extends('layouts.dashboard')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<h2 class="fw-bold">Detail Pengumuman</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">Informasi Pengumuman</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Waktu:</strong> {{ $announcement->sent_at->format('d M Y H:i:s') }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Mode:</strong>
|
||||
<span class="badge bg-{{ $announcement->mode === 'reguler' ? 'primary' : 'success' }}">
|
||||
{{ strtoupper($announcement->mode) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<strong>Ruangan Tujuan:</strong>
|
||||
<div class="mt-2">
|
||||
@foreach($announcement->ruangans as $ruangan)
|
||||
<span class="badge bg-secondary mb-1">{{ $ruangan->nama_ruangan }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<strong>Isi Pengumuman:</strong>
|
||||
<div class="p-3 bg-light rounded mt-2">
|
||||
@if($announcement->mode === 'tts')
|
||||
<p>{{ $announcement->message }}</p>
|
||||
@if($announcement->audio_path)
|
||||
<audio controls class="w-100 mt-3">
|
||||
<source src="{{ $announcement->audio_url }}" type="audio/wav">
|
||||
Browser Anda tidak mendukung pemutar audio.
|
||||
</audio>
|
||||
@endif
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">
|
||||
<strong>Suara:</strong> {{ $announcement->voice }} |
|
||||
<strong>Kecepatan:</strong> {{ $announcement->speed }}
|
||||
</small>
|
||||
</div>
|
||||
@else
|
||||
<p>{{ $announcement->message }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<strong>Dibuat Oleh:</strong> {{ $announcement->user->name }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Dibuat Pada:</strong> {{ $announcement->created_at->format('d M Y H:i:s') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<a href="{{ route('admin.announcement.index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
|
@ -41,21 +41,6 @@ class="bg-gradient-to-r from-blue-600 to-blue-800 hover:from-blue-700 hover:to-b
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jurusan Aktif -->
|
||||
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-green-500 hover:shadow-md transition duration-300">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-green-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-sm font-medium text-gray-500">Jurusan Aktif</h3>
|
||||
<p class="text-2xl font-bold text-gray-700">{{ $jurusan->count() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
|
@ -123,15 +108,9 @@ class="block w-full md:w-64 rounded-md border-gray-300 shadow-sm focus:border-bl
|
|||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900">{{ $item->nama_jurusan }}</div>
|
||||
<!-- <div class="text-xs text-gray-500">Kode: {{ $item->kode_jurusan ?? '-' }}</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- <td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||
{{ $item->ruangan_count ?? 0 }} Ruangan
|
||||
</span>
|
||||
</td> -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="{{ route('admin.jurusan.edit', $item->id) }}"
|
||||
|
|
|
@ -43,36 +43,6 @@ class="bg-gradient-to-r from-blue-600 to-blue-800 hover:from-blue-700 hover:to-b
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kelas Aktif -->
|
||||
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-green-500 hover:shadow-md transition duration-300">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-green-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-sm font-medium text-gray-500">Kelas Aktif</h3>
|
||||
<p class="text-2xl font-bold text-gray-700">{{ $kelas->count() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kelas per Jurusan -->
|
||||
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-purple-500 hover:shadow-md transition duration-300">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-purple-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-sm font-medium text-gray-500">Jurusan Terdaftar</h3>
|
||||
<p class="text-2xl font-bold text-gray-700">{{ $kelas->unique('jurusan_id')->count() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
|
|
|
@ -41,36 +41,6 @@ class="bg-gradient-to-r from-blue-600 to-blue-800 hover:from-blue-700 hover:to-b
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ruangan Aktif -->
|
||||
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-green-500 hover:shadow-md transition duration-300">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-green-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-sm font-medium text-gray-500">Ruangan Tersedia</h3>
|
||||
<p class="text-2xl font-bold text-gray-700">{{ $ruangan->count() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ruangan per Jurusan -->
|
||||
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-purple-500 hover:shadow-md transition duration-300">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-purple-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-sm font-medium text-gray-500">Jurusan Terdaftar</h3>
|
||||
<p class="text-2xl font-bold text-gray-700">{{ $ruangan->unique('jurusan_id')->count() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
|
@ -138,8 +108,7 @@ class="block w-full md:w-64 rounded-md border-gray-300 shadow-sm focus:border-bl
|
|||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900">{{ $item->nama_ruangan }}</div>
|
||||
<div class="text-xs text-gray-500">Kapasitas: 30 siswa</div>
|
||||
<div class="text-sm font-semibold text-gray-900">ruangan {{ $item->nama_ruangan }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -106,11 +106,20 @@
|
|||
});
|
||||
});
|
||||
|
||||
// Announcement System
|
||||
// Announcement System
|
||||
Route::prefix('announcement')->controller(AnnouncementController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('admin.announcement.index');
|
||||
Route::get('/history', 'history')->name('admin.announcement.history');
|
||||
Route::post('/', 'store')->name('admin.announcement.store');
|
||||
Route::get('/{id}/details', 'details')->name('admin.announcement.details');
|
||||
Route::delete('/{id}', 'destroy')->name('admin.announcement.destroy');
|
||||
|
||||
// MQTT & Relay
|
||||
Route::get('/check/mqtt', 'checkMqtt')->name('admin.check.mqtt');
|
||||
Route::post('/control-relay', 'controlRelay')->name('admin.announcement.control-relay');
|
||||
Route::get('/relay-status', 'relayStatus')->name('admin.announcement.relay-status');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue