TKK_E32231556/src/Containers/Dashboard/index.tsx

574 lines
21 KiB
TypeScript

import React, { useEffect, useState, memo } from 'react';
import {
View, Text, ScrollView, Pressable,
StatusBar, ActivityIndicator, RefreshControl, Alert,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import styles from './styles';
import Navbar from '../../Components/Navbar';
import api, { clearToken } from '../../api/api';
import { RootStackParamList } from '../../Constants/RouteParamsList.contants';
import RouteName from '../../Constants/RouteName.constants';
interface IMonitoring {
id: number;
alat_id: number;
nama_alat: string;
status: string;
waktu_aktif: string | null;
waktu_nonaktif: string | null;
durasi_menit: number;
sumber: string;
}
interface IAlat {
id: number;
nama_alat: string;
status: string;
}
interface ISensor {
id: number;
jarak_cm: number;
status: string;
waktu: string;
}
interface IRPM {
id: number;
alat_id: number;
rpm: number;
status: string;
waktu: string;
}
interface IRealtimePress {
status: string; // 'ON' | 'OFF'
phase: string; // 'TURUN'|'TAHAN'|'NAIK'|'MUNDUR'|'IDLE'
sisa: number;
updatedAt: string | null;
}
interface IRealtimeGiling {
status: string; // 'ON' | 'OFF'
rpm: number;
sisa: number;
updatedAt: string | null;
}
interface IRealtimeSensor {
jarak_cm: number;
status: string;
updatedAt: string | null;
}
interface IRealtimeState {
press: IRealtimePress;
giling: IRealtimeGiling;
estop: { aktif: boolean; updatedAt: string | null };
device: { status: string; updatedAt: string | null };
sensor: IRealtimeSensor;
}
type DashboardNavProp = NativeStackNavigationProp<RootStackParamList>;
// Komponen terisolasi untuk timer durasi — hanya komponen ini yang re-render setiap detik,
// bukan seluruh Dashboard.
interface LiveTimerProps {
waktuAktif: string;
pad: (n: number) => string;
timerStyle: any;
unitStyle: any;
}
const LiveTimer = memo(({ waktuAktif, pad, timerStyle, unitStyle }: LiveTimerProps) => {
const [now, setNow] = useState(Date.now());
useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, []);
const totalMenit = Math.floor((now - new Date(waktuAktif).getTime()) / 60000);
return (
<View style={{ flexDirection: 'row', alignItems: 'baseline' }}>
<Text style={timerStyle}>{pad(Math.floor(totalMenit / 60))}</Text>
<Text style={unitStyle}>jam</Text>
<Text style={timerStyle}>{pad(totalMenit % 60)}</Text>
<Text style={unitStyle}>mnt</Text>
</View>
);
});
const Dashboard = () => {
const navigation = useNavigation<DashboardNavProp>();
const [alatList, setAlatList] = useState<IAlat[]>([]);
const [monitoring, setMonitoring] = useState<IMonitoring[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [namaUser, setNamaUser] = useState('');
const [sensor, setSensor] = useState<ISensor | null>(null);
const [rpmMap, setRpmMap] = useState<Record<number, IRPM>>({});
const [realtime, setRealtime] = useState<IRealtimeState | null>(null);
useEffect(() => {
const loadUser = async () => {
const savedUser = await AsyncStorage.getItem('user');
if (savedUser) {
const user = JSON.parse(savedUser);
setNamaUser(user.nama || '');
}
};
loadUser();
}, []);
const fetchData = async () => {
try {
// 1 request ke backend menggantikan 5 request terpisah
const [dashRes, realtimeRes] = await Promise.all([
api.get('/device/dashboard'),
api.get('/sensor/realtime'),
]);
if (dashRes.data.success) {
const { alat, monitoring, rpmMap: rpm, sensor: sensorData } = dashRes.data.data;
setAlatList(alat);
setMonitoring(monitoring);
setRpmMap(rpm);
setSensor(sensorData);
}
if (realtimeRes.data.success) setRealtime(realtimeRes.data.data);
} catch (err) {
console.log('Dashboard fetch error:', err);
} finally {
setLoading(false);
setRefreshing(false);
}
};
// Fetch sensor HC-SR04 dari realtime state (polling lebih sering untuk update cepat)
const fetchSensor = async () => {
try {
const res = await api.get('/sensor/realtime');
if (res.data.success) setRealtime(res.data.data);
} catch (_) {}
};
useEffect(() => {
fetchData();
const fetchInterval = setInterval(() => { fetchData(); }, 10000);
const sensorInterval = setInterval(() => { fetchSensor(); }, 1000);
return () => {
clearInterval(fetchInterval);
clearInterval(sensorInterval);
};
}, []);
const handleLogout = () => {
Alert.alert('Logout', 'Yakin ingin keluar?', [
{ text: 'Batal', style: 'cancel' },
{
text: 'Keluar', style: 'destructive', onPress: async () => {
await AsyncStorage.removeItem('token');
await AsyncStorage.removeItem('user');
clearToken();
navigation.reset({
index: 0,
routes: [{ name: RouteName.LoginNavigation }],
});
},
},
]);
};
const pad = (n: number) => String(n).padStart(2, '0');
const formatWaktu = (waktu: string) => {
if (!waktu) return '-';
const d = new Date(waktu);
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
const formatDurasi = (menit: number) => {
const j = Math.floor(menit / 60);
const m = menit % 60;
if (j > 0) return `${j}j ${pad(m)}m`;
return `${m} menit`;
};
const hitungTotalMenit = (alat_id: number): number => {
const dataAlat = monitoring.filter(m => m.alat_id === alat_id);
const alat = alatList.find(a => a.id === alat_id);
const isPengepres = alat?.nama_alat.toLowerCase().includes('pres') ?? false;
const rtAlat = isPengepres ? realtime?.press : realtime?.giling;
const mesinNyala = rtAlat?.status === 'ON';
if (!mesinNyala) return 0;
const sesiAktif = dataAlat
.filter(m => m.status === 'aktif' && m.waktu_aktif)
.sort((a, b) => new Date(b.waktu_aktif!).getTime() - new Date(a.waktu_aktif!).getTime())[0];
if (!sesiAktif) return 0;
// Kembalikan waktuAktif string supaya LiveTimer yang hitung detiknya
return new Date(sesiAktif.waktu_aktif!).getTime();
};
// Hitung total menit statis (tidak real-time) — untuk progress bar & stats
const hitungTotalMenitStatis = (alat_id: number): number => {
const dataAlat = monitoring.filter(m => m.alat_id === alat_id);
const alat = alatList.find(a => a.id === alat_id);
const isPengepres = alat?.nama_alat.toLowerCase().includes('pres') ?? false;
const rtAlat = isPengepres ? realtime?.press : realtime?.giling;
const mesinNyala = rtAlat?.status === 'ON';
if (!mesinNyala) return 0;
const sesiAktif = dataAlat
.filter(m => m.status === 'aktif' && m.waktu_aktif)
.sort((a, b) => new Date(b.waktu_aktif!).getTime() - new Date(a.waktu_aktif!).getTime())[0];
if (!sesiAktif) return 0;
return Math.floor((Date.now() - new Date(sesiAktif.waktu_aktif!).getTime()) / 60000);
};
const getWaktuAktif = (alat_id: number): string | null => {
const dataAlat = monitoring.filter(m => m.alat_id === alat_id);
const alat = alatList.find(a => a.id === alat_id);
const isPengepres = alat?.nama_alat.toLowerCase().includes('pres') ?? false;
const rtAlat = isPengepres ? realtime?.press : realtime?.giling;
if (rtAlat?.status !== 'ON') return null;
const sesiAktif = dataAlat
.filter(m => m.status === 'aktif' && m.waktu_aktif)
.sort((a, b) => new Date(b.waktu_aktif!).getTime() - new Date(a.waktu_aktif!).getTime())[0];
return sesiAktif?.waktu_aktif ?? null;
};
const sensorColor = (status: string) => {
if (status === 'peringatan') return '#ef9f27';
if (status === 'bahaya') return '#e24b4a';
return '#1d9e75';
};
const sensorBgColor = (status: string) => {
if (status === 'peringatan') return '#2a1f10';
if (status === 'bahaya') return '#2a1010';
return '#1a2a22';
};
const sensorLabel = (status: string) => {
if (status === 'peringatan') return 'Peringatan';
if (status === 'bahaya') return 'Bahaya';
return 'Normal';
};
const rpmColor = (status: string) => {
if (status === 'peringatan') return '#ef9f27';
if (status === 'bahaya') return '#e24b4a';
return '#ffffff';
};
if (loading) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" backgroundColor="#161b24" />
<View style={styles.topbar}>
<View>
<Text style={styles.topbarTitle}>EdaSmart</Text>
<Text style={styles.topbarSub}>
{namaUser ? `Halo, ${namaUser} 👋` : 'Sistem Manajemen Edamame'}
</Text>
</View>
</View>
<ActivityIndicator color="#1d9e75" size="large" style={{ flex: 1 }} />
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar barStyle="light-content" backgroundColor="#161b24" />
{/* Topbar */}
<View style={styles.topbar}>
<View>
<Text style={styles.topbarTitle}>EdaSmart</Text>
<Text style={styles.topbarSub}>
{namaUser ? `Halo, ${namaUser} 👋` : 'Sistem Manajemen Edamame'}
</Text>
</View>
<View style={{ alignItems: 'flex-end', gap: 6 }}>
{/* Status device dari MQTT realtime */}
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
<View style={[
styles.statusDot,
{ backgroundColor: realtime?.device.status === 'ONLINE' ? '#1d9e75' : '#6b7280' },
]} />
<Text style={styles.statusText}>
{realtime?.device.status === 'ONLINE' ? 'Online' : 'Offline'}
</Text>
</View>
{/* Badge E-Stop jika aktif */}
{realtime?.estop.aktif && (
<View style={{
backgroundColor: '#2a1010',
borderWidth: 1,
borderColor: '#e24b4a',
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 2,
}}>
<Text style={{ color: '#e24b4a', fontSize: 10, fontWeight: '700' }}>
🚨 E-STOP AKTIF
</Text>
</View>
)}
<Pressable
onPress={handleLogout}
style={{
backgroundColor: '#2a1a1a',
borderWidth: 1,
borderColor: '#c0392b',
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 4,
}}>
<Text style={{ color: '#e74c3c', fontSize: 11, fontWeight: '600' }}>
Logout
</Text>
</Pressable>
</View>
</View>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.scrollContent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={() => { setRefreshing(true); fetchData(); }}
tintColor="#1d9e75"
/>
}>
{/* Status Alat */}
<Text style={styles.sectionLabel}>STATUS ALAT</Text>
<View style={styles.card}>
{alatList.length === 0 && (
<Text style={{ color: '#6b7280', textAlign: 'center' }}>Belum ada alat</Text>
)}
{alatList.map((alat, index) => {
const isPengepres = alat.nama_alat.toLowerCase().includes('pres');
const rtAlat = isPengepres ? realtime?.press : realtime?.giling;
const alatNyala = rtAlat?.status === 'ON';
return (
<View key={alat.id}>
<View style={styles.machineRow}>
<View style={styles.machineLeft}>
<View style={styles.machineIcon}>
<Text style={styles.machineIconText}>
{isPengepres ? '🔧' : '⚙️'}
</Text>
</View>
<View>
<Text style={styles.machineName}>{alat.nama_alat}</Text>
<Text style={alatNyala ? styles.machineStatusOn : styles.machineStatusOff}>
{alatNyala ? '● Sedang berjalan' : '● Mati'}
</Text>
</View>
</View>
<View style={[
{ paddingHorizontal: 12, paddingVertical: 4, borderRadius: 20 },
alatNyala ? { backgroundColor: '#1a2a22' } : { backgroundColor: '#1e1e24' },
]}>
<Text style={alatNyala ? styles.machineStatusOn : styles.machineStatusOff}>
{alatNyala ? 'Aktif' : 'Nonaktif'}
</Text>
</View>
</View>
{index < alatList.length - 1 && (
<View style={{ height: 0.5, backgroundColor: '#2a2e3a', marginVertical: 10 }} />
)}
</View>
);
})}
</View>
{/* Monitoring Durasi */}
<Text style={[styles.sectionLabel, { marginTop: 4 }]}>MONITORING DURASI</Text>
{alatList.length === 0 && (
<View style={styles.card}>
<Text style={{ color: '#6b7280', textAlign: 'center' }}>Belum ada data monitoring</Text>
</View>
)}
{alatList.map(alat => {
const dataAlat = monitoring.filter(m => m.alat_id === alat.id);
const totalMenit = hitungTotalMenitStatis(alat.id);
const waktuAktif = getWaktuAktif(alat.id);
const sesiHariIni = dataAlat.length;
const progress = Math.min((totalMenit / 480) * 100, 100);
const rpmData = rpmMap[alat.id];
const isPengepres = alat.nama_alat.toLowerCase().includes('pres');
// Data realtime dari MQTT — jadikan sumber kebenaran status
const rt = isPengepres ? realtime?.press : realtime?.giling;
const mesinNyala = rt?.status === 'ON'; // pakai MQTT, bukan DB
const rtRpm = realtime?.giling.rpm;
const rtPhase = realtime?.press.phase;
const rtSisa = rt?.sisa ?? 0;
return (
<View key={alat.id} style={styles.card}>
<View style={styles.monitorHeader}>
<Text style={styles.monitorTitle}>
{isPengepres ? '🔧' : '⚙️'} {alat.nama_alat}
</Text>
<Text style={mesinNyala ? styles.badgeActive : styles.badgeInactive}>
{mesinNyala ? 'Aktif' : 'Tidak aktif'}
</Text>
</View>
{/* Banner realtime — hanya tampil saat mesin benar-benar ON */}
{isPengepres && mesinNyala && rtPhase && rtPhase !== 'IDLE' && (
<View style={{
flexDirection: 'row', alignItems: 'center', gap: 6,
backgroundColor: '#1a2a22', borderRadius: 8,
paddingHorizontal: 10, paddingVertical: 5, marginBottom: 8,
}}>
<Text style={{ color: '#1d9e75', fontSize: 12, fontWeight: '600' }}>
Fase: {rtPhase}
</Text>
{rtSisa > 0 && (
<Text style={{ color: '#9ca3af', fontSize: 11 }}>
· Sisa {rtSisa}s
</Text>
)}
</View>
)}
{!isPengepres && mesinNyala && (
<View style={{
flexDirection: 'row', alignItems: 'center', gap: 6,
backgroundColor: '#1a2a22', borderRadius: 8,
paddingHorizontal: 10, paddingVertical: 5, marginBottom: 8,
}}>
<Text style={{ color: '#1d9e75', fontSize: 12, fontWeight: '600' }}>
RPM: {rtRpm ?? '—'}
</Text>
{rtSisa > 0 && (
<Text style={{ color: '#9ca3af', fontSize: 11 }}>
· Sisa {rtSisa}s
</Text>
)}
</View>
)}
<Text style={styles.monitorSub}>Total durasi hari ini</Text>
{waktuAktif && mesinNyala ? (
<LiveTimer
waktuAktif={waktuAktif}
pad={pad}
timerStyle={styles.timerNumber}
unitStyle={styles.timerUnit}
/>
) : (
<View style={styles.timerRow}>
<Text style={styles.timerNumber}>00</Text>
<Text style={styles.timerUnit}>jam</Text>
<Text style={styles.timerNumber}>00</Text>
<Text style={styles.timerUnit}>mnt</Text>
</View>
)}
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${progress}%` }]} />
</View>
<View style={styles.statsRow}>
<View style={styles.statBox}>
<Text style={styles.statLabel}>Sesi hari ini</Text>
<Text style={styles.statValue}>{sesiHariIni}x</Text>
</View>
<View style={styles.statBox}>
<Text style={styles.statLabel}>Total durasi</Text>
<Text style={styles.statValue}>{formatDurasi(totalMenit)}</Text>
</View>
{/* RPM hanya tampil untuk alat penggiling (bukan pengepres) */}
{!isPengepres && (
<View style={styles.statBox}>
<Text style={styles.statLabel}>Kecepatan</Text>
<Text style={[
styles.statValue,
{ color: rpmData ? rpmColor(rpmData.status) : '#6b7280' },
]}>
{rpmData ? `${rpmData.rpm} RPM` : '— RPM'}
</Text>
</View>
)}
<View style={styles.statBox}>
<Text style={styles.statLabel}>Status</Text>
<Text style={styles.statValue}>
{alat.status === 'aktif' ? '🟢 On' : '⚫ Off'}
</Text>
</View>
</View>
</View>
);
})}
{/* Sensor Jarak HC-SR04 */}
<Text style={[styles.sectionLabel, { marginTop: 4 }]}>SENSOR JARAK HC-SR04</Text>
<View style={styles.card}>
{(() => {
// Prioritas: realtime.sensor (MQTT langsung) → sensor (DB fallback)
const rtSensor = realtime?.sensor;
const hasRt = rtSensor && rtSensor.updatedAt !== null;
const jarakCm = hasRt ? rtSensor!.jarak_cm : sensor?.jarak_cm ?? null;
const status = hasRt ? rtSensor!.status : sensor?.status ?? 'normal';
const waktu = hasRt ? rtSensor!.updatedAt : sensor?.waktu ?? null;
if (jarakCm === null) return (
<Text style={{ color: '#6b7280', textAlign: 'center' }}>
Belum ada data sensor
</Text>
);
return (
<>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<Text style={{ color: '#9ca3af', fontSize: 12 }}>Jarak terdeteksi</Text>
<View style={{
paddingHorizontal: 10, paddingVertical: 3, borderRadius: 20,
backgroundColor: sensorBgColor(status),
borderWidth: 0.5, borderColor: sensorColor(status),
}}>
<Text style={{ fontSize: 10, fontWeight: '600', color: sensorColor(status) }}>
{sensorLabel(status)}
</Text>
</View>
</View>
<View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 6 }}>
<Text style={{ fontSize: 48, fontWeight: '500', color: sensorColor(status), lineHeight: 56 }}>
{jarakCm}
</Text>
<Text style={{ fontSize: 18, color: '#6b7280' }}>cm</Text>
</View>
<Text style={{ fontSize: 11, color: '#6b7280', marginTop: 6 }}>
Diperbarui: {waktu ? formatWaktu(waktu) : '-'}
</Text>
</>
);
})()}
</View>
</ScrollView>
<Navbar activeScreen="Dashboard" />
</SafeAreaView>
);
};
export default Dashboard;