370 lines
11 KiB
Dart
370 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import 'dart:async';
|
|
import 'package:intl/date_symbol_data_local.dart';
|
|
import 'riwayat_screen.dart';
|
|
|
|
void main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
await initializeDateFormatting('id_ID', null);
|
|
runApp(const MyApp());
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
const MyApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Health Monitor',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: ThemeData.light().copyWith(
|
|
scaffoldBackgroundColor: const Color(0xFFF8F9FA),
|
|
),
|
|
home: const DashboardScreen(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class DashboardScreen extends StatefulWidget {
|
|
const DashboardScreen({super.key});
|
|
|
|
@override
|
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
|
}
|
|
|
|
class _DashboardScreenState extends State<DashboardScreen> {
|
|
int _currentIndex = 0;
|
|
|
|
int bpm = 0;
|
|
int spo2 = 0;
|
|
double temperature = 0.0;
|
|
int systolic = 0;
|
|
int diastolic = 0;
|
|
|
|
String lastUpdate = "Menunggu data...";
|
|
String statusMessage = "Menghubungkan ke server...";
|
|
|
|
// URL untuk mengambil data terbaru
|
|
final String fetchUrl = "http://10.105.126.239/iot/api.php?action=latest";
|
|
|
|
Timer? timer;
|
|
|
|
Future<void> fetchData() async {
|
|
setState(() => statusMessage = "Mengambil data...");
|
|
|
|
print("🔄 Fetching from: $fetchUrl"); // Debug Console
|
|
|
|
try {
|
|
final response = await http
|
|
.get(Uri.parse(fetchUrl), headers: {'Cache-Control': 'no-cache'})
|
|
.timeout(const Duration(seconds: 10));
|
|
|
|
print("📡 Status Code: ${response.statusCode}");
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = json.decode(response.body);
|
|
print("📦 Response Body: ${response.body}"); // Debug
|
|
|
|
if (data['status'] == 'success') {
|
|
setState(() {
|
|
bpm = data['bpm'] ?? 0;
|
|
spo2 = data['spo2'] ?? 0;
|
|
temperature = (data['temperature'] ?? 0.0).toDouble();
|
|
systolic = data['systolic'] ?? 120;
|
|
diastolic = data['diastolic'] ?? 80;
|
|
lastUpdate = data['time'] ?? "Baru saja";
|
|
statusMessage = "✅ Data berhasil diambil";
|
|
});
|
|
} else {
|
|
setState(
|
|
() => statusMessage = "❌ ${data['message'] ?? 'Error dari server'}",
|
|
);
|
|
}
|
|
} else {
|
|
setState(
|
|
() => statusMessage = "❌ Server error (${response.statusCode})",
|
|
);
|
|
}
|
|
} catch (e) {
|
|
print("❌ Error: $e");
|
|
setState(() => statusMessage = "❌ Tidak bisa terhubung ke laptop");
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
fetchData();
|
|
timer = Timer.periodic(const Duration(seconds: 4), (timer) {
|
|
fetchData();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final List<Widget> screens = [
|
|
SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Halo, User! 👋",
|
|
style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold),
|
|
),
|
|
const Text(
|
|
"Berikut adalah hasil kesehatan Anda.",
|
|
style: TextStyle(fontSize: 16, color: Colors.grey),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
statusMessage,
|
|
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Layout 2x2 Cards
|
|
Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildCard(
|
|
Icons.favorite,
|
|
Colors.red,
|
|
"Detak Jantung",
|
|
bpm.toString(),
|
|
"BPM",
|
|
bpm >= 60 && bpm <= 100,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildCard(
|
|
Icons.air,
|
|
Colors.teal,
|
|
"SpO2",
|
|
spo2.toString(),
|
|
"%",
|
|
spo2 >= 95 && spo2 <= 100,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildCard(
|
|
Icons.water_drop,
|
|
Colors.purple,
|
|
"Tekanan Darah",
|
|
"$systolic/$diastolic",
|
|
"mmHg",
|
|
systolic < 140 && diastolic < 90,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildCard(
|
|
Icons.thermostat,
|
|
Colors.blue,
|
|
"Suhu Tubuh",
|
|
temperature.toStringAsFixed(1),
|
|
"°C",
|
|
temperature >= 36.0 && temperature <= 37.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Ringkasan Hasil
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Ringkasan Hasil",
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildSummary("Detak Jantung", "$bpm BPM", Colors.red),
|
|
_buildSummary("SpO2", "$spo2%", Colors.teal),
|
|
_buildSummary(
|
|
"Suhu Tubuh",
|
|
"${temperature.toStringAsFixed(1)} °C",
|
|
Colors.blue,
|
|
),
|
|
_buildSummary(
|
|
"Tekanan Darah",
|
|
"$systolic/$diastolic mmHg",
|
|
Colors.purple,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const RiwayatScreen(),
|
|
];
|
|
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF8F9FA),
|
|
appBar: _currentIndex == 0
|
|
? AppBar(
|
|
backgroundColor: Colors.white,
|
|
elevation: 0,
|
|
title: Row(
|
|
children: const [
|
|
Icon(Icons.favorite, color: Colors.red, size: 28),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
"Health Monitor",
|
|
style: TextStyle(
|
|
color: Colors.black87,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: null,
|
|
body: screens[_currentIndex],
|
|
bottomNavigationBar: BottomNavigationBar(
|
|
currentIndex: _currentIndex,
|
|
onTap: (index) {
|
|
setState(() => _currentIndex = index);
|
|
},
|
|
selectedItemColor: Colors.blue,
|
|
unselectedItemColor: Colors.grey,
|
|
items: const [
|
|
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Beranda"),
|
|
BottomNavigationBarItem(icon: Icon(Icons.history), label: "Riwayat"),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCard(
|
|
IconData icon,
|
|
Color color,
|
|
String title,
|
|
String value,
|
|
String unit,
|
|
bool isNormal,
|
|
) {
|
|
final bool isSpo2 = title == "SpO2";
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(color: Colors.grey.withOpacity(0.08), blurRadius: 10),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 24,
|
|
backgroundColor: color.withOpacity(0.1),
|
|
child: isSpo2
|
|
? const Text(
|
|
"O₂",
|
|
style: TextStyle(
|
|
fontSize: 26,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.teal,
|
|
),
|
|
)
|
|
: Icon(icon, color: color, size: 24),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
title,
|
|
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 26,
|
|
fontWeight: FontWeight.bold,
|
|
color: color,
|
|
),
|
|
),
|
|
Text(unit, style: const TextStyle(fontSize: 12)),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: isNormal
|
|
? Colors.green.withOpacity(0.1)
|
|
: Colors.orange.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
isNormal ? "Normal" : "Perlu Perhatian",
|
|
style: TextStyle(
|
|
color: isNormal ? Colors.green : Colors.orange,
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 11,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSummary(String label, String value, Color color) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.circle, size: 12, color: color),
|
|
const SizedBox(width: 10),
|
|
Text(label, style: const TextStyle(fontSize: 16)),
|
|
],
|
|
),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|