291 lines
9.0 KiB
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;
|
|
}
|