|
|
|
|
@ -133,7 +133,7 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
SizedBox(height: 12.h),
|
|
|
|
|
if (!viewModel.isGraphView) _buildHistoryListView(viewModel) else _buildHistoryFlowchart()
|
|
|
|
|
if (!viewModel.isGraphView) _buildHistoryListView(viewModel) else _buildHistoryGraph()
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
@ -162,6 +162,37 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
subTitle: "${todayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%",
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Add history data if available (show ALL entries)
|
|
|
|
|
if (viewModel.historyList.isNotEmpty) {
|
|
|
|
|
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
|
|
|
|
|
listItems.add(
|
|
|
|
|
Padding(
|
|
|
|
|
padding: EdgeInsets.symmetric(vertical: 8.h),
|
|
|
|
|
child: "Water Intake History".toText14(
|
|
|
|
|
weight: FontWeight.w600,
|
|
|
|
|
color: AppColors.textColor,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Show all history entries
|
|
|
|
|
for (var history in viewModel.historyList) {
|
|
|
|
|
final quantity = "${history.quantity?.toStringAsFixed(0) ?? '0'} ml";
|
|
|
|
|
final time = _formatHistoryDate(history.createdDate ?? '');
|
|
|
|
|
|
|
|
|
|
listItems.add(
|
|
|
|
|
buildHistoryListTile(
|
|
|
|
|
title: quantity,
|
|
|
|
|
subTitle: time,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (history != viewModel.historyList.last) {
|
|
|
|
|
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
listItems.add(
|
|
|
|
|
Center(
|
|
|
|
|
@ -174,10 +205,12 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
}
|
|
|
|
|
} else if (selectedDuration == 'Weekly') {
|
|
|
|
|
if (viewModel.weekProgressList.isNotEmpty) {
|
|
|
|
|
// Show today + last 6 days (total 7 days)
|
|
|
|
|
// Show previous 6 days + today (total 7 days)
|
|
|
|
|
// API returns data in reverse order (today first), so we reverse it to show oldest to newest (top to bottom)
|
|
|
|
|
// This ensures today appears at the end (bottom)
|
|
|
|
|
final totalDays = viewModel.weekProgressList.length;
|
|
|
|
|
final startIndex = totalDays > 7 ? totalDays - 7 : 0;
|
|
|
|
|
final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList();
|
|
|
|
|
final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList().reversed.toList();
|
|
|
|
|
|
|
|
|
|
for (var dayData in weekDataToShow) {
|
|
|
|
|
listItems.add(
|
|
|
|
|
@ -202,11 +235,11 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
}
|
|
|
|
|
} else if (selectedDuration == 'Monthly') {
|
|
|
|
|
if (viewModel.monthProgressList.isNotEmpty) {
|
|
|
|
|
// Show current month + last 6 months (total 7 months)
|
|
|
|
|
// Reverse order to show oldest to newest (top to bottom)
|
|
|
|
|
// Show last 6 months + current month (total 7 months)
|
|
|
|
|
// Show in chronological order: oldest to newest (top to bottom)
|
|
|
|
|
final totalMonths = viewModel.monthProgressList.length;
|
|
|
|
|
final startIndex = totalMonths > 7 ? totalMonths - 7 : 0;
|
|
|
|
|
final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList().reversed.toList();
|
|
|
|
|
final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList();
|
|
|
|
|
|
|
|
|
|
for (var monthData in monthDataToShow) {
|
|
|
|
|
listItems.add(
|
|
|
|
|
@ -250,7 +283,7 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildHistoryFlowchart() {
|
|
|
|
|
Widget _buildHistoryGraph() {
|
|
|
|
|
return Consumer<WaterMonitorViewModel>(
|
|
|
|
|
builder: (context, viewModel, _) {
|
|
|
|
|
final selectedDuration = viewModel.selectedDurationFilter;
|
|
|
|
|
@ -259,8 +292,40 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
List<DataPoint> dataPoints = [];
|
|
|
|
|
|
|
|
|
|
if (selectedDuration == 'Daily') {
|
|
|
|
|
// For daily, we show a single bar/point with today's percentage
|
|
|
|
|
if (viewModel.todayProgressList.isNotEmpty) {
|
|
|
|
|
// For daily, show last 7 history entries with at least 5 minutes difference
|
|
|
|
|
if (viewModel.historyList.isNotEmpty) {
|
|
|
|
|
// Filter entries with at least 5 minutes difference
|
|
|
|
|
List<DataPoint> filteredPoints = [];
|
|
|
|
|
DateTime? lastTime;
|
|
|
|
|
|
|
|
|
|
for (var historyItem in viewModel.historyList) {
|
|
|
|
|
final currentTime = _parseHistoryDate(historyItem.createdDate ?? '');
|
|
|
|
|
|
|
|
|
|
// Add if first entry OR if more than 5 minutes difference from last added entry
|
|
|
|
|
if (lastTime == null || currentTime.difference(lastTime).inMinutes.abs() >= 5) {
|
|
|
|
|
final quantity = historyItem.quantity?.toDouble() ?? 0.0;
|
|
|
|
|
final time = _formatHistoryDate(historyItem.createdDate ?? '');
|
|
|
|
|
|
|
|
|
|
filteredPoints.add(
|
|
|
|
|
DataPoint(
|
|
|
|
|
value: quantity,
|
|
|
|
|
actualValue: quantity.toStringAsFixed(0),
|
|
|
|
|
label: time,
|
|
|
|
|
displayTime: time,
|
|
|
|
|
unitOfMeasurement: 'ml',
|
|
|
|
|
time: currentTime,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
lastTime = currentTime;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Take only last 7 filtered entries
|
|
|
|
|
final totalFiltered = filteredPoints.length;
|
|
|
|
|
final startIndex = totalFiltered > 7 ? totalFiltered - 7 : 0;
|
|
|
|
|
dataPoints = filteredPoints.skip(startIndex).toList();
|
|
|
|
|
} else if (viewModel.todayProgressList.isNotEmpty) {
|
|
|
|
|
// Fallback: show today's percentage if no history
|
|
|
|
|
final todayData = viewModel.todayProgressList.first;
|
|
|
|
|
final percentage = todayData.percentageConsumed?.toDouble() ?? 0.0;
|
|
|
|
|
dataPoints.add(
|
|
|
|
|
@ -275,11 +340,13 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else if (selectedDuration == 'Weekly') {
|
|
|
|
|
// For weekly, show today + last 6 days (total 7 days)
|
|
|
|
|
// For weekly, show previous 6 days + today (total 7 days)
|
|
|
|
|
// API returns data in reverse order (today first), so we reverse it to show oldest to newest (left to right)
|
|
|
|
|
// This ensures today appears at the end (right side)
|
|
|
|
|
if (viewModel.weekProgressList.isNotEmpty) {
|
|
|
|
|
final totalDays = viewModel.weekProgressList.length;
|
|
|
|
|
final startIndex = totalDays > 7 ? totalDays - 7 : 0;
|
|
|
|
|
final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList();
|
|
|
|
|
final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList().reversed.toList();
|
|
|
|
|
|
|
|
|
|
for (var dayData in weekDataToShow) {
|
|
|
|
|
final percentage = dayData.percentageConsumed?.toDouble() ?? 0.0;
|
|
|
|
|
@ -297,12 +364,12 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (selectedDuration == 'Monthly') {
|
|
|
|
|
// For monthly, show current month + last 6 months (total 7 months)
|
|
|
|
|
// Reverse order to show oldest to newest (left to right)
|
|
|
|
|
// For monthly, show last 6 months + current month (total 7 months)
|
|
|
|
|
// Show in chronological order: oldest to newest (left to right)
|
|
|
|
|
if (viewModel.monthProgressList.isNotEmpty) {
|
|
|
|
|
final totalMonths = viewModel.monthProgressList.length;
|
|
|
|
|
final startIndex = totalMonths > 7 ? totalMonths - 7 : 0;
|
|
|
|
|
final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList().reversed.toList();
|
|
|
|
|
final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList();
|
|
|
|
|
|
|
|
|
|
for (var monthData in monthDataToShow) {
|
|
|
|
|
final percentage = monthData.percentageConsumed?.toDouble() ?? 0.0;
|
|
|
|
|
@ -329,9 +396,14 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.bar_chart, size: 48.w, color: AppColors.greyTextColor.withValues(alpha: 0.5)),
|
|
|
|
|
Utils.buildSvgWithAssets(
|
|
|
|
|
icon: AppAssets.graphIcon,
|
|
|
|
|
iconColor: AppColors.greyTextColor.withValues(alpha: 0.5),
|
|
|
|
|
height: 32.w,
|
|
|
|
|
width: 32.w,
|
|
|
|
|
),
|
|
|
|
|
SizedBox(height: 12.h),
|
|
|
|
|
"No chart data available".toText14(color: AppColors.greyTextColor),
|
|
|
|
|
"No graph data available".toText14(color: AppColors.greyTextColor),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
@ -346,43 +418,85 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Define ranges for percentage (0-100%)
|
|
|
|
|
const double low = 25.0; // Below 25% is low
|
|
|
|
|
const double medium = 50.0; // 25-50% is medium
|
|
|
|
|
const double good = 75.0; // 50-75% is good
|
|
|
|
|
const double maxY = 100.0; // Max is 100%
|
|
|
|
|
// Configure graph based on selected duration
|
|
|
|
|
double maxY;
|
|
|
|
|
double minY;
|
|
|
|
|
double horizontalInterval;
|
|
|
|
|
double leftLabelInterval;
|
|
|
|
|
|
|
|
|
|
if (selectedDuration == 'Daily') {
|
|
|
|
|
// For daily (quantity in ml), use max available cup size
|
|
|
|
|
// Get the biggest cup from available cups
|
|
|
|
|
final maxCupSize = viewModel.cups.isEmpty ? 500.0 : viewModel.cups.map((cup) => cup.capacityMl.toDouble()).reduce((a, b) => a > b ? a : b);
|
|
|
|
|
|
|
|
|
|
maxY = maxCupSize;
|
|
|
|
|
minY = 0;
|
|
|
|
|
// Divide into 4 intervals (5 labels: 0, 1/4, 1/2, 3/4, max)
|
|
|
|
|
horizontalInterval = maxY / 4;
|
|
|
|
|
leftLabelInterval = maxY / 4;
|
|
|
|
|
} else {
|
|
|
|
|
// For weekly/monthly (percentage), use 0-100%
|
|
|
|
|
maxY = 100.0;
|
|
|
|
|
minY = 0;
|
|
|
|
|
horizontalInterval = 25;
|
|
|
|
|
leftLabelInterval = 25;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return CustomGraph(
|
|
|
|
|
bottomLabelReservedSize: 30,
|
|
|
|
|
dataPoints: dataPoints,
|
|
|
|
|
makeGraphBasedOnActualValue: true,
|
|
|
|
|
leftLabelReservedSize: 50.w,
|
|
|
|
|
leftLabelInterval: 25,
|
|
|
|
|
leftLabelReservedSize: 50.h,
|
|
|
|
|
showGridLines: true,
|
|
|
|
|
maxY: maxY,
|
|
|
|
|
minY: 0,
|
|
|
|
|
minY: minY,
|
|
|
|
|
maxX: dataPoints.length > 1 ? dataPoints.length.toDouble() - 0.75 : 1.0,
|
|
|
|
|
minX: -0.2,
|
|
|
|
|
horizontalInterval: 25,
|
|
|
|
|
// Grid lines every 25%
|
|
|
|
|
horizontalInterval: horizontalInterval,
|
|
|
|
|
leftLabelInterval: leftLabelInterval,
|
|
|
|
|
showShadow: true,
|
|
|
|
|
getDrawingHorizontalLine: (value) {
|
|
|
|
|
// Draw dashed lines at 25%, 50%, 75%
|
|
|
|
|
if (value == low || value == medium || value == good) {
|
|
|
|
|
return FlLine(
|
|
|
|
|
color: AppColors.greyTextColor.withValues(alpha: 0.3),
|
|
|
|
|
strokeWidth: 1.5,
|
|
|
|
|
dashArray: [8, 4],
|
|
|
|
|
);
|
|
|
|
|
// Draw dashed lines at intervals
|
|
|
|
|
if (selectedDuration == 'Daily') {
|
|
|
|
|
// For daily, draw lines every 50 or 100 ml
|
|
|
|
|
if (value % horizontalInterval == 0 && value > 0) {
|
|
|
|
|
return FlLine(
|
|
|
|
|
color: AppColors.greyTextColor.withValues(alpha: 0.3),
|
|
|
|
|
strokeWidth: 1.5,
|
|
|
|
|
dashArray: [8, 4],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// For weekly/monthly, draw lines at 25%, 50%, 75%
|
|
|
|
|
if (value == 25 || value == 50 || value == 75) {
|
|
|
|
|
return FlLine(
|
|
|
|
|
color: AppColors.successColor.withValues(alpha: 0.3),
|
|
|
|
|
strokeWidth: 1.5,
|
|
|
|
|
dashArray: [8, 4],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return FlLine(color: AppColors.transparent, strokeWidth: 0);
|
|
|
|
|
},
|
|
|
|
|
leftLabelFormatter: (value) {
|
|
|
|
|
// Show percentage labels at key points
|
|
|
|
|
if (value == 0) return '0%'.toText10(weight: FontWeight.w600);
|
|
|
|
|
if (value == 25) return '25%'.toText10(weight: FontWeight.w600);
|
|
|
|
|
if (value == 50) return '50%'.toText10(weight: FontWeight.w600);
|
|
|
|
|
if (value == 75) return '75%'.toText10(weight: FontWeight.w600);
|
|
|
|
|
if (value == 100) return '100%'.toText10(weight: FontWeight.w600);
|
|
|
|
|
if (selectedDuration == 'Daily') {
|
|
|
|
|
// Show exactly 5 labels: 0, 1/4, 1/2, 3/4, max
|
|
|
|
|
// Check if value matches one of the 5 positions
|
|
|
|
|
final interval = maxY / 4;
|
|
|
|
|
final positions = [0.0, interval, interval * 2, interval * 3, maxY];
|
|
|
|
|
|
|
|
|
|
for (var position in positions) {
|
|
|
|
|
if ((value - position).abs() < 1) {
|
|
|
|
|
return '${value.toInt()}ml'.toText10(weight: FontWeight.w600);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Show percentage labels
|
|
|
|
|
if (value == 0) return '0%'.toText10(weight: FontWeight.w600);
|
|
|
|
|
if (value == 25) return '25%'.toText10(weight: FontWeight.w600);
|
|
|
|
|
if (value == 50) return '50%'.toText10(weight: FontWeight.w600);
|
|
|
|
|
if (value == 75) return '75%'.toText10(weight: FontWeight.w600);
|
|
|
|
|
if (value == 100) return '100%'.toText10(weight: FontWeight.w600);
|
|
|
|
|
}
|
|
|
|
|
return SizedBox.shrink();
|
|
|
|
|
},
|
|
|
|
|
graphColor: AppColors.successColor,
|
|
|
|
|
@ -398,12 +512,12 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
int index = value.round();
|
|
|
|
|
if (index < 0 || index >= data.length) return SizedBox.shrink();
|
|
|
|
|
|
|
|
|
|
// For daily, show only index 0
|
|
|
|
|
if (selectedDuration == 'Daily' && index == 0) {
|
|
|
|
|
// For daily, show all 7 time labels (last 7 entries)
|
|
|
|
|
if (selectedDuration == 'Daily' && index < 7) {
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: EdgeInsets.only(top: 8.h),
|
|
|
|
|
child: data[index].label.toText10(
|
|
|
|
|
weight: FontWeight.w600,
|
|
|
|
|
padding: EdgeInsets.only(top: 5.h),
|
|
|
|
|
child: data[index].label.toText8(
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
color: AppColors.labelTextColor,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
@ -412,7 +526,7 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
// For weekly, show all 7 days (today + last 6 days)
|
|
|
|
|
if (selectedDuration == 'Weekly' && index < 7) {
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: EdgeInsets.only(top: 8.h),
|
|
|
|
|
padding: EdgeInsets.only(top: 5.h),
|
|
|
|
|
child: data[index].label.toText10(
|
|
|
|
|
weight: FontWeight.w600,
|
|
|
|
|
color: AppColors.labelTextColor,
|
|
|
|
|
@ -423,7 +537,7 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
// For monthly, show all 7 months (current month + last 6 months)
|
|
|
|
|
if (selectedDuration == 'Monthly' && index < 7) {
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: EdgeInsets.only(top: 8.h),
|
|
|
|
|
padding: EdgeInsets.only(top: 5.h),
|
|
|
|
|
child: data[index].label.toText10(
|
|
|
|
|
weight: FontWeight.w600,
|
|
|
|
|
color: AppColors.labelTextColor,
|
|
|
|
|
@ -626,6 +740,42 @@ class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
|
|
|
|
return '${hour12.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Format history date from /Date(milliseconds+0300)/ format
|
|
|
|
|
String _formatHistoryDate(String dateString) {
|
|
|
|
|
try {
|
|
|
|
|
// Parse the /Date(milliseconds+0300)/ format
|
|
|
|
|
final regex = RegExp(r'\/Date\((\d+)');
|
|
|
|
|
final match = regex.firstMatch(dateString);
|
|
|
|
|
if (match != null) {
|
|
|
|
|
final milliseconds = int.tryParse(match.group(1)!);
|
|
|
|
|
if (milliseconds != null) {
|
|
|
|
|
final dateTime = DateTime.fromMillisecondsSinceEpoch(milliseconds);
|
|
|
|
|
return _formatTime(dateTime);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return dateString;
|
|
|
|
|
}
|
|
|
|
|
return dateString;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parse history date from /Date(milliseconds+0300)/ format to DateTime
|
|
|
|
|
DateTime _parseHistoryDate(String dateString) {
|
|
|
|
|
try {
|
|
|
|
|
final regex = RegExp(r'\/Date\((\d+)');
|
|
|
|
|
final match = regex.firstMatch(dateString);
|
|
|
|
|
if (match != null) {
|
|
|
|
|
final milliseconds = int.tryParse(match.group(1)!);
|
|
|
|
|
if (milliseconds != null) {
|
|
|
|
|
return DateTime.fromMillisecondsSinceEpoch(milliseconds);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Return current time as fallback
|
|
|
|
|
}
|
|
|
|
|
return DateTime.now();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Scaffold(
|
|
|
|
|
|