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 createState() => _GraphWidgetState(); } class _GraphWidgetState extends State { 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?>( stream: _graphService.cycleStream(widget.userId ?? ''), builder: (context, cycleSnapshot) { final startDate = _graphService.extractStartDate( cycleSnapshot.data, ); return StreamBuilder>>( 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 spots; final double minY, maxY; final String yLabel; final List 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), ), ], ), ), ), ], ), ); } }