diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 1c7e581..d34cc9e 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -8,6 +8,140 @@ import 'package:rental_income_tracker/state/rent_controller.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); + Future _showStatusPicker( + BuildContext context, + PaymentStatus current, + ) async { + return showModalBottomSheet( + context: context, + builder: (context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: const Text('Paid on time'), + selected: current == PaymentStatus.onTime, + onTap: () => Navigator.of(context).pop(PaymentStatus.onTime), + ), + ListTile( + title: const Text('Paid late'), + selected: current == PaymentStatus.late, + onTap: () => Navigator.of(context).pop(PaymentStatus.late), + ), + ListTile( + title: const Text('Not paid'), + selected: current == PaymentStatus.notPaid, + onTap: () => Navigator.of(context).pop(PaymentStatus.notPaid), + ), + ListTile( + title: const Text('Pending'), + selected: current == PaymentStatus.pending, + onTap: () => Navigator.of(context).pop(PaymentStatus.pending), + ), + ], + ), + ); + }, + ); + } + + Future _showPaidUsdDialog( + BuildContext context, { + required double initialValue, + }) async { + final controller = TextEditingController( + text: initialValue > 0 ? initialValue.toStringAsFixed(2) : '', + ); + + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Enter paid amount (USD)'), + content: TextField( + controller: controller, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration(labelText: 'USD amount'), + autofocus: true, + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () { + final value = double.tryParse(controller.text.trim()); + if (value == null || value <= 0) { + return; + } + Navigator.of(context).pop(value); + }, + child: const Text('Save'), + ), + ], + ); + }, + ); + } + + Future _handleStatusTap( + BuildContext context, + RentController controller, + RentTableRow row, + ) async { + if (row.status == PaymentStatus.notOccupied) { + if (!context.mounted) { + return; + } + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Use Settings -> Unit occupied to manage not-occupied months.', + ), + ), + ); + return; + } + + final nextStatus = await _showStatusPicker(context, row.status); + if (!context.mounted) { + return; + } + if (nextStatus == null || nextStatus == row.status) { + return; + } + + double? paidUsd; + if (nextStatus == PaymentStatus.onTime || + nextStatus == PaymentStatus.late) { + paidUsd = await _showPaidUsdDialog( + context, + initialValue: row.usdAmount ?? controller.settings.rentUsd, + ); + if (paidUsd == null) { + return; + } + } + + await controller.setMonthStatusManually( + year: row.year, + month: row.month, + status: nextStatus, + paidUsd: paidUsd, + ); + + if (!context.mounted) { + return; + } + if (controller.errorMessage != null) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(controller.errorMessage!))); + } + } + @override Widget build(BuildContext context) { return Consumer( @@ -95,8 +229,17 @@ class HomeScreen extends StatelessWidget { _statusIcon(row.status), const SizedBox(width: 8), Text(_statusLabel(row.status)), + const SizedBox(width: 8), + const Icon(Icons.edit, size: 16), ], ), + onTap: () async { + await _handleStatusTap( + context, + controller, + row, + ); + }, ), DataCell( Text( diff --git a/lib/state/rent_controller.dart b/lib/state/rent_controller.dart index 71790f0..c7079ef 100644 --- a/lib/state/rent_controller.dart +++ b/lib/state/rent_controller.dart @@ -227,6 +227,68 @@ class RentController extends ChangeNotifier { } } + Future setMonthStatusManually({ + required int year, + required int month, + required PaymentStatus status, + double? paidUsd, + }) async { + _errorMessage = null; + _setLoading(true); + + try { + final key = TimeService.monthKey(year, month); + + if (status == PaymentStatus.notOccupied) { + throw StateError( + 'Use Settings -> Unit occupied to manage not-occupied months.', + ); + } + + if (status == PaymentStatus.notPaid || status == PaymentStatus.pending) { + _records.remove(key); + await _storageService.saveRecords(_records); + await _notificationService.cancelForMonth(year, month); + await _syncNotificationScheduleForCurrentMonth(); + return; + } + + if (_isNotOccupiedMonth(year, month)) { + throw StateError('Cannot mark a non-occupied month as paid.'); + } + + final usd = paidUsd ?? _settings.rentUsd; + if (usd <= 0) { + throw StateError('Enter a valid paid amount in USD.'); + } + + final estDate = DateTime(year, month, 1); + final conversion = await _exchangeService.convertUsdToSek( + estDate: estDate, + amount: usd, + ); + + _records[key] = MonthlyRentRecord( + year: year, + month: month, + usdAmount: usd, + sekAmount: conversion.result, + usdToSekRate: conversion.quote, + paidAtUtc: DateTime.now().toUtc(), + onTime: status == PaymentStatus.onTime, + source: 'manual-status', + ); + + await _storageService.saveRecords(_records); + await _notificationService.cancelForMonth(year, month); + await _syncNotificationScheduleForCurrentMonth(); + } catch (err) { + _errorMessage = err.toString(); + } finally { + _setLoading(false); + } + } + Future backfillLastYear() async { _errorMessage = null; _setLoading(true);