HydroNutrify_Ari_e41221567/lib/ui/shared/graph_widget.dart

321 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:mobile_monitoring/core/constants/constants.dart';
import 'package:mobile_monitoring/ui/widgets/graph_component_service.dart';
class GraphWidget extends StatefulWidget {
final String? userId;
const GraphWidget({super.key, this.userId});
@override
State<GraphWidget> createState() => _GraphWidgetState();
}
class _GraphWidgetState extends State<GraphWidget> {
final _graphService = GraphComponentService();
Widget _buildEmptyState({
required IconData icon,
required String title,
required String subtitle,
Color? iconColor,
VoidCallback? onRetry,
}) {
return SizedBox(
height: 400,
child: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: iconColor ?? AppColors.textSecondary, size: 50),
const SizedBox(height: 16),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
subtitle,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12, color: AppColors.textSecondary),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
if (onRetry != null) ...[
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh, size: 18),
label: const Text(AppStrings.retry),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
),
),
],
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
if (widget.userId == null || widget.userId!.isEmpty) {
return _buildEmptyState(
icon: Icons.error_outline,
title: AppStrings.pleaseLogin,
subtitle: 'Sesi Anda telah berakhir, login kembali',
iconColor: AppColors.error,
);
}
return Center(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: 320,
child: StreamBuilder<Map<String, dynamic>?>(
stream: _graphService.cycleStream(widget.userId ?? ''),
builder: (context, cycleSnapshot) {
final startDate = _graphService.extractStartDate(
cycleSnapshot.data,
);
return StreamBuilder<List<Map<String, dynamic>>>(
stream: _graphService.historyStream(
limit: 500,
userId: widget.userId,
),
builder: (context, snapshot) {
if (snapshot.hasError) {
final isAuthError = _graphService.isAuthError(
snapshot.error!,
);
return _buildEmptyState(
icon: Icons.error_outline,
title: isAuthError
? AppStrings.pleaseLogin
: AppStrings.failedToLoadData,
subtitle: isAuthError
? 'Sesi Anda telah berakhir, login kembali'
: 'Terjadi kesalahan saat mengambil data grafik',
iconColor: isAuthError ? AppColors.error : AppColors.warning,
onRetry: () => setState(() {}),
);
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(
height: 400,
child: Center(child: CircularProgressIndicator()),
);
}
final docs = snapshot.data ?? [];
if (docs.isEmpty) {
return _buildEmptyState(
icon: Icons.agriculture,
title: AppStrings.startPlantingFirst,
subtitle: 'Mulai monitoring untuk melihat grafik data',
iconColor: AppColors.primary,
);
}
final prepared = _graphService.prepareGraphData(
docs,
startDate,
);
if (!prepared.success || prepared.data == null) {
return _buildEmptyState(
icon: Icons.calendar_today,
title: AppStrings.noDataAvailable,
subtitle:
'Data akan tampil setelah Anda memulai monitoring',
);
}
final graphData = prepared.data!;
final phSpots = List.generate(
graphData.phValues.length,
(i) => FlSpot(i.toDouble(), graphData.phValues[i]),
);
final nutrientSpots = List.generate(
graphData.nutrientValues.length,
(i) => FlSpot(i.toDouble(), graphData.nutrientValues[i]),
);
return Column(
children: [
_GraphCard(
title: 'Grafik pH 7 Hari Terakhir',
color: AppColors.secondaryDark,
spots: phSpots,
minY: 0.0,
maxY: 8.0,
yLabel: 'pH',
xLabels: graphData.dayLabels,
yDecimal: true,
),
const SizedBox(height: 24),
_GraphCard(
title: 'Grafik Nutrisi (TDS) 7 Hari Terakhir',
color: AppColors.info,
spots: nutrientSpots,
minY: 0,
maxY: 1400,
yLabel: 'ppm',
xLabels: graphData.dayLabels,
),
],
);
},
);
},
),
),
),
);
}
}
class _GraphCard extends StatelessWidget {
final String title;
final Color color;
final List<FlSpot> spots;
final double minY, maxY;
final String yLabel;
final List<String> xLabels;
final bool yDecimal;
const _GraphCard({
required this.title,
required this.color,
required this.spots,
required this.minY,
required this.maxY,
required this.yLabel,
required this.xLabels,
this.yDecimal = false,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: AppColors.darkGray.withValues(alpha: 0.1),
spreadRadius: 2,
blurRadius: 10,
),
],
),
height: 280,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
Expanded(
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: yDecimal ? 0.5 : (maxY - minY) / 5,
getDrawingHorizontalLine: (_) =>
const FlLine(color: Color(0xffe7e8ec), strokeWidth: 1),
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 45,
interval: 1,
getTitlesWidget: (value, meta) {
final idx = value.toInt();
return SideTitleWidget(
axisSide: meta.axisSide,
child: idx >= 0 && idx < xLabels.length
? Text(
xLabels[idx],
style: const TextStyle(
fontSize: 9,
fontWeight: FontWeight.bold,
color: Color(0xff68737d),
),
textAlign: TextAlign.center,
)
: const SizedBox.shrink(),
);
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: yDecimal ? 0.5 : (maxY - minY) / 5,
reservedSize: 36,
getTitlesWidget: (value, meta) {
return value >= minY && value <= maxY
? Text(
yDecimal
? value.toStringAsFixed(1)
: value.toStringAsFixed(0),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xff67727d),
),
)
: const SizedBox.shrink();
},
),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: (xLabels.length - 1).toDouble(),
minY: minY,
maxY: maxY,
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: color,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
),
],
),
),
),
],
),
);
}
}