574 lines
21 KiB
TypeScript
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; |