Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
167 lines
4.6 KiB
Dart
167 lines
4.6 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:fintracker_api/fintracker_api.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../core/api/api_client.dart';
|
|
|
|
/// What the События screen asks the ledger for. `instrumentId` has no control of its own —
|
|
/// the instrument card already lists an instrument's own events — but it is carried here
|
|
/// so navigating in with a preset instrument stays a one-line change.
|
|
class EventsFilter {
|
|
const EventsFilter({
|
|
this.q,
|
|
this.from,
|
|
this.to,
|
|
this.accountId,
|
|
this.instrumentId,
|
|
this.kind,
|
|
this.status,
|
|
this.externalFlow,
|
|
});
|
|
|
|
final String? q;
|
|
final DateTime? from;
|
|
final DateTime? to;
|
|
final int? accountId;
|
|
final int? instrumentId;
|
|
final EventKind? kind;
|
|
final EventStatus? status;
|
|
final bool? externalFlow;
|
|
|
|
EventsFilter copyWith({
|
|
String? Function()? q,
|
|
DateTime? Function()? from,
|
|
DateTime? Function()? to,
|
|
int? Function()? accountId,
|
|
int? Function()? instrumentId,
|
|
EventKind? Function()? kind,
|
|
EventStatus? Function()? status,
|
|
bool? Function()? externalFlow,
|
|
}) {
|
|
return EventsFilter(
|
|
q: q != null ? q() : this.q,
|
|
from: from != null ? from() : this.from,
|
|
to: to != null ? to() : this.to,
|
|
accountId: accountId != null ? accountId() : this.accountId,
|
|
instrumentId: instrumentId != null ? instrumentId() : this.instrumentId,
|
|
kind: kind != null ? kind() : this.kind,
|
|
status: status != null ? status() : this.status,
|
|
externalFlow: externalFlow != null ? externalFlow() : this.externalFlow,
|
|
);
|
|
}
|
|
}
|
|
|
|
class EventsState {
|
|
const EventsState({
|
|
this.items = const [],
|
|
this.page = 1,
|
|
this.pageSize = 50,
|
|
this.total = 0,
|
|
this.loading = false,
|
|
this.loadingMore = false,
|
|
this.error,
|
|
});
|
|
|
|
final List<EventOut> items;
|
|
final int page;
|
|
final int pageSize;
|
|
final int total;
|
|
final bool loading;
|
|
final bool loadingMore;
|
|
final Object? error;
|
|
|
|
bool get hasMore => items.length < total;
|
|
|
|
EventsState copyWith({
|
|
List<EventOut>? items,
|
|
int? page,
|
|
int? pageSize,
|
|
int? total,
|
|
bool? loading,
|
|
bool? loadingMore,
|
|
Object? error,
|
|
bool clearError = false,
|
|
}) {
|
|
return EventsState(
|
|
items: items ?? this.items,
|
|
page: page ?? this.page,
|
|
pageSize: pageSize ?? this.pageSize,
|
|
total: total ?? this.total,
|
|
loading: loading ?? this.loading,
|
|
loadingMore: loadingMore ?? this.loadingMore,
|
|
error: clearError ? null : (error ?? this.error),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Filters + paginated results for События, the same shape Операции uses: `refresh()`
|
|
/// restarts from page 1 (on build and on every filter change), `loadMore()` appends the
|
|
/// next page for infinite scroll.
|
|
class EventsController extends Notifier<EventsState> {
|
|
EventsFilter filter = const EventsFilter();
|
|
|
|
@override
|
|
EventsState build() {
|
|
Future.microtask(refresh);
|
|
return const EventsState(loading: true);
|
|
}
|
|
|
|
Future<void> setFilter(EventsFilter Function(EventsFilter) update) {
|
|
filter = update(filter);
|
|
return refresh();
|
|
}
|
|
|
|
Future<EventPage> _fetch(int page) async {
|
|
final r = await ref
|
|
.read(apiProvider)
|
|
.getEventsApi()
|
|
.eventsList(
|
|
from: filter.from,
|
|
to: filter.to,
|
|
accountId: filter.accountId,
|
|
instrumentId: filter.instrumentId,
|
|
kind: filter.kind,
|
|
status: filter.status,
|
|
externalFlow: filter.externalFlow,
|
|
q: (filter.q == null || filter.q!.isEmpty) ? null : filter.q,
|
|
page: page,
|
|
pageSize: state.pageSize,
|
|
);
|
|
return r.data!;
|
|
}
|
|
|
|
Future<void> refresh() async {
|
|
state = state.copyWith(loading: true, clearError: true);
|
|
try {
|
|
final page = await _fetch(1);
|
|
state = EventsState(
|
|
items: page.items,
|
|
page: page.page,
|
|
pageSize: page.pageSize,
|
|
total: page.total,
|
|
);
|
|
} on DioException catch (e) {
|
|
state = state.copyWith(loading: false, error: e);
|
|
}
|
|
}
|
|
|
|
Future<void> loadMore() async {
|
|
if (state.loading || state.loadingMore || !state.hasMore) return;
|
|
state = state.copyWith(loadingMore: true, clearError: true);
|
|
try {
|
|
final page = await _fetch(state.page + 1);
|
|
state = state.copyWith(
|
|
items: [...state.items, ...page.items],
|
|
page: page.page,
|
|
total: page.total,
|
|
loadingMore: false,
|
|
);
|
|
} on DioException catch (e) {
|
|
state = state.copyWith(loadingMore: false, error: e);
|
|
}
|
|
}
|
|
}
|
|
|
|
final eventsControllerProvider =
|
|
NotifierProvider<EventsController, EventsState>(EventsController.new);
|