Files
fin-tracker/app/lib/features/pending/widgets/link_instrument_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

120 lines
3.7 KiB
Dart

import 'dart:async';
import 'package:fintracker_api/fintracker_api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../portfolio/labels.dart' show assetClassLabel;
import '../providers.dart';
/// Search-and-pick over `GET /instruments?q=`. Deliberately dumb: it shows what the search
/// returned and nothing else — no "probably this one" pre-selection, no highlighted best
/// guess. The binding is the user's statement, not the app's inference.
class LinkInstrumentDialog extends ConsumerStatefulWidget {
const LinkInstrumentDialog({required this.initialQuery, super.key});
/// Prefilled search text (the ISIN or ticker from the report). It only fills the search
/// box — nothing is selected until the user taps a row.
final String initialQuery;
@override
ConsumerState<LinkInstrumentDialog> createState() => _LinkInstrumentDialogState();
}
class _LinkInstrumentDialogState extends ConsumerState<LinkInstrumentDialog> {
late final TextEditingController _controller =
TextEditingController(text: widget.initialQuery);
String _query = '';
Timer? _debounce;
@override
void initState() {
super.initState();
_query = widget.initialQuery;
}
@override
void dispose() {
_debounce?.cancel();
_controller.dispose();
super.dispose();
}
void _onChanged(String value) {
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted) setState(() => _query = value);
});
}
@override
Widget build(BuildContext context) {
final results = ref.watch(instrumentSearchProvider(_query));
return AlertDialog(
title: const Text('Привязать к инструменту'),
content: SizedBox(
width: 520,
height: 420,
child: Column(
children: [
TextField(
controller: _controller,
autofocus: true,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: 'Тикер, ISIN или название',
border: OutlineInputBorder(),
isDense: true,
),
onChanged: _onChanged,
),
const SizedBox(height: 12),
Expanded(
child: results.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('$e')),
data: (rows) {
if (_query.trim().length < 2) {
return const Center(child: Text('Введите минимум 2 символа'));
}
if (rows.isEmpty) {
return const Center(child: Text('Ничего не найдено'));
}
return ListView.builder(
itemCount: rows.length,
itemBuilder: (context, i) => _row(rows[i]),
);
},
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Отмена'),
),
],
);
}
Widget _row(InstrumentOut instrument) {
final subtitle = [
assetClassLabel(instrument.assetClass),
if (instrument.isin != null) instrument.isin!,
if (instrument.board != null) instrument.board!,
instrument.currency,
].join(' · ');
return ListTile(
dense: true,
title: Text([
if (instrument.ticker != null) instrument.ticker!,
instrument.name,
].join(' · ')),
subtitle: Text(subtitle),
onTap: () => Navigator.of(context).pop(instrument.id),
);
}
}