Skip to content
Merged

Uts #26

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Trashware PR Gatekeeper

on:
pull_request:
branches: [ main ]
branches: [ main, UTS ]
types: [ opened, synchronize, reopened ]

concurrency:
Expand Down
32 changes: 0 additions & 32 deletions src/lib/services/binService.ts

This file was deleted.

13 changes: 6 additions & 7 deletions src/lib/services/deriveNotificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
28 changes: 15 additions & 13 deletions src/lib/services/historyAnalyticsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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;
}

Expand Down
6 changes: 4 additions & 2 deletions src/lib/services/historyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
};
});
Expand Down
14 changes: 5 additions & 9 deletions src/views/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,19 @@ 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<Bin[]>([]);
const [loading, setLoading] = useState(true);
const [historyRecords, setHistoryRecords] = useState<HistoryRecord[]>([]);

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);
Expand All @@ -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 (
Expand Down Expand Up @@ -92,7 +89,6 @@ export default function DashboardView() {

<div className="grid grid-cols-2 gap-2 sm:gap-4 lg:grid-cols-1">
{(() => {
// Algoritma ranking otomatis menggunakan data bins yang statusnya sudah valid
const ranking = getPickupPriorityRanking(bins, historyRecords).slice(0, 10);

if (!ranking || ranking.length === 0) return (
Expand Down
18 changes: 5 additions & 13 deletions src/views/manage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<BinData[]>([]);
Expand All @@ -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);
Expand Down Expand Up @@ -85,7 +77,7 @@ export default function ManageView() {
<th className="px-6 py-5">Lantai</th>
<th className="px-6 py-5">Ruang</th>
<th className="px-6 py-5">Kapasitas</th>
<th className="px-6 py-5">Level Isi</th>
<th className="px-6 py-5 text-center">Level Isi</th>
<th className="px-6 py-5 text-center">Status Isi</th>
<th className="px-6 py-5 text-center">Device</th>
<th className="px-6 py-5 text-center">Aksi</th>
Expand All @@ -94,7 +86,7 @@ export default function ManageView() {
<tbody className="divide-y divide-gray-50">
{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 (
Expand All @@ -104,7 +96,7 @@ export default function ManageView() {
<td className="px-6 py-4 text-sm text-gray-600 font-medium">{bin.lantai}</td>
<td className="px-6 py-4 text-sm text-gray-600 font-medium">{bin.ruang}</td>
<td className="px-6 py-4 text-sm text-gray-500 font-bold italic">{bin.capacity}L</td>
<td className="px-6 py-4 text-sm font-bold text-gray-700">{level}%</td>
<td className="px-6 py-4 text-sm font-bold text-center text-gray-700">{level}%</td>

<td className="px-6 py-4 text-center">
<span className={`px-4 py-1.5 rounded-xl text-[11px] font-bold text-white inline-block w-28 text-center shadow-sm ${
Expand Down
11 changes: 4 additions & 7 deletions src/views/monitoring/ActionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
FiBarChart2
} from 'react-icons/fi';
import { useRouter } from 'next/router';
import { binService } from '@/lib/services/binService';

export default function ActionView({ id }: { id: string }) {
const router = useRouter();
Expand Down Expand Up @@ -58,10 +57,9 @@ export default function ActionView({ id }: { id: string }) {
if (!id) return;
const unsubscribe = onSnapshot(doc(db, "bins", id), (docSnap) => {
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();
Expand Down Expand Up @@ -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
};

Expand All @@ -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 (
Expand Down Expand Up @@ -180,7 +177,7 @@ export default function ActionView({ id }: { id: string }) {
<p className="text-[9px] font-bold text-emerald-600 uppercase tracking-widest mb-2 flex items-center gap-2 sm:text-[10px]">
<FiBarChart2 /> Real-time
</p>
{/* Tambahan visual dinamis jika alat offline secara mendadak */}
{/* Membaca string status asli kiriman hardware */}
<p className={`font-black text-lg tracking-tighter sm:text-2xl ${
binData?.status === 'off'
? 'text-gray-400 line-through'
Expand Down
20 changes: 6 additions & 14 deletions src/views/monitoring/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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() {
// Intersection Type agar menampung properti firestoreId secara legal
Expand All @@ -22,16 +21,9 @@ export default function MonitoringView() {
useEffect(() => {
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);
Expand All @@ -42,19 +34,19 @@ 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;

if (statusFilter === "Penuh") matchesStatus = bin.status === 'on' && currentLevel >= 90;
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;
});
Expand Down Expand Up @@ -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}
/>
Expand Down
Loading