first(); $query = SortingHistory::latest("classification_date"); $this->applyFilters( $query, $request, "classification_date", $activeSession, ); $latest = (clone $query)->first(); $history = (clone $query)->limit(200)->get(); // Hitung total kalkulasi dari query yang terfilter $calcQuery = clone $query; $totals = [ "total_raw_banana_weight" => (float) $calcQuery->sum("raw_banana_weight"), "total_ripe_banana_weight" => (float) $calcQuery->sum("ripe_banana_weight"), "total_raw_orange_weight" => (float) $calcQuery->sum("raw_orange_weight"), "total_ripe_orange_weight" => (float) $calcQuery->sum("ripe_orange_weight"), "total_records" => (int) $calcQuery->count(), ]; // Daftar sesi yang sudah selesai untuk dropdown di frontend $sessions = SortingSession::where("status", "completed") ->latest("ended_at") ->limit(20) ->get() ->map(function ($s) { return [ "id" => (string) $s->_id, "started_at" => $s->started_at, "ended_at" => $s->ended_at, "total_records" => $s->total_records, ]; }); return response()->json([ "latest" => $latest, "history" => $history, "session" => $activeSession ? [ "id" => (string) $activeSession->_id, "status" => $activeSession->status, "started_at" => $activeSession->started_at, ] : null, "sessions" => $sessions, "totals" => $totals, ]); } /** * Mulai sesi sortir baru * POST /api/session/start */ public function startSession() { // Cek apakah sudah ada sesi aktif $existing = SortingSession::active()->first(); if ($existing) { return response()->json( [ "success" => false, "message" => "Sudah ada sesi sortir yang aktif.", "session" => $existing, ], 409, ); } $session = SortingSession::create([ "status" => "active", "started_at" => now(), "ended_at" => null, "total_raw_banana_weight" => 0, "total_ripe_banana_weight" => 0, "total_raw_orange_weight" => 0, "total_ripe_orange_weight" => 0, "total_records" => 0, ]); return response()->json([ "success" => true, "message" => "Sesi sortir dimulai.", "session" => [ "id" => (string) $session->_id, "status" => $session->status, "started_at" => $session->started_at, ], ]); } /** * Stop sesi sortir, kalkulasi hasil, kirim Telegram * POST /api/session/stop */ public function stopSession() { $session = SortingSession::active()->first(); if (!$session) { return response()->json( [ "success" => false, "message" => "Tidak ada sesi sortir yang aktif.", ], 404, ); } // Kalkulasi total dari semua data yang masuk selama sesi ini $sessionId = (string) $session->_id; $records = SortingHistory::where("session_id", $sessionId)->get(); $totals = [ "total_raw_banana_weight" => $records->sum("raw_banana_weight"), "total_ripe_banana_weight" => $records->sum("ripe_banana_weight"), "total_raw_orange_weight" => $records->sum("raw_orange_weight"), "total_ripe_orange_weight" => $records->sum("ripe_orange_weight"), "total_records" => $records->count(), ]; // Update sesi dengan hasil kalkulasi $session->update( array_merge($totals, [ "status" => "completed", "ended_at" => now(), ]), ); // Refresh model setelah update $session->refresh(); // Kirim hasil ke Telegram try { $telegramService = new TelegramService(); $telegramService->sendSessionResult($session); } catch (\Exception $e) { \Log::warning("Telegram alert gagal: " . $e->getMessage()); } return response()->json([ "success" => true, "message" => "Sesi sortir selesai. Hasil telah dikalkulasi.", "session" => $session, "totals" => $totals, ]); } /** * Endpoint untuk menerima data dari IoT device * POST /api/store-data * Data hanya diterima jika ada sesi aktif */ public function storeData(Request $request) { // Cek apakah ada sesi aktif $session = SortingSession::active()->first(); if (!$session) { return response()->json( [ "success" => false, "message" => "Tidak ada sesi sortir yang aktif. Tekan Start terlebih dahulu.", ], 403, ); } $validated = $request->validate([ "raw_banana_weight" => "required|numeric|min:0", "ripe_banana_weight" => "required|numeric|min:0", "raw_orange_weight" => "required|numeric|min:0", "ripe_orange_weight" => "required|numeric|min:0", ]); // 1. Abaikan data jika semua berat adalah 0 (tidak ada buah di timbangan) $hasWeight = (float) $validated["raw_banana_weight"] > 0 || (float) $validated["ripe_banana_weight"] > 0 || (float) $validated["raw_orange_weight"] > 0 || (float) $validated["ripe_orange_weight"] > 0; if (!$hasWeight) { return response()->json( [ "success" => true, "message" => "Data dilewati karena semua berat bernilai 0 (timbangan kosong).", "data" => null, ], 200, ); } // 2. Cegah duplikasi data buah yang sama dalam waktu < 3 detik $lastRecord = SortingHistory::where( "session_id", (string) $session->_id, ) ->latest("classification_date") ->first(); if ($lastRecord) { $timeDiff = now()->diffInSeconds($lastRecord->classification_date); if ($timeDiff < 3) { $isDuplicate = abs( (float) $lastRecord->raw_banana_weight - (float) $validated["raw_banana_weight"], ) < 0.1 && abs( (float) $lastRecord->ripe_banana_weight - (float) $validated["ripe_banana_weight"], ) < 0.1 && abs( (float) $lastRecord->raw_orange_weight - (float) $validated["raw_orange_weight"], ) < 0.1 && abs( (float) $lastRecord->ripe_orange_weight - (float) $validated["ripe_orange_weight"], ) < 0.1; if ($isDuplicate) { return response()->json( [ "success" => true, "message" => "Data dilewati karena dideteksi sebagai duplikat dari buah yang sama.", "data" => null, ], 200, ); } } } $validated["session_id"] = (string) $session->_id; $validated["classification_date"] = now(); $record = SortingHistory::create($validated); return response()->json( [ "success" => true, "data" => $record, ], 201, ); } /** * Ambil daftar foto buah terbaru * GET /api/foto */ public function latestFoto(Request $request) { $activeSession = SortingSession::active()->first(); $query = FruitImage::latest("captured_at"); // Filter by label jika ada if ($request->has("label") && $request->label) { $query->where("label", $request->label); } $this->applyFilters($query, $request, "captured_at", $activeSession); $fotos = $query ->limit(200) ->get() ->map(function ($item) { return [ "id" => (string) $item->_id, "label" => $item->label, "image_base64" => $item->image_base64, "image_size" => $item->image_size, "captured_at" => $item->captured_at, ]; }); return response()->json(["fotos" => $fotos]); } /** * Apply shared filter logic to a query builder. * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Http\Request $request * @param string $dateColumn The date column to filter on (e.g. 'classification_date' or 'captured_at') * @param \App\Models\SortingSession|null $activeSession */ private function applyFilters( $query, Request $request, string $dateColumn, $activeSession = null, ): void { $filter = $request->input("filter"); switch ($filter) { case "session": $sessionId = $request->input("session_id"); if ($sessionId) { $query->where("session_id", $sessionId); } else { $this->applyDefaultSessionFilter($query, $activeSession); } break; case "today": $query ->where($dateColumn, ">=", Carbon::today()->startOfDay()) ->where($dateColumn, "<=", Carbon::today()->endOfDay()); break; case "week": $query->where( $dateColumn, ">=", Carbon::now()->subDays(7)->startOfDay(), ); break; case "month": $query->where( $dateColumn, ">=", Carbon::now()->subDays(30)->startOfDay(), ); break; case "custom": if ($request->has("date_from") && $request->date_from) { $query->where( $dateColumn, ">=", Carbon::parse($request->date_from)->startOfDay(), ); } if ($request->has("date_to") && $request->date_to) { $query->where( $dateColumn, "<=", Carbon::parse($request->date_to)->endOfDay(), ); } break; default: // Backward compatibility: no filter param uses active/latest session $this->applyDefaultSessionFilter($query, $activeSession); break; } } /** * Apply the default session-based filter (active session or latest completed). */ private function applyDefaultSessionFilter($query, $activeSession): void { if ($activeSession) { $query->where("session_id", (string) $activeSession->_id); } else { $latestCompleted = SortingSession::where("status", "completed") ->latest("ended_at") ->first(); if ($latestCompleted) { $query->where("session_id", (string) $latestCompleted->_id); } else { $query->where("session_id", "non-existent-session-id"); } } } /** * Cek status sesi saat ini * GET /api/session/status */ public function sessionStatus() { $session = SortingSession::active()->first(); return response()->json([ "active" => $session ? true : false, "session" => $session ? [ "id" => (string) $session->_id, "status" => $session->status, "started_at" => $session->started_at, ] : null, ]); } }