fixes before build 3
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../models/line_item.dart';
|
||||
import '../utils/money.dart';
|
||||
@@ -26,47 +27,147 @@ class ItemEditSheet extends StatefulWidget {
|
||||
|
||||
class _ItemEditSheetState extends State<ItemEditSheet> {
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _priceController;
|
||||
late final TextEditingController _quantityController;
|
||||
late final TextEditingController _unitPriceController;
|
||||
late final TextEditingController _totalController;
|
||||
String? _error;
|
||||
bool _syncing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final item = widget.item;
|
||||
final quantity = item?.quantity ?? 1;
|
||||
final total = item?.price;
|
||||
final unit = item?.unitPrice ??
|
||||
(total != null && quantity > 0 ? roundMoney(total / quantity) : null);
|
||||
|
||||
_nameController = TextEditingController(text: item?.name ?? '');
|
||||
_priceController = TextEditingController(
|
||||
text: item == null ? '' : item.price.toStringAsFixed(2),
|
||||
_quantityController = TextEditingController(text: '$quantity');
|
||||
_unitPriceController = TextEditingController(
|
||||
text: unit == null ? '' : unit.toStringAsFixed(2),
|
||||
);
|
||||
_totalController = TextEditingController(
|
||||
text: total == null ? '' : total.toStringAsFixed(2),
|
||||
);
|
||||
|
||||
_quantityController.addListener(_onQuantityChanged);
|
||||
_unitPriceController.addListener(_onUnitPriceChanged);
|
||||
_totalController.addListener(_onTotalChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_quantityController.removeListener(_onQuantityChanged);
|
||||
_unitPriceController.removeListener(_onUnitPriceChanged);
|
||||
_totalController.removeListener(_onTotalChanged);
|
||||
_nameController.dispose();
|
||||
_priceController.dispose();
|
||||
_quantityController.dispose();
|
||||
_unitPriceController.dispose();
|
||||
_totalController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int? get _quantity {
|
||||
final raw = _quantityController.text.trim();
|
||||
if (raw.isEmpty) return null;
|
||||
return int.tryParse(raw);
|
||||
}
|
||||
|
||||
void _setText(TextEditingController controller, String value) {
|
||||
if (controller.text == value) return;
|
||||
controller.value = TextEditingValue(
|
||||
text: value,
|
||||
selection: TextSelection.collapsed(offset: value.length),
|
||||
);
|
||||
}
|
||||
|
||||
void _onQuantityChanged() {
|
||||
if (_syncing) return;
|
||||
final quantity = _quantity;
|
||||
if (quantity == null || quantity <= 0) {
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
final unit = tryParsePrice(_unitPriceController.text);
|
||||
final total = tryParsePrice(_totalController.text);
|
||||
_syncing = true;
|
||||
if (unit != null) {
|
||||
_setText(_totalController, roundMoney(quantity * unit).toStringAsFixed(2));
|
||||
} else if (total != null) {
|
||||
_setText(
|
||||
_unitPriceController,
|
||||
roundMoney(total / quantity).toStringAsFixed(2),
|
||||
);
|
||||
}
|
||||
_syncing = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _onUnitPriceChanged() {
|
||||
if (_syncing) return;
|
||||
final quantity = _quantity;
|
||||
final unit = tryParsePrice(_unitPriceController.text);
|
||||
if (quantity == null || quantity <= 0 || unit == null) {
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
_syncing = true;
|
||||
_setText(_totalController, roundMoney(quantity * unit).toStringAsFixed(2));
|
||||
_syncing = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _onTotalChanged() {
|
||||
if (_syncing) return;
|
||||
final quantity = _quantity;
|
||||
final total = tryParsePrice(_totalController.text);
|
||||
if (quantity == null || quantity <= 0 || total == null) {
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
_syncing = true;
|
||||
_setText(
|
||||
_unitPriceController,
|
||||
roundMoney(total / quantity).toStringAsFixed(2),
|
||||
);
|
||||
_syncing = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _save() {
|
||||
final name = _nameController.text.trim();
|
||||
final price = tryParsePrice(_priceController.text);
|
||||
if (name.isEmpty || price == null) {
|
||||
setState(() => _error = 'Enter a product name and a price.');
|
||||
final quantity = _quantity;
|
||||
final unit = tryParsePrice(_unitPriceController.text);
|
||||
final total = tryParsePrice(_totalController.text);
|
||||
|
||||
if (name.isEmpty) {
|
||||
setState(() => _error = 'Enter a product name.');
|
||||
return;
|
||||
}
|
||||
final rounded = roundMoney(price);
|
||||
final quantity = widget.item?.quantity;
|
||||
if (quantity == null || quantity <= 0) {
|
||||
setState(() => _error = 'Enter a quantity of at least 1.');
|
||||
return;
|
||||
}
|
||||
if (total == null && unit == null) {
|
||||
setState(() => _error = 'Enter a unit price or a total.');
|
||||
return;
|
||||
}
|
||||
|
||||
final resolvedTotal = total ?? roundMoney(quantity * unit!);
|
||||
final resolvedUnit = unit ?? roundMoney(resolvedTotal / quantity);
|
||||
final hasMulti = quantity > 1;
|
||||
|
||||
Navigator.of(context).pop(
|
||||
LineItem(
|
||||
id:
|
||||
widget.item?.id ??
|
||||
'manual-${DateTime.now().microsecondsSinceEpoch}',
|
||||
name: name,
|
||||
price: rounded,
|
||||
quantity: quantity,
|
||||
unitPrice: quantity != null && quantity > 0
|
||||
? roundMoney(rounded / quantity)
|
||||
: widget.item?.unitPrice,
|
||||
discount: widget.item?.price == rounded
|
||||
price: roundMoney(resolvedTotal),
|
||||
quantity: hasMulti ? quantity : null,
|
||||
unitPrice: hasMulti ? roundMoney(resolvedUnit) : null,
|
||||
discount: widget.item?.price == roundMoney(resolvedTotal)
|
||||
? widget.item?.discount ?? 0
|
||||
: 0,
|
||||
),
|
||||
@@ -76,6 +177,11 @@ class _ItemEditSheetState extends State<ItemEditSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottom = MediaQuery.viewInsetsOf(context).bottom;
|
||||
final quantity = _quantity;
|
||||
final unit = tryParsePrice(_unitPriceController.text);
|
||||
final preview = quantity != null && quantity > 1 && unit != null
|
||||
? '$quantity × ${formatEuro(unit)}'
|
||||
: null;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 16 + bottom),
|
||||
@@ -95,10 +201,37 @@ class _ItemEditSheetState extends State<ItemEditSheet> {
|
||||
autofocus: widget.item == null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 88,
|
||||
child: TextField(
|
||||
controller: _quantityController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: const InputDecoration(labelText: 'Qty'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _unitPriceController,
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: const InputDecoration(labelText: 'Unit price'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _priceController,
|
||||
controller: _totalController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Price'),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Total price',
|
||||
helperText: preview,
|
||||
),
|
||||
),
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import '../services/app_settings.dart';
|
||||
import '../services/backup_service.dart';
|
||||
import '../supermarkets_screen.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
|
||||
class SettingsButton extends StatelessWidget {
|
||||
const SettingsButton({super.key});
|
||||
@@ -104,6 +105,36 @@ class _SettingsSheetState extends State<_SettingsSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _wipeAllData(AppSettings settings) async {
|
||||
final confirmed = await confirmAction(
|
||||
context,
|
||||
title: 'Delete all data?',
|
||||
message:
|
||||
'This permanently removes all products, shopping trips, lists, '
|
||||
'photos, bonus cards, and settings on this device. '
|
||||
'Export a backup first if you want to keep anything.',
|
||||
confirmLabel: 'Delete everything',
|
||||
);
|
||||
if (!confirmed || !mounted) return;
|
||||
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
await settings.wipeAllData();
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('All data deleted.')),
|
||||
);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Could not delete all data.')),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _openSupermarkets(BuildContext context, AppSettings settings) {
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
@@ -117,18 +148,20 @@ class _SettingsSheetState extends State<_SettingsSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = SettingsScope.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final bottom = MediaQuery.viewInsetsOf(context).bottom;
|
||||
final maxHeight = MediaQuery.sizeOf(context).height * 0.85;
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: settings,
|
||||
builder: (context, _) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 16 + bottom),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
return SizedBox(
|
||||
height: maxHeight,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 16 + bottom),
|
||||
children: [
|
||||
Text('Settings', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text('Settings', style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@@ -151,10 +184,10 @@ class _SettingsSheetState extends State<_SettingsSheet> {
|
||||
: () => _openSupermarkets(context, settings),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Backup', style: Theme.of(context).textTheme.titleMedium),
|
||||
Text('Backup', style: theme.textTheme.titleMedium),
|
||||
Text(
|
||||
'Save everything to a JSON file, or replace this device from a backup.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
@@ -180,6 +213,28 @@ class _SettingsSheetState extends State<_SettingsSheet> {
|
||||
const SizedBox(height: 8),
|
||||
const LinearProgressIndicator(),
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Danger zone',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: scheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Permanently delete everything on this device.',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: scheme.error,
|
||||
side: BorderSide(color: scheme.error),
|
||||
),
|
||||
onPressed: _busy ? null : () => _wipeAllData(settings),
|
||||
icon: const Icon(Icons.delete_forever_outlined),
|
||||
label: const Text('Delete all data'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user