Files
Tracker/lib/screens/settings/cycle_history_screen.dart

360 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:collection/collection.dart';
import '../../models/cycle_entry.dart';
import '../../providers/user_provider.dart';
class CycleHistoryScreen extends ConsumerStatefulWidget {
const CycleHistoryScreen({super.key});
@override
ConsumerState<CycleHistoryScreen> createState() => _CycleHistoryScreenState();
}
class _CycleHistoryScreenState extends ConsumerState<CycleHistoryScreen> {
bool _isUnlocked = false;
void _showDeleteAllDialog(BuildContext context, WidgetRef ref) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete All History?'),
content: const Text(
'This will permanently delete all cycle entries. This action cannot be undone.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
ref.read(cycleEntriesProvider.notifier).clearEntries();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('All cycle history has been deleted.')),
);
},
child: const Text('Delete All', style: TextStyle(color: Colors.red)),
),
],
),
);
}
void _showDeleteMonthDialog(BuildContext context, WidgetRef ref) {
showDialog(
context: context,
builder: (context) => const _DeleteMonthDialog(),
);
}
Future<void> _authenticate() async {
final user = ref.read(userProfileProvider);
if (user?.privacyPin == null) return;
final controller = TextEditingController();
final pin = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Enter PIN'),
content: TextField(
controller: controller,
keyboardType: TextInputType.number,
obscureText: true,
maxLength: 4,
style: const TextStyle(fontSize: 24, letterSpacing: 8),
textAlign: TextAlign.center,
decoration: const InputDecoration(
hintText: '....',
border: OutlineInputBorder(),
),
autofocus: true,
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
ElevatedButton(
onPressed: () => Navigator.pop(context, controller.text),
child: const Text('Unlock'),
),
],
),
);
if (pin == user!.privacyPin) {
setState(() {
_isUnlocked = true;
});
} else if (pin != null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Incorrect PIN')),
);
}
}
}
@override
Widget build(BuildContext context) {
final entries = ref.watch(cycleEntriesProvider);
final user = ref.watch(userProfileProvider);
// Privacy Check
final isProtected = user?.isHistoryProtected ?? false;
final hasPin = user?.privacyPin != null && user!.privacyPin!.isNotEmpty;
final isLocked = isProtected && hasPin && !_isUnlocked;
if (isLocked) {
return Scaffold(
appBar: AppBar(title: const Text('Cycle History')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.lock_outline, size: 64, color: Colors.grey),
const SizedBox(height: 16),
const Text(
'History is Protected',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _authenticate,
icon: const Icon(Icons.key),
label: const Text('Enter PIN to View'),
),
],
),
),
);
}
final groupedEntries = groupBy(
entries,
(CycleEntry entry) => DateFormat('MMMM yyyy').format(entry.date),
);
return Scaffold(
appBar: AppBar(
title: const Text('Cycle History'),
actions: [
if (entries.isNotEmpty)
PopupMenuButton<String>(
onSelected: (value) {
if (value == 'delete_all') {
_showDeleteAllDialog(context, ref);
} else if (value == 'delete_month') {
_showDeleteMonthDialog(context, ref);
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'delete_month',
child: Text('Delete by Month'),
),
const PopupMenuItem<String>(
value: 'delete_all',
child: Text('Delete All Data'),
),
],
),
],
),
body: entries.isEmpty
? Center(
child: Text(
'No cycle history found.',
style: Theme.of(context).textTheme.bodyLarge,
),
)
: ListView.builder(
itemCount: groupedEntries.keys.length,
itemBuilder: (context, index) {
final month = groupedEntries.keys.elementAt(index);
final monthEntries = groupedEntries[month]!;
return ExpansionTile(
title: Text(month, style: Theme.of(context).textTheme.titleLarge),
initiallyExpanded: index == 0,
children: monthEntries.map((entry) {
return Dismissible(
key: Key(entry.id),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
ref.read(cycleEntriesProvider.notifier).deleteEntry(entry.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Entry for ${DateFormat.yMMMd().format(entry.date)} deleted.')),
);
},
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: const Icon(Icons.delete, color: Colors.white),
),
child: ListTile(
title: Text(DateFormat.yMMMMEEEEd().format(entry.date)),
subtitle: Text(_buildEntrySummary(entry, ref)),
isThreeLine: true,
),
);
}).toList(),
);
},
),
);
}
String _buildEntrySummary(CycleEntry entry, WidgetRef ref) {
final summary = <String>[];
// Calculate Cycle Day / Phase
// This is a simplified calculation. For accurate phase, we need cycle logic.
// We'll calculate the 'Day of Cycle' by finding the most recent period start before this entry.
final allEntries = ref.read(cycleEntriesProvider);
DateTime? lastPeriodStart;
// Inefficient for large lists but acceptable for now.
// Optimization: Calculate this once or pass cycle context.
final sortedEntries = List<CycleEntry>.from(allEntries)..sort((a,b) => a.date.compareTo(b.date));
for (var e in sortedEntries) {
if (e.date.isAfter(entry.date)) break;
if (e.isPeriodDay) {
// If it's a period day and the previous day wasn't (or gap > 1), it's a start.
// Simplified: Just take the period day closest to entry.
// Actually, if 'entry' IS a period day, then it's Menstrual phase.
// We'll just look for the last period day.
lastPeriodStart = e.date; // continuously update to find the latest one <= entry.date
// But we need the START of that period block.
}
}
// Better Approach: Use CycleService static helper if available, or just check entry props.
if (entry.isPeriodDay) {
summary.add('Menstrual Phase');
} else if (lastPeriodStart != null) {
final day = entry.date.difference(lastPeriodStart).inDays + 1;
// Estimate phase based on standard 28 day. User might want actual phase logic.
// Reusing CycleService logic would be best but requires instantiating it with all data.
String phase = 'Follicular';
if (day > 14) phase = 'Luteal'; // Very rough approximation
if (day == 14) phase = 'Ovulation';
summary.add('Day $day ($phase)');
}
if (entry.mood != null) {
summary.add('Mood: ${entry.mood!.label}');
}
if (entry.symptomCount > 0) {
summary.add('${entry.symptomCount} symptom(s)');
}
if (entry.notes != null && entry.notes!.isNotEmpty) {
summary.add('Note: "${entry.notes}"');
}
if (summary.isEmpty) {
return 'No specific data logged.';
}
return summary.join('\n'); // Use newline for better readability with notes
}
}
class _DeleteMonthDialog extends ConsumerStatefulWidget {
const _DeleteMonthDialog();
@override
ConsumerState<_DeleteMonthDialog> createState() => _DeleteMonthDialogState();
}
class _DeleteMonthDialogState extends ConsumerState<_DeleteMonthDialog> {
late int _selectedYear;
late int _selectedMonth;
@override
void initState() {
super.initState();
final now = DateTime.now();
_selectedYear = now.year;
_selectedMonth = now.month;
}
@override
Widget build(BuildContext context) {
final years =
List.generate(5, (index) => DateTime.now().year - index);
final months = List.generate(12, (index) => index + 1);
return AlertDialog(
title: const Text('Delete by Month'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Select a month and year to delete all entries from.'),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DropdownButton<int>(
value: _selectedMonth,
items: months.map((month) => DropdownMenuItem(
value: month,
child: Text(DateFormat('MMMM').format(DateTime(0, month))),
))
.toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedMonth = value;
});
}
},
),
const SizedBox(width: 16),
DropdownButton<int>(
value: _selectedYear,
items: years
.map((year) => DropdownMenuItem(
value: year,
child: Text(year.toString()),
))
.toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedYear = value;
});
}
},
),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
ref
.read(cycleEntriesProvider.notifier)
.deleteEntriesForMonth(_selectedYear, _selectedMonth);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Deleted entries for ${DateFormat('MMMM yyyy').format(DateTime(0, _selectedMonth))}.')),
);
},
child: const Text('Delete', style: TextStyle(color: Colors.red)),
),
],
);
}
}