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 {
|
class HomeScreen extends StatelessWidget {
|
||||||
const HomeScreen({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<RentController>(
|
return Consumer<RentController>(
|
||||||
@@ -95,8 +229,17 @@ class HomeScreen extends StatelessWidget {
|
|||||||
_statusIcon(row.status),
|
_statusIcon(row.status),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(_statusLabel(row.status)),
|
Text(_statusLabel(row.status)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(Icons.edit, size: 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
onTap: () async {
|
||||||
|
await _handleStatusTap(
|
||||||
|
context,
|
||||||
|
controller,
|
||||||
|
row,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
DataCell(
|
DataCell(
|
||||||
Text(
|
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 {
|
Future<void> backfillLastYear() async {
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
_setLoading(true);
|
_setLoading(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user