Files
fin-tracker/app/lib/features/goals/goal_edit_dialog.dart
T
Dmitry b69bb4a0c9 feat(app): экраны импорта отчётов, целей, доходов, ребалансировки, налогов и бенчмарков
Импорт (/imports, /instruments/pending) и фаза 4 (/goals, /income,
/rebalance, /tax, аналитика-хаб с benchmarks_card) — по
docs/ai/import-contract.md и docs/ai/phase4-contract.md. file_picker для
загрузки отчёта.
2026-09-19 10:44:38 +03:00

172 lines
6.9 KiB
Dart
Raw 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('Сохранить'),
),
],
);
}
}