TKK_E32232282/Source Code Mobile/dashboard_page.dart

1093 lines
36 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/sensor_provider.dart';
import '../providers/ac_provider.dart';
import '../providers/ac_config_provider.dart';
import '../providers/ac_setup_provider.dart';
import '../providers/auth_provider.dart';
import '../models/sensor_model.dart';
import '../models/ac_model.dart';
import 'monitoring_history_page.dart';
import 'profile_page.dart';
import 'login_page.dart';
import '../core/app_color.dart';
class DashboardPage extends StatefulWidget {
const DashboardPage({super.key});
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
int currentIndex = 0;
@override
void initState() {
super.initState();
context.read<SensorProvider>().startListening();
context.read<ACProvider>().startListening();
context.read<ACConfigProvider>().startListening();
context.read<ACSetupProvider>().startListening();
}
@override
Widget build(BuildContext context) {
final sensorProvider = context.watch<SensorProvider>();
final sensor = sensorProvider.sensor;
final acProvider = context.watch<ACProvider>();
final ac = acProvider.ac;
final acConfig = context.watch<ACConfigProvider>();
final setupProvider = context.watch<ACSetupProvider>();
final pages = [
_buildDashboard(
sensor,
sensorProvider.isLoading,
sensorProvider.errorMessage,
ac,
acProvider,
acConfig,
setupProvider,
),
const MonitoringHistoryPage(),
const ProfilePage(),
];
return Scaffold(
body: pages[currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
onTap: (value) => setState(() => currentIndex = value),
selectedItemColor: Colors.blueAccent,
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.dashboard),
label: 'Dashboard',
),
BottomNavigationBarItem(icon: Icon(Icons.history), label: 'Histori'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profil'),
],
),
);
}
// ====================== DASHBOARD BUILD ======================
Widget _buildDashboard(
SensorModel? sensor,
bool isLoading,
String? errorMessage,
ACModel ac,
ACProvider acProvider,
ACConfigProvider acConfig,
ACSetupProvider setupProvider,
) {
Widget bodyContent;
if (isLoading) {
bodyContent = const Center(
child: CircularProgressIndicator(color: Colors.blueAccent),
);
} else if (sensor == null) {
bodyContent = Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.cloud_off, size: 80, color: Colors.grey),
const SizedBox(height: 16),
const Text(
'Data Tidak Tersedia',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
errorMessage ?? 'Database kosong atau koneksi ke IoT terputus.',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
context.read<SensorProvider>().startListening();
context.read<ACProvider>().startListening();
},
icon: const Icon(Icons.refresh),
label: const Text('Coba Lagi'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
),
),
],
),
),
);
} else {
final isAirGood =
sensor.overallStatus == 'Sangat Baik' ||
sensor.overallStatus == 'Baik';
bodyContent = ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: [
// Header
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Colors.blueAccent,
shape: BoxShape.circle,
),
child: const Icon(Icons.ac_unit, color: Colors.white, size: 24),
),
const SizedBox(width: 12),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Smart AC',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'IoT Dashboard',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
const Spacer(),
// Live Badge
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
border: Border.all(
color: Colors.green.withValues(alpha: 0.5),
),
borderRadius: BorderRadius.circular(16),
),
child: const Row(
children: [
Icon(
Icons.fiber_manual_record,
color: Colors.green,
size: 10,
),
SizedBox(width: 4),
Text(
'Live',
style: TextStyle(
color: Colors.green,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.logout, color: Colors.white70),
onPressed: () async {
await context.read<AuthProvider>().logout();
if (!mounted) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const LoginPage()),
(route) => false,
);
},
),
],
),
const SizedBox(height: 16),
// Air Quality Banner
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isAirGood ? Colors.green : Colors.red,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Icon(
isAirGood
? Icons.check_circle_outline
: Icons.warning_amber_outlined,
color: Colors.white,
),
const SizedBox(width: 12),
Expanded(
child: Text(
isAirGood
? 'Kualitas udara baik — semua parameter normal'
: 'Kualitas udara buruk — Buka ventilasi ruangan',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
],
),
),
const SizedBox(height: 20),
const Text(
'Sensor Data',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white70,
),
),
const SizedBox(height: 10),
// 2x2 Sensor Grid
Row(
children: [
Expanded(
child: _buildSensorCard(
title: 'Suhu',
value: '${sensor.temperature.toStringAsFixed(1)} °C',
status: sensor.temperatureStatus,
icon: Icons.thermostat,
iconColor: Colors.blue,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSensorCard(
title: 'Kelembaban',
value: '${sensor.humidity.toStringAsFixed(1)} %',
status: sensor.humidityStatus,
icon: Icons.water_drop,
iconColor: Colors.green,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildSensorCard(
title: 'PM2.5',
value: '${sensor.pm25.toStringAsFixed(2)} µg/m³',
status: sensor.pm25Status,
icon: Icons.grain,
iconColor: Colors.amber,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSensorCard(
title: 'Karbon Dioksida',
value: '${sensor.co2.toStringAsFixed(0)} ppm',
status: sensor.co2Status,
icon: Icons.co2,
iconColor: Colors.redAccent,
),
),
],
),
const SizedBox(height: 24),
// ============ AC CARD (inline setup + manual control) ============
_buildACCard(ac, acProvider, acConfig, setupProvider),
const SizedBox(height: 24),
],
);
}
return Scaffold(
backgroundColor: AppColor.background,
body: SafeArea(child: bodyContent),
);
}
// ====================== AC CARD ======================
// This single card handles both setup flow and manual control inline.
Widget _buildACCard(
ACModel ac,
ACProvider acProvider,
ACConfigProvider acConfig,
ACSetupProvider setupProvider,
) {
// Determine if we're currently in setup flow
final bool isInSetup = setupProvider.mode == 'config' ||
setupProvider.mode == 'sweep';
// If in active setup flow, show the setup UI
if (isInSetup) {
return _buildSetupFlowCard(setupProvider);
}
// If has config, show manual control
if (acConfig.hasConfig) {
return _buildManualControlCard(ac, acProvider, setupProvider);
}
// No config and not in setup — show "start config" prompt
return _buildStartConfigCard(setupProvider);
}
// ====================== START CONFIG CARD ======================
Widget _buildStartConfigCard(ACSetupProvider setupProvider) {
return Card(
color: AppColor.cardBg,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCardHeader(
icon: Icons.tune,
title: 'Konfigurasi AC',
subtitle: 'Pilih merk lalu uji model hingga cocok',
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: setupProvider.isSending
? null
: () => setupProvider.startConfigMode(),
icon: setupProvider.isSending
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.settings),
label: Text(
setupProvider.isSending
? 'Mengirim...'
: 'Mulai Konfigurasi AC',
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
),
const SizedBox(height: 12),
const Text(
'Langkah singkat:',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 6),
const Text(
'1) Tekan "Mulai Konfigurasi".\n'
'2) Pilih merek AC dari daftar.\n'
'3) Untuk tiap model tekan "YA" bila AC merespon.',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
),
);
}
// ====================== SETUP FLOW CARD ======================
Widget _buildSetupFlowCard(ACSetupProvider setupProvider) {
return Card(
color: AppColor.cardBg,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCardHeader(
icon: Icons.tune,
title: 'Konfigurasi AC',
subtitle: setupProvider.message,
trailing: TextButton.icon(
onPressed: setupProvider.isSending
? null
: () => setupProvider.cancelConfig(),
icon: const Icon(Icons.close, size: 16, color: Colors.redAccent),
label: const Text(
'Batal',
style: TextStyle(color: Colors.redAccent, fontSize: 12),
),
),
),
const SizedBox(height: 16),
// Step: select_brand — show brand list
if (setupProvider.step == 'select_brand')
_buildBrandGrid(setupProvider),
// Step: confirm — show sweep confirmation
if (setupProvider.step == 'confirm')
_buildSweepConfirm(setupProvider),
],
),
),
);
}
Widget _buildBrandGrid(ACSetupProvider setupProvider) {
if (setupProvider.brands.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
CircularProgressIndicator(color: Colors.blueAccent),
SizedBox(height: 12),
Text(
'Memuat daftar merek...',
style: TextStyle(color: Colors.grey),
),
],
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pilih Merek AC:',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 12),
...setupProvider.brands.map((brand) {
final int index = brand['index'] ?? 0;
final String desc = brand['description'] ?? brand['name'] ?? '';
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: setupProvider.isSending
? null
: () => setupProvider.selectBrand(index),
icon: const Icon(Icons.air, size: 18),
label: Text(desc),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blueAccent,
side: BorderSide(
color: Colors.blueAccent.withValues(alpha: 0.4),
),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
);
}),
],
);
}
Widget _buildSweepConfirm(ACSetupProvider setupProvider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Show current brand/model being tested
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blueAccent.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.blueAccent.withValues(alpha: 0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Menguji: ${setupProvider.brandDesc}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
if (setupProvider.modelName.isNotEmpty)
Text(
'Model: ${setupProvider.modelName}',
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
),
const SizedBox(height: 16),
const Text(
'Apakah AC merespon sinyal ini?',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: setupProvider.isSending
? null
: () => setupProvider.confirm('y'),
icon: const Icon(Icons.check, size: 18),
label: const Text('YA'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: setupProvider.isSending
? null
: () => setupProvider.confirm('n'),
icon: const Icon(Icons.close, size: 18),
label: const Text('TIDAK'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton(
onPressed: setupProvider.isSending
? null
: () => setupProvider.confirm('skip'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white70,
side: const BorderSide(color: Colors.white30),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text('SKIP'),
),
),
],
),
],
);
}
// ====================== MANUAL CONTROL CARD ======================
Widget _buildManualControlCard(
ACModel ac,
ACProvider acProvider,
ACSetupProvider setupProvider,
) {
return Card(
color: AppColor.cardBg,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section header with reset button
_buildCardHeader(
icon: Icons.tune,
title: 'Kontrol Manual',
subtitle: 'Kirim sinyal IR ke AC via ESP32',
trailing: IconButton(
icon: const Icon(Icons.settings, color: Colors.blueAccent, size: 20),
tooltip: 'Konfigurasi Ulang',
onPressed: () => _showResetConfirmation(setupProvider),
),
),
const SizedBox(height: 20),
// Power, Swing, & Control Mode (Switch) Button Row
Row(
children: [
Expanded(
flex: 5,
child: OutlinedButton.icon(
onPressed: () => acProvider.setPower(!ac.power),
icon: Icon(
ac.power ? Icons.power_off : Icons.power_settings_new,
color: ac.power ? Colors.red : Colors.green,
size: 18,
),
label: Text(
ac.power ? 'Matikan' : 'Nyalakan',
style: TextStyle(
color: ac.power ? Colors.red : Colors.green,
fontSize: 11,
),
),
style: OutlinedButton.styleFrom(
side: BorderSide(
color: ac.power
? Colors.red.withValues(alpha: 0.5)
: Colors.green.withValues(alpha: 0.5),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 10),
),
),
),
const SizedBox(width: 6),
Expanded(
flex: 5,
child: OutlinedButton.icon(
onPressed: () => acProvider.toggleSwing(),
icon: Icon(
Icons.swap_vert,
color: ac.swing ? Colors.blueAccent : Colors.white60,
size: 18,
),
label: Text(
ac.swing ? 'Swing ON' : 'Swing OFF',
style: TextStyle(
color: ac.swing ? Colors.blueAccent : Colors.white60,
fontSize: 11,
),
),
style: OutlinedButton.styleFrom(
side: BorderSide(
color: ac.swing
? Colors.blueAccent.withValues(alpha: 0.5)
: Colors.white30,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 10),
),
),
),
const SizedBox(width: 6),
Expanded(
flex: 4,
child: Container(
height: 44, // Match the height of OutlinedButton with vertical padding 10
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
ac.controlMode == 'auto' ? 'Auto' : 'Manual',
style: TextStyle(
color: ac.controlMode == 'auto' ? Colors.blueAccent : Colors.orangeAccent,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 1),
SizedBox(
height: 20,
child: Transform.scale(
scale: 0.65,
child: Switch.adaptive(
value: ac.controlMode == 'auto',
activeThumbColor: Colors.blueAccent,
activeTrackColor: Colors.blueAccent.withValues(alpha: 0.3),
inactiveThumbColor: Colors.orangeAccent,
inactiveTrackColor: Colors.orangeAccent.withValues(alpha: 0.3),
onChanged: (bool value) {
acProvider.setControlMode(value ? 'auto' : 'manual');
},
),
),
),
],
),
),
),
],
),
const SizedBox(height: 16),
// Mode & Fan cyclic buttons + Temp in a row
Row(
children: [
// Mode cyclic button
Expanded(
child: _buildCyclicButton(
label: 'Mode',
value: ac.mode,
icon: _modeIcon(ac.mode),
color: Colors.blueAccent,
onTap: () => acProvider.cycleMode(),
),
),
const SizedBox(width: 8),
// Fan cyclic button
Expanded(
child: _buildCyclicButton(
label: 'Kipas',
value: ac.fan,
icon: Icons.air,
color: Colors.tealAccent,
onTap: () => acProvider.cycleFan(),
),
),
const SizedBox(width: 8),
// Temperature control
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Suhu',
style: TextStyle(
color: Colors.grey,
fontSize: 9,
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
GestureDetector(
onTap: () =>
acProvider.setTemp(ac.temp - 1),
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
border: Border.all(
color: Colors.white30,
),
shape: BoxShape.circle,
),
child: const Icon(
Icons.remove,
size: 12,
color: Colors.white,
),
),
),
Text(
'${ac.temp}°',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
GestureDetector(
onTap: () =>
acProvider.setTemp(ac.temp + 1),
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
border: Border.all(
color: Colors.white30,
),
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
size: 12,
color: Colors.white,
),
),
),
],
),
],
),
),
),
],
),
],
),
),
);
}
// ====================== RESET CONFIRMATION ======================
void _showResetConfirmation(ACSetupProvider setupProvider) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppColor.cardBg,
title: const Text(
'Konfigurasi Ulang AC',
style: TextStyle(color: Colors.white),
),
content: const Text(
'Konfigurasi saat ini akan dihapus dan Anda harus memilih merek AC lagi. Lanjutkan?',
style: TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Batal', style: TextStyle(color: Colors.grey)),
),
TextButton(
onPressed: () async {
Navigator.of(ctx).pop();
await setupProvider.clearConfig();
await setupProvider.startConfigMode();
},
child: const Text('Ya, Reset', style: TextStyle(color: Colors.redAccent)),
),
],
),
);
}
// ====================== CARD HEADER ======================
Widget _buildCardHeader({
required IconData icon,
required String title,
required String subtitle,
Widget? trailing,
}) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.blue, size: 20),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
subtitle,
style: const TextStyle(fontSize: 11, color: Colors.grey),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
?trailing,
],
);
}
// ====================== CYCLIC BUTTON ======================
Widget _buildCyclicButton({
required String label,
required String value,
required IconData icon,
required Color color,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withValues(alpha: 0.4)),
),
child: Column(
children: [
Icon(icon, color: color, size: 16),
const SizedBox(height: 2),
Text(
label,
style: const TextStyle(color: Colors.grey, fontSize: 9),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
color: color,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
Icon(Icons.refresh, color: color.withValues(alpha: 0.5), size: 10),
],
),
),
);
}
IconData _modeIcon(String mode) {
switch (mode.toUpperCase()) {
case 'COOL':
return Icons.ac_unit;
case 'DRY':
return Icons.water_drop_outlined;
case 'FAN':
return Icons.air;
case 'HEAT':
return Icons.local_fire_department;
case 'AUTO':
return Icons.auto_mode;
default:
return Icons.ac_unit;
}
}
// ====================== SENSOR CARD ======================
Widget _buildSensorCard({
required String title,
required String value,
required String status,
required IconData icon,
required Color iconColor,
}) {
return Card(
color: AppColor.cardBg,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: iconColor.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(icon, color: iconColor, size: 20),
),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 13,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 16),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 10),
_buildStatusChip(status),
],
),
),
);
}
Widget _buildStatusChip(String status) {
Color color;
if (status == 'Sangat Baik' || status == 'Baik') {
color = AppColor.success;
} else if (status == 'Buruk') {
color = AppColor.danger;
} else {
color = AppColor.warning;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withValues(alpha: 0.3), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(
status.isEmpty ? 'Normal' : status,
style: TextStyle(
color: color,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}