TKK_E3220375/app/Http/Controllers/belController.php

647 lines
22 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\JadwalBel;
use App\Models\Status;
use App\Models\BellHistory;
use App\Services\MqttService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class BelController extends Controller
{
protected $mqttService;
protected $mqttConfig;
protected const DAY_MAP = [
'Monday' => 'Senin',
'Tuesday' => 'Selasa',
'Wednesday' => 'Rabu',
'Thursday' => 'Kamis',
'Friday' => 'Jumat',
'Saturday' => 'Sabtu',
'Sunday' => 'Minggu'
];
protected const DAY_ORDER = [
'Senin' => 1,
'Selasa' => 2,
'Rabu' => 3,
'Kamis' => 4,
'Jumat' => 5,
'Sabtu' => 6,
'Minggu' => 7
];
public function __construct(MqttService $mqttService)
{
$this->mqttService = $mqttService;
$this->mqttConfig = config('mqtt');
$this->initializeMqttSubscriptions();
}
protected function initializeMqttSubscriptions(): void
{
try {
$topics = $this->mqttConfig['topics']['responses'];
$events = $this->mqttConfig['topics']['events'];
$this->mqttService->subscribe($topics['status'], fn($t, $m) => $this->handleStatusResponse($m));
$this->mqttService->subscribe($topics['ack'], fn($t, $m) => $this->handleAckResponse($m));
$this->mqttService->subscribe($events['bell_manual'], fn($t, $m) => $this->handleBellEvent($m, 'manual'));
$this->mqttService->subscribe($events['bell_schedule'], fn($t, $m) => $this->handleBellEvent($m, 'schedule'));
Log::info('Successfully subscribed to MQTT topics');
} catch (\Exception $e) {
Log::error('Failed to initialize MQTT subscriptions: ' . $e->getMessage());
}
}
protected function handleBellEvent(string $message, string $triggerType): void
{
Log::debug("Processing {$triggerType} bell", ['raw_message' => $message]);
try {
$data = json_decode($message, true, 512, JSON_THROW_ON_ERROR);
$requiredFields = ['hari', 'waktu', 'file_number'];
foreach ($requiredFields as $field) {
if (!isset($data[$field])) {
throw new \Exception("Field {$field} tidak ditemukan");
}
}
$history = BellHistory::create([
'hari' => $data['hari'],
'waktu' => $this->normalizeWaktu($data['waktu']),
'file_number' => $data['file_number'],
'trigger_type' => $triggerType,
'ring_time' => now(),
'volume' => $data['volume'] ?? 15,
'repeat' => $data['repeat'] ?? 1
]);
Log::info("Bell {$triggerType} tersimpan", [
'id' => $history->id,
'hari' => $data['hari'],
'file' => $data['file_number']
]);
} catch (\Exception $e) {
Log::error("Gagal menyimpan bell {$triggerType}", [
'error' => $e->getMessage(),
'message' => $message,
'trace' => $e->getTraceAsString()
]);
}
}
private function normalizeWaktu(?string $time): string
{
if (empty($time)) {
return '00:00:00';
}
$parts = explode(':', $time);
$hour = min(23, max(0, (int)($parts[0] ?? 0)));
$minute = min(59, max(0, (int)($parts[1] ?? 0)));
$second = min(59, max(0, (int)($parts[2] ?? 0)));
return sprintf('%02d:%02d:%02d', $hour, $minute, $second);
}
protected function handleStatusResponse(string $message): void
{
try {
$data = json_decode($message, true);
if (!is_array($data)) {
throw new \Exception('Invalid status data format');
}
$requiredKeys = ['rtc', 'dfplayer', 'rtc_time', 'last_communication', 'last_sync'];
foreach ($requiredKeys as $key) {
if (!array_key_exists($key, $data)) {
throw new \Exception("Missing required key: {$key}");
}
}
Status::updateOrCreate(
['id' => 1],
[
'rtc' => $data['rtc'],
'dfplayer' => $data['dfplayer'],
'rtc_time' => $data['rtc_time'],
'last_communication' => Carbon::createFromTimestamp($data['last_communication']),
'last_sync' => Carbon::createFromTimestamp($data['last_sync'])
]
);
} catch (\Exception $e) {
Log::error('Error handling status response: ' . $e->getMessage());
}
}
protected function handleAckResponse(string $message): void
{
try {
$data = json_decode($message, true);
if (!isset($data['action'])) {
return;
}
$action = $data['action'];
$message = $data['message'] ?? '';
if ($action === 'sync_ack') {
Status::updateOrCreate(['id' => 1], ['last_sync' => Carbon::now()]);
Log::info('Schedule sync acknowledged: ' . $message);
} elseif ($action === 'ring_ack') {
Log::info('Bell ring acknowledged: ' . $message);
}
} catch (\Exception $e) {
Log::error('Error handling ack response: ' . $e->getMessage());
}
}
public function index(Request $request)
{
try {
$query = JadwalBel::query();
if ($request->filled('hari')) {
$query->where('hari', $request->hari);
}
if ($request->filled('search')) {
$query->where(function($q) use ($request) {
$q->where('hari', 'like', '%'.$request->search.'%')
->orWhere('file_number', 'like', '%'.$request->search.'%');
});
}
$today = Carbon::now()->isoFormat('dddd');
$currentTime = Carbon::now()->format('H:i:s');
return view('admin.bel.index', [
'schedules' => $query->orderBy('hari')->orderBy('waktu')->paginate(10),
'todaySchedules' => JadwalBel::where('hari', $today)
->orderBy('waktu')
->get(),
'nextSchedule' => JadwalBel::where('hari', $today)
->where('waktu', '>', $currentTime)
->orderBy('waktu')
->first(),
'status' => Status::firstOrCreate(['id' => 1]),
'mqttStatus' => $this->getMqttStatus()
]);
} catch (\Exception $e) {
Log::error('Error in index method: ' . $e->getMessage());
return back()->with('error', 'Terjadi kesalahan saat memuat data jadwal');
}
}
protected function getMqttStatus(): string
{
try {
return $this->mqttService->isConnected() ? 'Connected' : 'Disconnected';
} catch (\Exception $e) {
Log::error('MQTT check failed: ' . $e->getMessage());
return 'Disconnected';
}
}
public function create()
{
return view('admin.bel.create', [
'days' => JadwalBel::DAYS,
'default_file' => '0001'
]);
}
public function store(Request $request)
{
$validated = $this->validateSchedule($request);
try {
$schedule = JadwalBel::create($validated);
$this->syncSchedules();
$this->logActivity('Jadwal dibuat', $schedule);
return redirect()
->route('bel.index')
->with([
'success' => 'Jadwal berhasil ditambahkan',
'scroll_to' => 'schedule-'.$schedule->id
]);
} catch (\Exception $e) {
Log::error('Gagal menambah jadwal: ' . $e->getMessage());
return back()->withInput()->with('error', 'Gagal menambah jadwal: ' . $e->getMessage());
}
}
public function edit($id)
{
return view('admin.bel.edit', [
'schedule' => JadwalBel::findOrFail($id),
'days' => JadwalBel::DAYS
]);
}
public function update(Request $request, $id)
{
$validated = $this->validateSchedule($request);
$schedule = JadwalBel::findOrFail($id);
try {
$schedule->update($validated);
$this->syncSchedules();
$this->logActivity('Jadwal diperbarui', $schedule);
return redirect()
->route('bel.index')
->with([
'success' => 'Jadwal berhasil diperbarui',
'scroll_to' => 'schedule-'.$schedule->id
]);
} catch (\Exception $e) {
Log::error('Gagal update jadwal ID '.$id.': ' . $e->getMessage());
return back()->withInput()->with('error', 'Gagal memperbarui jadwal: ' . $e->getMessage());
}
}
public function destroy($id)
{
try {
$schedule = JadwalBel::findOrFail($id);
$schedule->delete();
$this->syncSchedules();
$this->logActivity('Jadwal dihapus', $schedule);
return redirect()
->route('bel.index')
->with('success', 'Jadwal berhasil dihapus');
} catch (\Exception $e) {
Log::error('Gagal hapus jadwal ID '.$id.': ' . $e->getMessage());
return back()->with('error', 'Gagal menghapus jadwal: ' . $e->getMessage());
}
}
public function deleteAll()
{
try {
JadwalBel::truncate();
$this->syncSchedules();
return redirect()
->route('bel.index')
->with('success', 'Semua jadwal berhasil dihapus');
} catch (\Exception $e) {
Log::error('Gagal hapus semua jadwal: ' . $e->getMessage());
return back()->with('error', 'Gagal menghapus semua jadwal: ' . $e->getMessage());
}
}
public function toggleStatus($id)
{
try {
$schedule = JadwalBel::findOrFail($id);
$newStatus = !$schedule->is_active;
$schedule->update(['is_active' => $newStatus]);
return response()->json([
'success' => true,
'message' => 'Status jadwal berhasil diubah',
'is_active' => $newStatus
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengubah status: ' . $e->getMessage()
], 500);
}
}
public function activateAll()
{
try {
JadwalBel::query()->update(['is_active' => true]);
$this->syncSchedules();
return response()->json([
'success' => true,
'message' => 'Semua jadwal diaktifkan'
]);
} catch (\Exception $e) {
Log::error('Gagal aktifkan semua jadwal: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Gagal mengaktifkan semua jadwal'
], 500);
}
}
public function deactivateAll()
{
try {
JadwalBel::query()->update(['is_active' => false]);
$this->syncSchedules();
return response()->json([
'success' => true,
'message' => 'Semua jadwal dinonaktifkan'
]);
} catch (\Exception $e) {
Log::error('Gagal nonaktifkan semua jadwal: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Gagal menonaktifkan semua jadwal'
], 500);
}
}
public function ring(Request $request)
{
$validated = $request->validate([
'file_number' => 'required|string|size:4',
'volume' => 'sometimes|integer|min:1|max:30',
]);
// Konversi nama hari ke format enum Indonesia
$dayMap = [
'Monday' => 'Senin',
'Tuesday' => 'Selasa',
'Wednesday' => 'Rabu',
'Thursday' => 'Kamis',
'Friday' => 'Jumat',
'Saturday' => 'Sabtu',
'Sunday' => 'Minggu',
];
$hari = $dayMap[now()->format('l')]; // format('l') = nama hari dalam Bahasa Inggris
try {
$bellData = [
'hari' => $hari,
'waktu' => now()->format('H:i:s'),
'file_number' => $validated['file_number'],
'volume' => $validated['volume'],
];
BellHistory::create(array_merge($bellData, [
'trigger_type' => 'manual',
'ring_time' => now()
]));
$this->mqttService->publish(
$this->mqttConfig['topics']['commands']['ring'],
json_encode($validated),
1
);
return response()->json([
'success' => true,
'message' => 'Bel manual berhasil diaktifkan',
'data' => $bellData
]);
} catch (\Exception $e) {
Log::error('Gagal mengirim bel manual: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Gagal mengaktifkan bel: ' . $e->getMessage()
], 500);
}
}
public function status()
{
try {
$this->mqttService->publish(
$this->mqttConfig['topics']['commands']['status'],
json_encode([
'action' => 'get_status',
'timestamp' => Carbon::now()->toDateTimeString()
]),
1
);
$status = Status::firstOrCreate(['id' => 1]);
return response()->json([
'success' => true,
'message' => 'Permintaan status terkirim',
'data' => [
'rtc' => $status->rtc,
'dfplayer' => $status->dfplayer,
'rtc_time' => $status->rtc_time,
'last_communication' => $status->last_communication,
'last_sync' => $status->last_sync,
'mqtt_status' => $this->mqttService->isConnected(),
'status' => $status->status ?? 'unknown'
]
]);
} catch (\Exception $e) {
Log::error('Gagal meminta status: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Gagal meminta status perangkat: ' . $e->getMessage()
], 500);
}
}
public function syncSchedule()
{
try {
$schedules = JadwalBel::active()
->get()
->map(fn($item) => [
'hari' => $item->hari,
'waktu' => Carbon::parse($item->waktu)->format('H:i'),
'file_number' => $item->file_number
]);
$this->mqttService->publish(
$this->mqttConfig['topics']['commands']['sync'],
json_encode([
'action' => 'sync',
'timestamp' => Carbon::now()->toDateTimeString(),
'schedules' => $schedules
]),
1
);
Status::updateOrCreate(['id' => 1], ['last_sync' => Carbon::now()]);
return response()->json([
'success' => true,
'message' => 'Jadwal berhasil disinkronisasi',
'data' => [
'count' => $schedules->count(),
'last_sync' => Carbon::now()->toDateTimeString()
]
]);
} catch (\Exception $e) {
Log::error('Gagal sync jadwal: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Gagal menyinkronisasi jadwal: ' . $e->getMessage()
], 500);
}
}
protected function syncSchedules(): void
{
try {
$schedules = JadwalBel::active()
->get()
->map(fn($item) => [
'hari' => $item->hari,
'waktu' => Carbon::parse($item->waktu)->format('H:i:s'),
'file_number' => $item->file_number
]);
$this->mqttService->publish(
$this->mqttConfig['topics']['commands']['sync'],
json_encode([
'action' => 'sync',
'timestamp' => Carbon::now()->toDateTimeString(),
'schedules' => $schedules
]),
1
);
Log::info("Auto sync: " . count($schedules) . " jadwal");
} catch (\Exception $e) {
Log::error('Gagal auto sync: ' . $e->getMessage());
}
}
public function getNextSchedule()
{
$now = Carbon::now();
$currentDay = self::DAY_MAP[$now->format('l')] ?? $now->format('l');
$currentTime = $now->format('H:i:s');
// 1. Try to find today's upcoming ACTIVE schedules
$todaysSchedule = JadwalBel::where('is_active', true)
->where('hari', $currentDay)
->where('waktu', '>', $currentTime)
->orderBy('waktu')
->first();
if ($todaysSchedule) {
return $this->formatScheduleResponse($todaysSchedule);
}
// 2. If no more today, find the next ACTIVE schedule in the week
$allSchedules = JadwalBel::where('is_active', true)
->orderByRaw("FIELD(hari, 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu')")
->orderBy('waktu')
->get();
$currentDayValue = self::DAY_ORDER[$currentDay] ?? 0;
$closestSchedule = null;
$minDayDiff = 8;
foreach ($allSchedules as $schedule) {
$scheduleDayValue = self::DAY_ORDER[$schedule->hari] ?? 0;
$dayDiff = ($scheduleDayValue - $currentDayValue + 7) % 7;
if ($dayDiff === 0 && $schedule->waktu <= $currentTime) {
$dayDiff = 7;
}
if ($dayDiff < $minDayDiff) {
$minDayDiff = $dayDiff;
$closestSchedule = $schedule;
}
}
return $closestSchedule
? $this->formatScheduleResponse($closestSchedule)
: response()->json([
'success' => false,
'message' => 'Tidak ada jadwal aktif yang akan datang'
]);
}
private function formatScheduleResponse(JadwalBel $schedule)
{
return response()->json([
'success' => true,
'next_schedule' => [
'hari' => $schedule->hari,
'time' => $schedule->waktu,
'file_number' => $schedule->file_number,
'is_active' => $schedule->is_active
]
]);
}
// public function history(Request $request)
// {
// try {
// $query = BellHistory::query()->latest('ring_time');
// if ($request->filled('date')) {
// $query->whereDate('ring_time', $request->date);
// }
// if ($request->filled('search')) {
// $query->where(function($q) use ($request) {
// $q->where('hari', 'like', '%'.$request->search.'%')
// ->orWhere('file_number', 'like', '%'.$request->search.'%')
// ->orWhere('trigger_type', 'like', '%'.$request->search.'%');
// });
// }
// return view('admin.bel.history', [
// 'histories' => $query->paginate(15)
// ]);
// } catch (\Exception $e) {
// Log::error('Error fetching history: ' . $e->getMessage());
// return back()->with('error', 'Gagal memuat riwayat bel');
// }
// }
public function logEvent(Request $request)
{
$validated = $request->validate([
'hari' => 'required|string',
'waktu' => 'required|date_format:H:i:s',
'file_number' => 'required|string|size:4',
'trigger_type' => 'required|in:schedule,manual',
'volume' => 'sometimes|integer|min:1|max:30',
'repeat' => 'sometimes|integer|min:1|max:5'
]);
try {
return response()->json([
'success' => true,
'message' => 'Bell event logged',
'data' => BellHistory::create($validated)
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to log bell event',
'error' => $e->getMessage()
], 500);
}
}
protected function validateSchedule(Request $request): array
{
return $request->validate([
'hari' => 'required|in:' . implode(',', JadwalBel::DAYS),
'waktu' => 'required|date_format:H:i',
'file_number' => 'required|string|size:4',
'is_active' => 'sometimes|boolean'
]);
}
protected function logActivity(string $action, JadwalBel $schedule): void
{
Log::info("{$action} - ID: {$schedule->id}, Hari: {$schedule->hari}, Waktu: {$schedule->waktu}, File: {$schedule->file_number}");
}
}