Files
Dmitry 322c60a359 style(app): остальные экраны под новый визуальный язык и форматирование
Доходы, ребалансировка, налоги, импорт, цели, здоровье данных, правила, транзакции, категории, cashflow, pending: новые виджеты и токены темы, dart format. Тесты подстроены под новые модели.
2026-09-19 22:14:27 +03:00

192 lines
7.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:decimal/decimal.dart';
import 'package:fintracker_api/fintracker_api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/utils/ru_date.dart';
import '../portfolio/providers.dart' show scopesProvider;
import 'data/goals_api.dart';
/// Create/edit form for a goal. Returns the goal to save, or null when cancelled.
class GoalEditDialog extends ConsumerStatefulWidget {
const GoalEditDialog({super.key, this.initial});
final Goal? initial;
@override
ConsumerState<GoalEditDialog> createState() => _GoalEditDialogState();
}
class _GoalEditDialogState extends ConsumerState<GoalEditDialog> {
final _formKey = GlobalKey<FormState>();
late final _name = TextEditingController(text: widget.initial?.name ?? '');
late final _amount = TextEditingController(
text: widget.initial?.targetAmount ?? '',
);
late final _contribution = TextEditingController(
text: widget.initial?.monthlyContribution ?? '',
);
late final _note = TextEditingController(text: widget.initial?.note ?? '');
late String _scope = widget.initial?.scope ?? 'all';
late DateTime? _targetDate = widget.initial?.targetDate;
late bool _archived = widget.initial?.archived ?? false;
@override
void dispose() {
_name.dispose();
_amount.dispose();
_contribution.dispose();
_note.dispose();
super.dispose();
}
String? _decimalValidator(String? v, {bool required = true}) {
final text = (v ?? '').trim().replaceAll(',', '.');
if (text.isEmpty) return required ? 'Обязательное поле' : null;
return Decimal.tryParse(text) == null ? 'Нужно число' : null;
}
static String? _decimal(String text) {
final normalized = text.trim().replaceAll(',', '.').replaceAll(' ', '');
if (normalized.isEmpty) return null;
return Decimal.tryParse(normalized)?.toString();
}
@override
Widget build(BuildContext context) {
final scopes = ref.watch(scopesProvider).valueOrNull ?? const <ScopeOut>[];
return AlertDialog(
title: Text(widget.initial == null ? 'Новая цель' : 'Цель'),
content: SizedBox(
width: 420,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _name,
decoration: const InputDecoration(labelText: 'Название'),
validator: (v) =>
(v ?? '').trim().isEmpty ? 'Обязательное поле' : null,
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue:
scopes.any((s) => s.scope == _scope) || _scope == 'all'
? _scope
: 'all',
decoration: const InputDecoration(labelText: 'Что считаем'),
items: [
if (!scopes.any((s) => s.scope == 'all'))
const DropdownMenuItem(value: 'all', child: Text('Всё')),
for (final s in scopes)
DropdownMenuItem(value: s.scope, child: Text(s.name)),
],
onChanged: (v) => setState(() => _scope = v ?? 'all'),
),
const SizedBox(height: 12),
TextFormField(
controller: _amount,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(
labelText: 'Целевая сумма, ₽',
),
validator: _decimalValidator,
),
const SizedBox(height: 12),
TextFormField(
controller: _contribution,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(
labelText: 'Ежемесячный взнос, ₽',
helperText: 'необязательно',
),
validator: (v) => _decimalValidator(v, required: false),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Text(
_targetDate == null
? 'Целевая дата не задана'
: 'Целевая дата: ${ruDate(_targetDate!)}',
),
),
TextButton(
onPressed: () async {
final now = DateTime.now();
final picked = await showDatePicker(
context: context,
initialDate:
_targetDate ??
DateTime(now.year + 3, now.month, now.day),
firstDate: DateTime(now.year - 1),
lastDate: DateTime(now.year + 50),
);
if (picked != null)
setState(() => _targetDate = picked);
},
child: const Text('Выбрать'),
),
if (_targetDate != null)
IconButton(
tooltip: 'Убрать дату',
icon: const Icon(Icons.clear),
onPressed: () => setState(() => _targetDate = null),
),
],
),
const SizedBox(height: 12),
TextFormField(
controller: _note,
decoration: const InputDecoration(labelText: 'Заметка'),
),
if (widget.initial != null)
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('В архиве'),
value: _archived,
onChanged: (v) => setState(() => _archived = v),
),
],
),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Отмена'),
),
FilledButton(
onPressed: () {
if (!(_formKey.currentState?.validate() ?? false)) return;
Navigator.of(context).pop(
Goal(
id: widget.initial?.id ?? 0,
name: _name.text.trim(),
scope: _scope,
targetAmount: _decimal(_amount.text) ?? '0',
currency: widget.initial?.currency ?? 'RUB',
targetDate: _targetDate,
monthlyContribution: _decimal(_contribution.text),
note: _note.text.trim().isEmpty ? null : _note.text.trim(),
archived: _archived,
),
);
},
child: const Text('Сохранить'),
),
],
);
}
}