import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/theme/halo_tokens.dart'; import '../providers/selesai_history_provider.dart'; import '../widgets/chat_row.dart'; /// Chat-Tab > selesai sub-tab. /// /// Cursor-paginated past sessions. Tap → read-only transcript at /// `/chat/transcript/:sessionId` (renamed from the retired /chat/history/:id). class SelesaiView extends ConsumerStatefulWidget { const SelesaiView({super.key}); @override ConsumerState createState() => _SelesaiViewState(); } class _SelesaiViewState extends ConsumerState { final _scroll = ScrollController(); @override void initState() { super.initState(); _scroll.addListener(_onScroll); } @override void dispose() { _scroll.removeListener(_onScroll); _scroll.dispose(); super.dispose(); } void _onScroll() { // Trigger load-more when within ~400px of the bottom. The notifier no-ops // if there's no cursor or another fetch is in flight. if (!_scroll.hasClients) return; final remaining = _scroll.position.maxScrollExtent - _scroll.position.pixels; if (remaining < 400) { ref.read(selesaiHistoryProvider.notifier).loadMore(); } } @override Widget build(BuildContext context) { final async = ref.watch(selesaiHistoryProvider); return RefreshIndicator( onRefresh: () => ref.read(selesaiHistoryProvider.notifier).refresh(), color: HaloTokens.brand, child: async.when( loading: () => const _Loading(), error: (e, _) => _CenteredMessage(text: 'gagal memuat: $e'), data: (state) { if (state.items.isEmpty) { return const _CenteredMessage(text: 'belum ada riwayat curhat'); } return ListView.builder( controller: _scroll, padding: const EdgeInsets.symmetric( horizontal: HaloSpacing.s20, vertical: HaloSpacing.s12, ), itemCount: state.items.length + (state.hasMore ? 1 : 0), itemBuilder: (_, i) { if (i >= state.items.length) { return const Padding( padding: EdgeInsets.symmetric(vertical: HaloSpacing.s16), child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: HaloTokens.brand, ), ), ), ); } final item = state.items[i]; return ChatRow( seed: item.mitraName.codeUnits.fold(0, (a, b) => a + b), name: item.mitraName, preview: item.previewText, trailing: item.endedAt == null ? null : _relativeTime(item.endedAt!), chips: [ if (item.durationMinutes != null) DurationChip(minutes: item.durationMinutes!), ], isCall: item.mode == 'call', onTap: () => context.push('/chat/transcript/${item.sessionId}'), ); }, ); }, ), ); } } String _relativeTime(DateTime when) { final delta = DateTime.now().difference(when); if (delta.inSeconds < 60) return 'baru aja'; if (delta.inMinutes < 60) return '${delta.inMinutes} mnt lalu'; if (delta.inHours < 24) return '${delta.inHours} jam lalu'; if (delta.inDays < 7) return '${delta.inDays} hari lalu'; return '${(delta.inDays / 7).floor()} mgg lalu'; } class _Loading extends StatelessWidget { const _Loading(); @override Widget build(BuildContext context) => const Center( child: CircularProgressIndicator(color: HaloTokens.brand), ); } class _CenteredMessage extends StatelessWidget { final String text; const _CenteredMessage({required this.text}); @override Widget build(BuildContext context) { return ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: HaloSpacing.s20, vertical: HaloSpacing.s64, ), child: Text( text, textAlign: TextAlign.center, style: const TextStyle( fontFamily: HaloTokens.fontBody, fontSize: 13, color: HaloTokens.inkMuted, ), ), ), ], ); } }