428 lines
13 KiB
PHP
428 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\SortingHistory;
|
|
use App\Models\SortingSession;
|
|
use App\Models\FruitImage;
|
|
use App\Services\TelegramService;
|
|
use Illuminate\Http\Request;
|
|
use Carbon\Carbon;
|
|
|
|
class DashboardController extends Controller
|
|
{
|
|
public function index()
|
|
{
|
|
return view("dashboard");
|
|
}
|
|
|
|
public function history()
|
|
{
|
|
return view("history");
|
|
}
|
|
|
|
public function gallery()
|
|
{
|
|
return view("gallery");
|
|
}
|
|
|
|
public function latestData(Request $request)
|
|
{
|
|
$activeSession = SortingSession::active()->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,
|
|
]);
|
|
}
|
|
}
|