From 79663f47ce7ca2fe745ebcdc9b0674abb73da186 Mon Sep 17 00:00:00 2001 From: soulqan Date: Mon, 8 Jun 2026 13:08:51 +0700 Subject: [PATCH] Fix: status, dan refactor dan build error --- src/components/manage/ManageBin.tsx | 55 ++++--- src/lib/services/binService.ts | 32 ++++ src/lib/services/deriveNotificationService.ts | 71 +++++---- src/pages/api/auth/[...nextauth].ts | 10 +- src/pages/api/change-password.ts | 7 +- src/pages/api/profile.ts | 6 +- src/pages/history/index.tsx | 5 - src/pages/profile/index.tsx | 17 +- src/types/database.ts | 9 +- src/views/dashboard/index.tsx | 61 ++----- src/views/history/index.tsx | 150 ------------------ src/views/manage/index.tsx | 35 ++-- src/views/monitoring/ActionView.tsx | 18 ++- src/views/monitoring/index.tsx | 33 ++-- 14 files changed, 216 insertions(+), 293 deletions(-) create mode 100644 src/lib/services/binService.ts delete mode 100644 src/pages/history/index.tsx delete mode 100644 src/views/history/index.tsx diff --git a/src/components/manage/ManageBin.tsx b/src/components/manage/ManageBin.tsx index 4d909c5..8d0ca6c 100644 --- a/src/components/manage/ManageBin.tsx +++ b/src/components/manage/ManageBin.tsx @@ -15,6 +15,7 @@ interface ManageBinProps { } export default function ManageBin({ isOpen, onClose, editData }: ManageBinProps) { + // FIX: Menambahkan distance ke dalam nilai awal state const [formData, setFormData] = useState({ id: "", gedung: "", @@ -22,7 +23,8 @@ export default function ManageBin({ isOpen, onClose, editData }: ManageBinProps) ruang: "", capacity: 0, level: 0, - status: 'on' + status: 'on', + distance: 0 }); const [error, setError] = useState(null); @@ -31,7 +33,7 @@ export default function ManageBin({ isOpen, onClose, editData }: ManageBinProps) if (editData) { setFormData(editData); } else { - // Reset form untuk create baru + // FIX: Menambahkan distance saat reset form untuk create baru setFormData({ id: "", gedung: "", @@ -39,7 +41,8 @@ export default function ManageBin({ isOpen, onClose, editData }: ManageBinProps) ruang: "", capacity: 0, level: 0, - status: 'on' + status: 'on', + distance: 0 }); } setError(null); @@ -50,7 +53,6 @@ export default function ManageBin({ isOpen, onClose, editData }: ManageBinProps) setError(null); try { - // Validasi ID wajib untuk create bin baru if (!editData && !formData.id.trim()) { setError('ID Bin wajib diisi'); return; @@ -150,8 +152,8 @@ export default function ManageBin({ isOpen, onClose, editData }: ManageBinProps) - {/* Capacity dan Level */} -
+ {/* Capacity, Level, dan Distance */} +
-
- - setFormData({...formData, level: Number(e.target.value)})} - required - /> + + {/* Grid Baru untuk Level dan Distance */} +
+
+ + setFormData({...formData, level: Number(e.target.value)})} + required + /> +
+ + {/* KOLOM BARU: Input untuk Distance */} +
+ + setFormData({...formData, distance: e.target.value ? Number(e.target.value) : 0})} + required + /> +
@@ -182,7 +201,7 @@ export default function ManageBin({ isOpen, onClose, editData }: ManageBinProps) setSelectedLocation(e.target.value)} - className="w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-[11px] text-gray-700 outline-none transition focus:border-emerald-400 sm:py-2.5 sm:text-sm lg:min-w-[280px]" - > - {LOCATION_OPTIONS.map((loc) => ( - - ))} - - -
- setStartDate(e.target.value)} className="bg-transparent outline-none" /> - sampai - setEndDate(e.target.value)} className="bg-transparent outline-none" /> -
- - -
-
-
-

Rata-rata Kapasitas

- -
-

{chartData.length > 0 ? chartData[chartData.length - 1].avg : 0}%

-

Berdasarkan data filter terakhir

-
- -
-
-

Total Aktivitas

- -
-

{filteredRecords.length}

-

Jumlah log yang tercatat

-
- -
-
-

Log Terbaru

- -
-

{latestRecord ? latestRecord.location : '-'}

-

{latestRecord ? `BIN: ${shortenId(latestRecord.binId)}` : 'BIN: -'}

-
-
- -
-
-

Jam Sibuk

-

{peakHour ? peakHour.label : '-'}

-

{peakHour ? `${peakHour.count} log pada jam ini` : 'Belum ada data log'}

-
- -
-

Top Lokasi Sering Penuh

- {topFullLocations.length > 0 ? ( -
- {topFullLocations.map((item, index) => ( -
-

- {index + 1}. {item.location} -

-

{item.count}x

-
- ))} -
- ) : ( -

Belum ada lokasi dengan kapasitas {FULL_THRESHOLD}% ke atas.

- )} -
-
- -
-

Tren Kapasitas Sampah

- -
- -
-

Tentang Dashboard Analitik

-

- Dashboard ini menampilkan analisis real-time pengisian sampah di seluruh lokasi. Gunakan filter lokasi dan tanggal untuk melihat metrik spesifik. - Pantau Jam Sibuk untuk mengetahui periode puncak pengumpulan, dan Top Lokasi Sering Penuh untuk prioritas pengangkutan. Grafik tren membantu merencanakan jadwal maintenance yang lebih efisien. -

-
-
- ); -} diff --git a/src/views/manage/index.tsx b/src/views/manage/index.tsx index 66ebcf8..172b458 100644 --- a/src/views/manage/index.tsx +++ b/src/views/manage/index.tsx @@ -1,11 +1,14 @@ +'use client'; + import { useState, useEffect } from 'react'; import PageContainer from '@/components/layout/PageContainer'; -import PageHeader from '@/components/layout/PageHeader'; // Import PageHeader +import PageHeader from '@/components/layout/PageHeader'; import { db } from '@/lib/firebase'; import { collection, onSnapshot, query, deleteDoc, doc } from 'firebase/firestore'; 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([]); @@ -17,10 +20,17 @@ export default function ManageView() { useEffect(() => { const q = query(collection(db, "bins")); const unsubscribe = onSnapshot(q, (snapshot) => { - const binsData = snapshot.docs.map(doc => ({ - firestoreId: doc.id, - ...doc.data() - } as BinData)); + // ✅ 2. BUNGKUS DENGAN ENFORCEHEARTBEAT DI PINTU GERBANG FIRESTORE + const binsData = snapshot.docs.map(doc => { + const rawBin = { firestoreId: doc.id, ...doc.data() } as any; + const processedBin = binService.enforceHeartbeat(rawBin); + + return { + ...processedBin, + firestoreId: doc.id // Pastikan ID dokumen Firestore tetap menempel untuk keperluan Delete/Edit + } as BinData; + }); + setBins(binsData); setLoading(false); }); @@ -33,6 +43,7 @@ export default function ManageView() { } }; + // LOGIKA FILTER PENCARIAN - Tetap Asli const filteredBins = bins.filter((bin) => { const searchTarget = `${bin.ruang} ${bin.lantai} ${bin.gedung} ${bin.id}`.toLowerCase(); return searchTarget.includes(searchQuery.toLowerCase()); @@ -48,7 +59,7 @@ export default function ManageView() { return ( - {/* Header Section menggunakan PageHeader */} + {/* Header Section */}
- {/* Table Section */} + {/* Table Section (Desktop) */}
@@ -82,7 +93,8 @@ export default function ManageView() { {filteredBins.map((bin) => { const level = bin.level || 0; - const isOnline = bin.status === 'on'; + // Status ini otomatis akurat (menjadi false jika IoT mati lebih dari 8 detik) + const isOnline = bin.status === 'on'; return ( @@ -107,7 +119,7 @@ export default function ManageView() { - {bin.status || 'OFF'} + {isOnline ? 'ON' : 'OFF'} @@ -130,6 +142,7 @@ export default function ManageView() { + {/* Card Section (Mobile) */}
{filteredBins.length === 0 ? (
@@ -151,7 +164,7 @@ export default function ManageView() { - {bin.status || 'OFF'} + {isOnline ? 'ON' : 'OFF'}
@@ -198,4 +211,4 @@ export default function ManageView() { /> ); -} +} \ No newline at end of file diff --git a/src/views/monitoring/ActionView.tsx b/src/views/monitoring/ActionView.tsx index f47ce93..3daa3c1 100644 --- a/src/views/monitoring/ActionView.tsx +++ b/src/views/monitoring/ActionView.tsx @@ -26,6 +26,7 @@ import { FiBarChart2 } from 'react-icons/fi'; import { useRouter } from 'next/router'; +import { binService } from '@/lib/services/binService'; // ✅ 1. IMPORT BINSERVICE DI SINI export default function ActionView({ id }: { id: string }) { const router = useRouter(); @@ -57,7 +58,10 @@ export default function ActionView({ id }: { id: string }) { if (!id) return; const unsubscribe = onSnapshot(doc(db, "bins", id), (docSnap) => { if (docSnap.exists()) { - setBinData({ id: docSnap.id, ...docSnap.data() } as Bin); + 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)); } }); return () => unsubscribe(); @@ -107,7 +111,7 @@ export default function ActionView({ id }: { id: string }) { location: `${binData.gedung} - ${binData.lantai} (${binData.ruang})`, levelCaptured: currentLevel, status: 'Approved', - timestamp: serverTimestamp() + timestamp: serverTimestamp() as any }; await addDoc(collection(db, "history"), historyData); @@ -175,8 +179,14 @@ export default function ActionView({ id }: { id: string }) {

Real-time

-

+ {/* Tambahan visual dinamis jika alat offline secara mendadak */} +

{binData ? `${binData.level}%` : '--%'} + {binData?.status === 'off' && ⚠️ Perangkat Offline}

@@ -232,4 +242,4 @@ export default function ActionView({ id }: { id: string }) { ); -} +} \ No newline at end of file diff --git a/src/views/monitoring/index.tsx b/src/views/monitoring/index.tsx index 30e5d0d..f982cad 100644 --- a/src/views/monitoring/index.tsx +++ b/src/views/monitoring/index.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useState, useEffect } from 'react'; import PageContainer from '@/components/layout/PageContainer'; import PageHeader from '@/components/layout/PageHeader'; @@ -7,9 +9,11 @@ import { useSearch } from '@/context/SearchContext'; import BinCard from '@/components/dashboard/BinCard'; import { FiFilter, FiInfo } from 'react-icons/fi'; import type { Bin } from '@/types/database'; +import { binService } from '@/lib/services/binService'; // ✅ 1. IMPORT SERVICE SINKRONISASI DI SINI export default function MonitoringView() { - const [bins, setBins] = useState([]); + // Intersection Type agar menampung properti firestoreId secara legal + const [bins, setBins] = useState<(Bin & { firestoreId: string })[]>([]); const [loading, setLoading] = useState(true); const { searchQuery } = useSearch(); @@ -18,17 +22,25 @@ export default function MonitoringView() { useEffect(() => { const q = query(collection(db, "bins")); const unsubscribe = onSnapshot(q, (snapshot) => { - const binsData = snapshot.docs.map(doc => ({ - firestoreId: doc.id, - ...doc.data() - })); + 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 + }; + }); + setBins(binsData); setLoading(false); }); return () => unsubscribe(); }, []); - // LOGIKA FILTER GABUNGAN (Search + Dropdown) + // LOGIKA FILTER GABUNGAN (Search + Dropdown) - 100% TETAP ASLI TANPA PERUBAHAN const filteredBins = bins.filter((bin) => { // 1. Filter Search (Menggunakan variabel baru: gedung, lantai, ruang) const searchTarget = `${bin.gedung || ''} ${bin.lantai || ''} ${bin.ruang || ''} ${bin.id || ''}`.toLowerCase(); @@ -42,7 +54,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'; + else if (statusFilter === "Offline") matchesStatus = bin.status === 'off'; // Otomatis sinkron dengan binService return matchesSearch && matchesStatus; }); @@ -96,14 +108,15 @@ export default function MonitoringView() {
{filteredBins.map((bin) => ( ))}
@@ -114,4 +127,4 @@ export default function MonitoringView() { )} ); -} +} \ No newline at end of file