Files
efrilmandClaude Opus 5.5 f5e3075205 EnakPoint & EnakCoin wallet, PIN, outlet, order, community, reward
- Wallet EnakPoint & EnakCoin: beranda, riwayat, saldo kedaluwarsa, kode
  bayar, tukar EnakCoin, transfer
- PIN (buat, ganti, lupa), outlet, riwayat order, game spin
- Halaman community, webview, coming soon, dan ikon menu beranda baru
- Redesign Reward Saya: kartu tiket, filter chip, detail kode + QR
- Hapus customer point loader, menu, dan checkout lama
- Base URL ke enaklo-pos-api.altru.id

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:43:22 +07:00

381 lines
12 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:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import '../../../application/wallet/wallet_history_loader/wallet_history_loader_bloc.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/wallet/wallet.dart';
import '../../../injection.dart';
import '../../components/button/button.dart';
import '../../components/toast/flushbar.dart';
import 'wallet_ui.dart';
import 'widgets/wallet_transaction_tile.dart';
/// Riwayat mutasi (§4.2).
@RoutePage()
class WalletHistoryPage extends StatefulWidget implements AutoRouteWrapper {
const WalletHistoryPage({
super.key,
this.initialCurrency = WalletCurrency.point,
});
final WalletCurrency initialCurrency;
@override
State<WalletHistoryPage> createState() => _WalletHistoryPageState();
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (_) =>
getIt<WalletHistoryLoaderBloc>()
..add(WalletHistoryLoaderEvent.currencyChanged(initialCurrency)),
child: this,
);
}
class _WalletHistoryPageState extends State<WalletHistoryPage>
with SingleTickerProviderStateMixin {
late final TabController _tabController;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_tabController = TabController(
length: WalletCurrency.values.length,
vsync: this,
initialIndex: WalletCurrency.values.indexOf(widget.initialCurrency),
);
_tabController.addListener(() {
if (_tabController.indexIsChanging) return;
context.read<WalletHistoryLoaderBloc>().add(
WalletHistoryLoaderEvent.currencyChanged(
WalletCurrency.values[_tabController.index],
),
);
});
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_tabController.dispose();
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
final position = _scrollController.position;
if (position.pixels >= position.maxScrollExtent - 200) {
context.read<WalletHistoryLoaderBloc>().add(
const WalletHistoryLoaderEvent.loadedMore(),
);
}
}
Future<void> _openFilter(WalletHistoryLoaderState state) async {
final result = await showModalBottomSheet<_HistoryFilter>(
context: context,
isScrollControlled: true,
backgroundColor: AppColor.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (_) => _FilterSheet(
initial: _HistoryFilter(
types: state.types,
from: state.from,
to: state.to,
),
),
);
if (result == null || !mounted) return;
context.read<WalletHistoryLoaderBloc>().add(
WalletHistoryLoaderEvent.filterChanged(
types: result.types,
from: result.from,
to: result.to,
),
);
}
@override
Widget build(BuildContext context) {
return BlocConsumer<WalletHistoryLoaderBloc, WalletHistoryLoaderState>(
listenWhen: (p, c) =>
p.failureOption != c.failureOption && c.items.isNotEmpty,
listener: (context, state) => state.failureOption.fold(
() {},
(f) => AppFlushbar.showError(context, walletFailureMessage(f)),
),
builder: (context, state) {
return Scaffold(
backgroundColor: AppColor.background,
appBar: AppBar(
title: const Text('Riwayat'),
actions: [
IconButton(
onPressed: () => _openFilter(state),
icon: Badge(
isLabelVisible: state.hasFilter,
backgroundColor: AppColor.primary,
smallSize: 8,
child: const Icon(Icons.filter_list),
),
),
],
bottom: TabBar(
controller: _tabController,
tabs: WalletCurrency.values
.map((currency) => Tab(text: currency.label))
.toList(),
),
),
body: Column(
children: [
if (state.hasFilter) _buildActiveFilter(state),
Expanded(child: _buildList(state)),
],
),
);
},
);
}
Widget _buildActiveFilter(WalletHistoryLoaderState state) {
final parts = <String>[
if (state.types.isNotEmpty) '${state.types.length} tipe',
if (state.from != null || state.to != null)
'${state.from == null ? '…' : formatShortDate(state.from!)} – '
'${state.to == null ? '…' : formatShortDate(state.to!)}',
];
return Container(
color: AppColor.white,
padding: const EdgeInsets.fromLTRB(16, 4, 4, 4),
child: Row(
children: [
Expanded(
child: Text(
'Filter: ${parts.join(' · ')}',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
),
TextButton(
onPressed: () => context.read<WalletHistoryLoaderBloc>().add(
const WalletHistoryLoaderEvent.filterChanged(types: []),
),
child: Text(
'Hapus filter',
style: AppStyle.sm.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Widget _buildList(WalletHistoryLoaderState state) {
if (state.isFetching && state.items.isEmpty) {
return const Center(
child: SpinKitFadingCircle(color: AppColor.primary, size: 36),
);
}
final failure = state.failureOption.toNullable();
if (failure != null && state.items.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(walletFailureMessage(failure), style: AppStyle.md),
const SizedBox(height: 16),
AppOutlineButton(
title: 'Coba lagi',
width: 160,
onPressed: () => context.read<WalletHistoryLoaderBloc>().add(
const WalletHistoryLoaderEvent.refreshed(),
),
),
],
),
);
}
return RefreshIndicator(
color: AppColor.primary,
onRefresh: () async {
final bloc = context.read<WalletHistoryLoaderBloc>();
bloc.add(const WalletHistoryLoaderEvent.refreshed());
await bloc.stream.firstWhere((s) => !s.isFetching);
},
child: state.items.isEmpty
? ListView(
children: [
const SizedBox(height: 120),
Text(
state.hasFilter
? 'Tidak ada transaksi yang cocok dengan filter'
: 'Belum ada transaksi',
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
],
)
: ListView.separated(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: state.items.length + (state.hasMore ? 1 : 0),
separatorBuilder: (_, __) =>
const Divider(height: 1, color: AppColor.borderLight),
itemBuilder: (context, index) {
if (index >= state.items.length) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: SpinKitFadingCircle(
color: AppColor.primary,
size: 24,
),
),
);
}
return Container(
color: AppColor.white,
child: WalletTransactionTile(transaction: state.items[index]),
);
},
),
);
}
}
class _HistoryFilter {
const _HistoryFilter({required this.types, this.from, this.to});
final List<WalletTransactionType> types;
final DateTime? from;
final DateTime? to;
}
class _FilterSheet extends StatefulWidget {
const _FilterSheet({required this.initial});
final _HistoryFilter initial;
@override
State<_FilterSheet> createState() => _FilterSheetState();
}
class _FilterSheetState extends State<_FilterSheet> {
late final Set<WalletTransactionType> _types = {...widget.initial.types};
late DateTime? _from = widget.initial.from;
late DateTime? _to = widget.initial.to;
Future<void> _pickRange() async {
final now = DateTime.now();
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(now.year - 5),
lastDate: now,
initialDateRange: _from != null && _to != null
? DateTimeRange(start: _from!, end: _to!)
: null,
);
if (picked == null) return;
setState(() {
_from = picked.start;
_to = picked.end;
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filter riwayat',
style: AppStyle.xl.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 16),
Text(
'Tipe',
style: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: WalletTransactionType.filterable.map((type) {
final selected = _types.contains(type);
return FilterChip(
label: Text(type.label, style: AppStyle.sm),
selected: selected,
selectedColor: AppColor.primaryWithOpacity(0.15),
checkmarkColor: AppColor.primary,
onSelected: (value) => setState(
() => value ? _types.add(type) : _types.remove(type),
),
);
}).toList(),
),
const SizedBox(height: 16),
Text(
'Tanggal',
style: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _pickRange,
icon: const Icon(Icons.date_range),
label: Text(
_from == null
? 'Pilih rentang tanggal'
: '${formatShortDate(_from!)} – ${formatShortDate(_to!)}',
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: AppOutlineButton(
title: 'Reset',
onPressed: () => Navigator.of(
context,
).pop(const _HistoryFilter(types: [])),
),
),
const SizedBox(width: 12),
Expanded(
child: AppElevatedButton(
title: 'Terapkan',
onPressed: () => Navigator.of(context).pop(
_HistoryFilter(
types: WalletTransactionType.filterable
.where(_types.contains)
.toList(),
from: _from,
to: _to,
),
),
),
),
],
),
],
),
),
);
}
}