From 9961d3a6a54fe998f614721b9473b6f1eade275c Mon Sep 17 00:00:00 2001 From: soulqan Date: Tue, 9 Jun 2026 21:09:21 +0700 Subject: [PATCH 1/2] fix the status --- .github/workflows/ci-cd.yml | 2 +- src/lib/services/binService.ts | 32 ------------------- src/lib/services/deriveNotificationService.ts | 13 ++++---- src/views/dashboard/index.tsx | 14 +++----- src/views/manage/index.tsx | 18 +++-------- src/views/monitoring/ActionView.tsx | 11 +++---- src/views/monitoring/index.tsx | 20 ++++-------- 7 files changed, 27 insertions(+), 83 deletions(-) delete mode 100644 src/lib/services/binService.ts diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9abf106..84b1dd6 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -2,7 +2,7 @@ name: Trashware PR Gatekeeper on: pull_request: - branches: [ main ] + branches: [ main, UTS ] types: [ opened, synchronize, reopened ] concurrency: diff --git a/src/lib/services/binService.ts b/src/lib/services/binService.ts deleted file mode 100644 index 625c408..0000000 --- a/src/lib/services/binService.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Bin } from '@/types/database'; - -export const binService = { - /** - * Mengoreksi status ON/OFF berdasarkan detak jantung (heartbeat) IoT. - * Jika tidak ada kiriman data baru lebih dari 8 detik, paksa status jadi 'off'. - */ - enforceHeartbeat(bin: Bin): Bin { - let isOnline = bin.status === 'on'; - - if (bin.lastUpdate) { - // Ekstrak waktu milidetik menggunakan safe structural casting tanpa 'any' - const lastUpdateMs = typeof bin.lastUpdate === 'object' && bin.lastUpdate && 'toMillis' in bin.lastUpdate - ? (bin.lastUpdate as { toMillis: () => number }).toMillis() - : new Date(bin.lastUpdate as string | number | Date).getTime(); - - const batasToleransi = 8 * 1000; // Jeda ketat 8 detik untuk interval IoT 5 detik - const waktuMaksimalUpdate = Date.now() - batasToleransi; - - if (lastUpdateMs < waktuMaksimalUpdate) { - isOnline = false; - } - } else { - isOnline = false; // Jika tidak ada timestamp, otomatis anggap offline - } - - return { - ...bin, - status: isOnline ? ('on' as const) : ('off' as const), - }; - } -}; \ No newline at end of file diff --git a/src/lib/services/deriveNotificationService.ts b/src/lib/services/deriveNotificationService.ts index 6e28885..75daed8 100644 --- a/src/lib/services/deriveNotificationService.ts +++ b/src/lib/services/deriveNotificationService.ts @@ -2,7 +2,6 @@ import { db } from '@/lib/firebase'; import { collection, onSnapshot } from 'firebase/firestore'; import { Bin } from '@/types/database'; import { notificationReadService } from './notificationReadService'; -import { binService } from './binService'; export interface DerivedNotification { id: string; @@ -26,9 +25,9 @@ export const deriveNotificationService = { callback: (notifications: DerivedNotification[]) => void ) { const unsubscribe = onSnapshot(collection(db, 'bins'), (snapshot) => { + // 🟢 SEKARANG: Langsung mengambil data asli dari Firestore const bins = snapshot.docs.map((doc) => { - const rawBin = { id: doc.id, ...doc.data() } as Bin; - return binService.enforceHeartbeat(rawBin); + return { id: doc.id, ...doc.data() } as Bin; }); // Derive notifications dari tempat sampah yang PENUH DAN AKTIF saja @@ -76,9 +75,9 @@ export const deriveNotificationService = { // Subscribe to bins const unsubscribeBins = onSnapshot(collection(db, 'bins'), (snapshot) => { + // 🟢 SEKARANG: Langsung bypass data tanpa filter enforceHeartbeat binsData = snapshot.docs.map((doc) => { - const rawBin = { id: doc.id, ...doc.data() } as Bin; - return binService.enforceHeartbeat(rawBin); + return { id: doc.id, ...doc.data() } as Bin; }); updateCounts(); @@ -126,9 +125,9 @@ export const deriveNotificationService = { // Subscribe to bins const unsubscribeBins = onSnapshot(collection(db, 'bins'), (snapshot) => { + // 🟢 SEKARANG: Bersih dari import binService binsData = snapshot.docs.map((doc) => { - const rawBin = { id: doc.id, ...doc.data() } as Bin; - return binService.enforceHeartbeat(rawBin); + return { id: doc.id, ...doc.data() } as Bin; }); updateNotifications(); diff --git a/src/views/dashboard/index.tsx b/src/views/dashboard/index.tsx index 50babb4..12f7b39 100644 --- a/src/views/dashboard/index.tsx +++ b/src/views/dashboard/index.tsx @@ -10,11 +10,9 @@ import { FiTrash2, FiCheckCircle, FiAlertTriangle, FiXCircle, FiWifiOff } from ' import { formatDurationInHours, getPickupPriorityRanking } from '@/lib/services/historyAnalyticsService'; import { subscribeHistoryRecords } from '@/lib/services/historyService'; import type { HistoryRecord } from '@/lib/services/historyService'; -import type { Bin } from '@/types/database'; // ✅ Gunakan tipe data Bin murni dari database -import { binService } from '@/lib/services/binService'; // ✅ 1. IMPORT SERVICE SINKRONISASI DI SINI +import type { Bin } from '@/types/database'; // ✅ Menggunakan tipe data Bin murni dari database export default function DashboardView() { - // FIX: Ubah tipe data state menjadi Bin[] agar aman dari Type Error TypeScript const [bins, setBins] = useState([]); const [loading, setLoading] = useState(true); const [historyRecords, setHistoryRecords] = useState([]); @@ -22,10 +20,9 @@ export default function DashboardView() { useEffect(() => { const q = query(collection(db, "bins")); const unsubscribe = onSnapshot(q, (snapshot) => { - // ✅ 2. KOREKSI STATUS DI PINTU GERBANG FIRESTORE (Jika > 8 detik mati, paksa status 'off') + // 🟢 SEKARANG: Langsung ambil data mentah dari Firestore tanpa filter waktu binService const binsData = snapshot.docs.map(doc => { - const rawBin = { id: doc.id, ...doc.data() } as Bin; - return binService.enforceHeartbeat(rawBin); + return { id: doc.id, ...doc.data() } as Bin; }); setBins(binsData); @@ -39,13 +36,13 @@ export default function DashboardView() { return () => unsub(); }, []); - // LOGIKA HITUNG STATISTIK - Otomatis Akurat karena status bins sudah diproses binService + // LOGIKA HITUNG STATISTIK - Sekarang membaca string "on"/"off" asli kiriman hardware const stats = { total: bins.length, empty: bins.filter(b => b.status === 'on' && (b.level ?? 0) < 20).length, nearlyFull: bins.filter(b => b.status === 'on' && (b.level ?? 0) >= 70 && (b.level ?? 0) < 90).length, full: bins.filter(b => b.status === 'on' && (b.level ?? 0) >= 90).length, - offline: bins.filter(b => b.status === 'off').length, // Otomatis bertambah jika alat mati suri + offline: bins.filter(b => b.status === 'off').length, }; if (loading) return ( @@ -92,7 +89,6 @@ export default function DashboardView() {
{(() => { - // Algoritma ranking otomatis menggunakan data bins yang statusnya sudah valid const ranking = getPickupPriorityRanking(bins, historyRecords).slice(0, 10); if (!ranking || ranking.length === 0) return ( diff --git a/src/views/manage/index.tsx b/src/views/manage/index.tsx index f519541..48653d9 100644 --- a/src/views/manage/index.tsx +++ b/src/views/manage/index.tsx @@ -8,7 +8,6 @@ import { collection, onSnapshot, query, deleteDoc, doc } from 'firebase/firestor import { useSearch } from '@/context/SearchContext'; import { FiPlus, FiEdit2, FiTrash2 } from 'react-icons/fi'; import ManageBin, { BinData } from '@/components/manage/ManageBin'; -import { binService } from '@/lib/services/binService'; // ✅ 1. IMPORT SERVICE JANTUNG DI SINI export default function ManageView() { const [bins, setBins] = useState([]); @@ -20,16 +19,9 @@ export default function ManageView() { useEffect(() => { const q = query(collection(db, "bins")); const unsubscribe = onSnapshot(q, (snapshot) => { - // ✅ 2. BUNGKUS DENGAN ENFORCEHEARTBEAT DI PINTU GERBANG FIRESTORE + // 🟢 SEKARANG: Langsung mengambil data asli dari Firestore secara bersih dan aman const binsData = snapshot.docs.map(doc => { - // FIX: Mengganti 'as any' menjadi 'as BinData' untuk menjamin Type Safety - const rawBin = { firestoreId: doc.id, ...doc.data() } as BinData; - const processedBin = binService.enforceHeartbeat(rawBin); - - return { - ...processedBin, - firestoreId: doc.id // Pastikan ID dokumen Firestore tetap menempel untuk keperluan Delete/Edit - } as BinData; + return { firestoreId: doc.id, ...doc.data() } as BinData; }); setBins(binsData); @@ -85,7 +77,7 @@ export default function ManageView() { Lantai Ruang Kapasitas - Level Isi + Level Isi Status Isi Device Aksi @@ -94,7 +86,7 @@ export default function ManageView() { {filteredBins.map((bin) => { const level = bin.level || 0; - // Status ini otomatis akurat (menjadi false jika IoT mati lebih dari 8 detik) + // Murni membaca string 'on' dari database hasil kiriman rutin hardware const isOnline = bin.status === 'on'; return ( @@ -104,7 +96,7 @@ export default function ManageView() { {bin.lantai} {bin.ruang} {bin.capacity}L - {level}% + {level}% { if (docSnap.exists()) { + // 🟢 SEKARANG: Langsung mengambil data asli Firestore tanpa perantara enforceHeartbeat const rawBin = { id: docSnap.id, ...docSnap.data() } as Bin; - - // ✅ 2. SINKRONISASI STATUS DI SINI (Memastikan status 'on'/'off' jujur dalam hitungan detik) - setBinData(binService.enforceHeartbeat(rawBin)); + setBinData(rawBin); } }); return () => unsubscribe(); @@ -111,7 +109,6 @@ export default function ActionView({ id }: { id: string }) { location: `${binData.gedung} - ${binData.lantai} (${binData.ruang})`, levelCaptured: currentLevel, status: 'Approved', - // FIX: Mengubah 'as any' menjadi 'as unknown as Date' agar lolos aturan linter ESLint strict timestamp: serverTimestamp() as unknown as Date }; @@ -135,7 +132,7 @@ export default function ActionView({ id }: { id: string }) { } }; - // UI state untuk proteksi tombol (Double Bang !! untuk menghindari null error di TypeScript) + // UI state untuk proteksi tombol const isButtonDisabled = !!isProcessing || !binData || (!!binData && binData.level < 20 && !message); return ( @@ -180,7 +177,7 @@ export default function ActionView({ id }: { id: string }) {

Real-time

- {/* Tambahan visual dinamis jika alat offline secara mendadak */} + {/* Membaca string status asli kiriman hardware */}

{ const q = query(collection(db, "bins")); const unsubscribe = onSnapshot(q, (snapshot) => { + // 🟢 SEKARANG: Langsung mengambil data asli Firestore tanpa perantara binService const binsData = snapshot.docs.map(doc => { - const rawBin = { firestoreId: doc.id, ...doc.data() } as Bin & { firestoreId: string }; - - // ✅ 2. KOREKSI STATUS DI SINI (Jika > 8 detik tak kirim data, status otomatis dibalik jadi 'off') - const processedBin = binService.enforceHeartbeat(rawBin); - - return { - ...processedBin, - firestoreId: doc.id // Pastikan ID dokumen Firestore tetap menempel - }; + return { firestoreId: doc.id, ...doc.data() } as Bin & { firestoreId: string }; }); setBins(binsData); @@ -42,11 +34,11 @@ export default function MonitoringView() { // LOGIKA FILTER GABUNGAN (Search + Dropdown) - 100% TETAP ASLI TANPA PERUBAHAN const filteredBins = bins.filter((bin) => { - // 1. Filter Search (Menggunakan variabel baru: gedung, lantai, ruang) + // 1. Filter Search (Menggunakan variabel: gedung, lantai, ruang) const searchTarget = `${bin.gedung || ''} ${bin.lantai || ''} ${bin.ruang || ''} ${bin.id || ''}`.toLowerCase(); const matchesSearch = searchTarget.includes(searchQuery.toLowerCase()); - // 2. Filter Dropdown Status (Menggunakan variabel 'level') + // 2. Filter Dropdown Status (Menggunakan variabel 'level' dan 'status' mentah) let matchesStatus = true; const currentLevel = bin.level ?? 0; @@ -54,7 +46,7 @@ export default function MonitoringView() { else if (statusFilter === "Hampir Penuh") matchesStatus = bin.status === 'on' && currentLevel >= 70 && currentLevel < 90; else if (statusFilter === "Terisi") matchesStatus = bin.status === 'on' && currentLevel > 0 && currentLevel < 70; else if (statusFilter === "Kosong") matchesStatus = bin.status === 'on' && currentLevel <= 0; - else if (statusFilter === "Offline") matchesStatus = bin.status === 'off'; // Otomatis sinkron dengan binService + else if (statusFilter === "Offline") matchesStatus = bin.status === 'off'; return matchesSearch && matchesStatus; }); @@ -114,7 +106,7 @@ export default function MonitoringView() { lantai={bin.lantai} ruang={bin.ruang} level={bin.level} - status={bin.status} // Otomatis akurat terkirim ke komponen UI kartu + status={bin.status} capacity={bin.capacity} distance={bin.distance} /> From 7ee1c65711c565c4fb5eec159b139df78bda289a Mon Sep 17 00:00:00 2001 From: vanssea Date: Tue, 9 Jun 2026 21:23:13 +0700 Subject: [PATCH 2/2] fix: update prioritas pengambilan --- src/lib/services/historyAnalyticsService.ts | 28 +++++++++++---------- src/lib/services/historyService.ts | 6 +++-- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/lib/services/historyAnalyticsService.ts b/src/lib/services/historyAnalyticsService.ts index de95fed..9472b85 100644 --- a/src/lib/services/historyAnalyticsService.ts +++ b/src/lib/services/historyAnalyticsService.ts @@ -27,23 +27,15 @@ export type BinPriorityItem = { // Compute pickup priority ranking using live bins and optional history records export const getPickupPriorityRanking = ( - liveBins: Array<{ id: string; location?: string; capacity?: number; level?: number; status?: string; gedung?: string; lantai?: string|number; ruang?: string }> = [], + liveBins: Array<{ id: string; location?: string; capacity?: number; level?: number; status?: string; lastUpdate?: Date | string | number | { toDate?: () => Date; toMillis?: () => number } | null; gedung?: string; lantai?: string|number; ruang?: string }> = [], historyRecords: HistoryRecord[] = [] ): BinPriorityItem[] => { const now = Date.now(); - // helper: find last timestamp when this bin was > - const lastFullTimeForBin = (binId: string) => { - const rec = [...historyRecords] - .filter((r) => r.binId === binId && typeof r.capacity === 'number' && r.capacity ) - .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())[0]; - return rec ? rec.timestamp.getTime() : null; - }; - // helper: count how often bin was full in recent period (e.g., 7 days) const fullCountForBin = (binId: string, days = 7) => { const cutoff = now - days * 24 * 60 * 60 * 1000; - return historyRecords.filter((r) => r.binId === binId && r.timestamp.getTime() >= cutoff && r.capacity).length; + return historyRecords.filter((r) => r.binId === binId && r.timestamp.getTime() >= cutoff && r.capacity >= 90).length; }; const items = liveBins.map((bin) => { @@ -62,14 +54,24 @@ export const getPickupPriorityRanking = ( 'Lokasi Tidak Diketahui'; const binId = bin.id; - const lastFullTs = lastFullTimeForBin(binId); + const lastUpdateTs = bin.lastUpdate + ? (typeof bin.lastUpdate === 'object' && bin.lastUpdate !== null && 'toDate' in bin.lastUpdate && typeof bin.lastUpdate.toDate === 'function' + ? bin.lastUpdate.toDate().getTime() + : typeof bin.lastUpdate === 'object' && bin.lastUpdate !== null && 'toMillis' in bin.lastUpdate && typeof bin.lastUpdate.toMillis === 'function' + ? bin.lastUpdate.toMillis() + : new Date(bin.lastUpdate as string | number | Date).getTime()) + : null; + + const lastFullTs = currentLevel >= 90 + ? lastUpdateTs ?? now + : null; let fullAgeMinutes: number | null = null; if (lastFullTs) { fullAgeMinutes = Math.max(0, Math.floor((now - lastFullTs) / 60000)); } - // Only rank bins that are currently full. This keeps the leaderboard aligned with manage/monitoring. - if (status !== 'on' || currentLevel ) { + // Rank only bins that are full and currently online. + if (status !== 'on' || currentLevel < 90) { return null; } diff --git a/src/lib/services/historyService.ts b/src/lib/services/historyService.ts index cc6709f..afce36b 100644 --- a/src/lib/services/historyService.ts +++ b/src/lib/services/historyService.ts @@ -25,7 +25,8 @@ const parseFirestoreDate = (value: Timestamp | Date | string | number | null | u export const subscribeHistoryRecords = (onData: (data: HistoryRecord[]) => void, onError?: (error: unknown) => void) => { - const historyQuery = query(collection(db, 'bin_history'), orderBy('timestamp', 'asc')); + // Use the 'history' collection where ActionView stores TrashHistory records + const historyQuery = query(collection(db, 'history'), orderBy('timestamp', 'asc')); return onSnapshot( historyQuery, @@ -43,7 +44,8 @@ export const subscribeHistoryRecords = (onData: (data: HistoryRecord[]) => void, id: historyDoc.id, binId: data.binId || 'Unknown ID', location: composedLocation, - capacity: data.capacity || 0, + // Accept either 'capacity' or 'levelCaptured' (ActionView uses levelCaptured) + capacity: typeof data.capacity === 'number' ? data.capacity : (typeof data.levelCaptured === 'number' ? data.levelCaptured : 0), timestamp: parseFirestoreDate(data.timestamp), }; });