HydroNutrify_Ari_e41221567/lib/ui/shared/monitoring_card.dart

291 lines
9.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:mobile_monitoring/core/constants/constants.dart';
import 'package:mobile_monitoring/ui/widgets/monitoring_card_service.dart';
import 'package:mobile_monitoring/core/utils/phase_display_utils.dart';
class MonitoringCard extends StatefulWidget {
final String title;
final String value;
final String unit;
final String status;
final Color statusColor;
final String description;
final String? userId;
final bool? switch1Value;
final bool? switch2Value;
final String? switch1Label;
final String? switch2Label;
final Function(bool)? onSwitch1Changed;
final Function(bool)? onSwitch2Changed;
const MonitoringCard({
super.key,
required this.title,
required this.value,
required this.unit,
required this.status,
required this.statusColor,
required this.description,
this.userId,
this.switch1Value,
this.switch2Value,
this.switch1Label,
this.switch2Label,
this.onSwitch1Changed,
this.onSwitch2Changed,
});
@override
State<MonitoringCard> createState() => _MonitoringCardState();
}
class _MonitoringCardState extends State<MonitoringCard> {
final _monitoringService = MonitoringCardService();
@override
Widget build(BuildContext context) {
if (widget.userId == null || widget.userId!.isEmpty) {
return _buildCard(PhaseDisplayUtils.vegetatif);
}
return StreamBuilder<PhaseDisplayConfig>(
stream: _monitoringService.growthPhaseStream(widget.userId),
builder: (context, snapshot) {
final phase = snapshot.data ?? PhaseDisplayUtils.vegetatif;
return _buildCard(phase);
},
);
}
Widget _buildCard(PhaseDisplayConfig phase) {
final phaseName = phase.name;
final phaseColor = phase.color;
final statusData = _monitoringService.determineStatus(
title: widget.title,
value: widget.value,
phase: phase,
fallbackStatus: widget.status,
fallbackColor: widget.statusColor,
fallbackDescription: widget.description,
);
final status = statusData.status;
final statusColor = statusData.color;
final description = statusData.description;
final isPh = widget.title.toLowerCase().contains('ph');
final isNutrient = widget.title.toLowerCase().contains('nutrient');
final hasControls =
widget.switch1Label != null && widget.switch2Label != null;
return Container(
width: 320,
height: hasControls ? 310 : 280,
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
padding: const EdgeInsets.fromLTRB(18, 14, 18, 16),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(36),
bottomRight: Radius.circular(36),
),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 16,
offset: Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: phaseColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: phaseColor, width: 1),
),
child: Text(
phaseName,
style: TextStyle(
color: phaseColor,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
if (isNutrient || isPh)
Text(
isPh
? 'Target: 4.0-6.0'
: 'Target: ${phase.nutrientMin}-${phase.nutrientMax} ppm',
style: const TextStyle(
fontSize: 11,
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isPh
? 'Level PH'
: isNutrient
? 'Kadar Nutrisi'
: widget.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
letterSpacing: 0.5,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
isPh
? 'PH ${widget.value.replaceAll(RegExp(r'[^0-9\.]'), '')}'
: widget.value,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
if (widget.unit.isNotEmpty && !isPh)
Padding(
padding: const EdgeInsets.only(left: 4.0, bottom: 2.0),
child: Text(
widget.unit,
style: const TextStyle(
fontSize: 16,
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
const SizedBox(height: 6),
Text(
isPh
? 'Jaga pH larutan antara 4.0 - 6.0 untuk pertumbuhan optimal.'
: isNutrient
? 'Pastikan TDS/PPM nutrisi sesuai kebutuhan tanaman.'
: description,
style: const TextStyle(fontSize: 12, color: AppColors.textSecondary),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CustomPaint(
painter: _CircleStatusPainter(color: statusColor),
child: SizedBox(
width: 85,
height: 85,
child: Center(
child: Text(
status,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.bold,
fontSize: 15,
),
textAlign: TextAlign.center,
),
),
),
),
const SizedBox(width: 12),
if (hasControls)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildSwitchRow(
widget.switch1Label!,
widget.switch1Value ?? false,
widget.onSwitch1Changed,
),
const SizedBox(height: 2),
_buildSwitchRow(
widget.switch2Label!,
widget.switch2Value ?? false,
widget.onSwitch2Changed,
),
],
),
),
],
),
],
),
);
}
Widget _buildSwitchRow(String label, bool value, Function(bool)? onChanged) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: value ? AppColors.secondary : AppColors.black,
),
),
Transform.scale(
scale: 0.85,
child: Switch(
value: value,
onChanged: onChanged,
activeThumbColor: AppColors.secondary,
inactiveThumbColor: AppColors.darkGray,
inactiveTrackColor: AppColors.mediumGray,
),
),
],
);
}
}
class _CircleStatusPainter extends CustomPainter {
final Color color;
_CircleStatusPainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = 5
..style = PaintingStyle.stroke;
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
size.width / 2 - 4,
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}