Implement status picker and payment dialog for rent status updates
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -8,6 +8,140 @@ import 'package:rental_income_tracker/state/rent_controller.dart';
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
Future<PaymentStatus?> _showStatusPicker(
|
||||
BuildContext context,
|
||||
PaymentStatus current,
|
||||
) async {
|
||||
return showModalBottomSheet<PaymentStatus>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
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<double?> _showPaidUsdDialog(
|
||||
BuildContext context, {
|
||||
required double initialValue,
|
||||
}) async {
|
||||
final controller = TextEditingController(
|
||||
text: initialValue > 0 ? initialValue.toStringAsFixed(2) : '',
|
||||
);
|
||||
|
||||
return showDialog<double>(
|
||||
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: <Widget>[
|
||||
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<void> _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<RentController>(
|
||||
@@ -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(
|
||||
|
||||
@@ -227,6 +227,68 @@ class RentController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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<void> backfillLastYear() async {
|
||||
_errorMessage = null;
|
||||
_setLoading(true);
|
||||
|
||||
Reference in New Issue
Block a user