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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5954510b93
commit
f5e3075205
@@ -4,12 +4,14 @@ import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/auth/auth_bloc.dart';
|
||||
import '../../../../../application/customer/customer_point_loader/customer_point_loader_bloc.dart';
|
||||
import '../../../../../common/constant/app_constant.dart';
|
||||
import '../../../../../application/outlet/outlet_loader/outlet_loader_bloc.dart';
|
||||
import '../../../../../application/wallet/wallet_loader/wallet_loader_bloc.dart';
|
||||
// import '../../../../../common/constant/app_constant.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../components/image/image.dart';
|
||||
import '../../../../router/app_router.gr.dart';
|
||||
import 'widgets/banner_card.dart';
|
||||
// import 'widgets/banner_card.dart';
|
||||
import 'widgets/outlet_section.dart';
|
||||
import 'widgets/point_card.dart';
|
||||
import 'widgets/popular_merchant_section.dart';
|
||||
import 'widgets/service_section.dart';
|
||||
@@ -38,9 +40,8 @@ class _HomePageState extends State<HomePage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (context.read<AuthBloc>().state.isAuthenticated) {
|
||||
context.read<CustomerPointLoaderBloc>().add(
|
||||
CustomerPointLoaderEvent.fetched(),
|
||||
);
|
||||
context.read<WalletLoaderBloc>().add(const WalletLoaderEvent.fetched());
|
||||
context.read<OutletLoaderBloc>().add(const OutletLoaderEvent.fetched());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,16 +53,17 @@ class _HomePageState extends State<HomePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderSection(),
|
||||
const SizedBox(height: 70),
|
||||
const SizedBox(height: 8),
|
||||
HomeServiceSection(),
|
||||
HomeBanner(
|
||||
title: '🎰 My Rewards',
|
||||
subtitle:
|
||||
'Main dan tepat menangkan produk gratis dari ${AppConstant.appName}.',
|
||||
actionText: 'Mainkan Sekarang',
|
||||
onTap: () => context.router.push(DrawRoute()),
|
||||
),
|
||||
// HomeBanner(
|
||||
// title: '🎰 My Rewards',
|
||||
// subtitle:
|
||||
// 'Main dan tepat menangkan produk gratis dari ${AppConstant.appName}.',
|
||||
// actionText: 'Mainkan Sekarang',
|
||||
// onTap: () => context.router.push(DrawRoute()),
|
||||
// ),
|
||||
HomePopularMerchantSection(),
|
||||
const HomeOutletSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -81,18 +83,11 @@ class _HomePageState extends State<HomePage> {
|
||||
top: 225,
|
||||
child: _buildCarouselIndicators(),
|
||||
),
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 240,
|
||||
child: HomePointCard(
|
||||
point: context
|
||||
.read<CustomerPointLoaderBloc>()
|
||||
.state
|
||||
.customerPoint
|
||||
.totalPoints
|
||||
.toString(),
|
||||
),
|
||||
// Tidak di-Positioned supaya tinggi kartu ikut dihitung, sehingga jarak
|
||||
// ke menu di bawahnya tetap walau isi kartu bertambah.
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 240, 16, 0),
|
||||
child: HomePointCard(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../../application/auth/auth_bloc.dart';
|
||||
import '../../../../../../application/outlet/outlet_loader/outlet_loader_bloc.dart';
|
||||
import '../../../../../router/app_router.gr.dart';
|
||||
import '../../../../outlet/widgets/outlet_tile.dart';
|
||||
import 'section_header.dart';
|
||||
|
||||
/// Seksi "Outlet Kami" (§4.4). Hanya untuk customer yang sudah masuk, karena
|
||||
/// daftar outlet mengikuti organisasi akunnya.
|
||||
class HomeOutletSection extends StatelessWidget {
|
||||
const HomeOutletSection({super.key});
|
||||
|
||||
static const int _maxShown = 3;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isAuthenticated = context.select<AuthBloc, bool>(
|
||||
(bloc) => bloc.state.isAuthenticated,
|
||||
);
|
||||
if (!isAuthenticated) return const SizedBox.shrink();
|
||||
|
||||
return BlocBuilder<OutletLoaderBloc, OutletLoaderState>(
|
||||
buildWhen: (p, c) => p.outlets != c.outlets,
|
||||
builder: (context, state) {
|
||||
if (state.outlets.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
HomeSectionHeader(
|
||||
eyebrow: 'Outlet',
|
||||
title: 'Outlet Kami',
|
||||
onSeeAll: state.outlets.length > _maxShown
|
||||
? () => context.router.push(const OutletRoute())
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
for (final outlet in state.outlets.take(_maxShown))
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 10),
|
||||
child: OutletTile(outlet: outlet),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,165 +3,64 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../../application/auth/auth_bloc.dart';
|
||||
import '../../../../../../application/wallet/wallet_loader/wallet_loader_bloc.dart';
|
||||
import '../../../../../../common/extension/extension.dart';
|
||||
import '../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../domain/wallet/wallet.dart';
|
||||
import '../../../../../router/app_router.gr.dart';
|
||||
import '../../../../wallet/wallet_ui.dart';
|
||||
|
||||
class HomePointCard extends StatelessWidget {
|
||||
final String point;
|
||||
const HomePointCard({super.key, required this.point});
|
||||
const HomePointCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final summary = context.watch<WalletLoaderBloc>().state.summary;
|
||||
|
||||
return BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, state) {
|
||||
return GestureDetector(
|
||||
onTap: () => state.isAuthenticated
|
||||
? context.router.push(CoinRoute())
|
||||
: context.router.push(OnboardingRoute()),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.textLight.withOpacity(0.15),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildCoinPattern(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
!state.isAuthenticated
|
||||
? Expanded(
|
||||
child: Text(
|
||||
'Hi, Selamat Datang Di Enaklo',
|
||||
style: AppStyle.md.copyWith(
|
||||
color: AppColor.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary,
|
||||
borderRadius: BorderRadius.circular(
|
||||
25,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.stars,
|
||||
color: AppColor.white,
|
||||
size: 18,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'$point Poin',
|
||||
style: AppStyle.md.copyWith(
|
||||
color: AppColor.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Kamu punya $point poin',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
final isAuthenticated = state.isAuthenticated;
|
||||
|
||||
SizedBox(
|
||||
width: 120,
|
||||
height: 40,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildCoin(
|
||||
right: 0,
|
||||
top: 0,
|
||||
size: 24,
|
||||
color: Colors.amber,
|
||||
),
|
||||
_buildCoin(
|
||||
right: 20,
|
||||
top: 8,
|
||||
size: 20,
|
||||
color: Colors.orange,
|
||||
),
|
||||
_buildCoin(
|
||||
right: 40,
|
||||
top: 4,
|
||||
size: 18,
|
||||
color: Colors.amber,
|
||||
),
|
||||
_buildCoin(
|
||||
right: 60,
|
||||
top: 12,
|
||||
size: 16,
|
||||
color: Colors.orange,
|
||||
),
|
||||
_buildCoin(
|
||||
right: 80,
|
||||
top: 8,
|
||||
size: 14,
|
||||
color: Colors.amber,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
state.isAuthenticated
|
||||
? 'Tukarkan poinmu dengan hadiah menarik'
|
||||
: 'Silahkan login untuk tukarkan poinmu',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textPrimary,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
return Material(
|
||||
color: AppColor.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
shadowColor: AppColor.textLight.withOpacity(0.3),
|
||||
elevation: 6,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => isAuthenticated
|
||||
? context.router.push(WalletRoute())
|
||||
: context.router.push(OnboardingRoute()),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
child: Column(
|
||||
children: [
|
||||
isAuthenticated ? _buildBalances(summary) : _buildGuest(),
|
||||
const SizedBox(height: 12),
|
||||
const Divider(height: 1, color: AppColor.borderLight),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
isAuthenticated
|
||||
? 'Bayar di kasir, tukar, transfer & main game'
|
||||
: 'Silahkan login untuk melihat EnakPoint kamu',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
size: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
color: AppColor.textSecondary,
|
||||
size: 14,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -169,90 +68,129 @@ class HomePointCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoinPattern() {
|
||||
return Positioned.fill(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
right: -20,
|
||||
top: -10,
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Widget _buildBalances(WalletSummary summary) {
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _BalanceTile(
|
||||
currency: WalletCurrency.point,
|
||||
balance: summary.pointBalance,
|
||||
caption: pointDiscountLabel(summary.pointDiscountValue),
|
||||
),
|
||||
Positioned(
|
||||
right: 40,
|
||||
top: 30,
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
const VerticalDivider(
|
||||
width: 24,
|
||||
thickness: 1,
|
||||
color: AppColor.borderLight,
|
||||
),
|
||||
Expanded(
|
||||
child: _BalanceTile(
|
||||
currency: WalletCurrency.coin,
|
||||
balance: summary.coinBalance,
|
||||
),
|
||||
Positioned(
|
||||
left: -15,
|
||||
bottom: -20,
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withOpacity(0.08),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 60,
|
||||
bottom: 10,
|
||||
child: Container(
|
||||
width: 15,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.12),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoin({
|
||||
required double right,
|
||||
required double top,
|
||||
required double size,
|
||||
required Color color,
|
||||
}) {
|
||||
return Positioned(
|
||||
right: right,
|
||||
top: top,
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
child: Center(
|
||||
Widget _buildGuest() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'\$',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: size * 0.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
'Hi, Selamat Datang Di Enaklo',
|
||||
style: AppStyle.md.copyWith(
|
||||
color: AppColor.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(
|
||||
width: 64,
|
||||
height: 40,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 4,
|
||||
child: WalletCurrencyIcon(
|
||||
currency: WalletCurrency.coin,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: WalletCurrencyIcon(
|
||||
currency: WalletCurrency.point,
|
||||
size: 36,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Satu kolom saldo: ikon, nama mata uang, dan jumlahnya.
|
||||
class _BalanceTile extends StatelessWidget {
|
||||
const _BalanceTile({
|
||||
required this.currency,
|
||||
required this.balance,
|
||||
this.caption,
|
||||
});
|
||||
|
||||
final WalletCurrency currency;
|
||||
final int balance;
|
||||
final String? caption;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
WalletCurrencyIcon(currency: currency, size: 36),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
currency.label,
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
balance.thousandFormat,
|
||||
style: AppStyle.xl.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
if (caption != null)
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
caption!,
|
||||
style: AppStyle.xs.copyWith(
|
||||
fontSize: 10,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,156 +1,82 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../sample/sample_data.dart';
|
||||
import '../../../../../components/image/image.dart';
|
||||
|
||||
/// Banner brand partner di seksi "Brand Kami".
|
||||
class HomePopularMerchantCard extends StatelessWidget {
|
||||
final MerchantModel merchant;
|
||||
final double width;
|
||||
final double height;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const HomePopularMerchantCard({
|
||||
super.key,
|
||||
this.onTap,
|
||||
required this.merchant,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
static const double _radius = 12;
|
||||
|
||||
Widget _image(BoxFit fit) => merchant.imageUrl.startsWith('http')
|
||||
? Image.network(
|
||||
merchant.imageUrl,
|
||||
fit: fit,
|
||||
errorBuilder: (_, _, _) => const ImagePlaceholder(),
|
||||
)
|
||||
: Image.asset(
|
||||
merchant.imageUrl,
|
||||
fit: fit,
|
||||
errorBuilder: (_, _, _) => const ImagePlaceholder(),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: merchant.name,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(_radius),
|
||||
border: Border.all(color: AppColor.borderLight),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.black.withOpacity(0.06),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
color: AppColor.black.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Image Container
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: AppColor.border,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: merchant.imageUrl.startsWith('http')
|
||||
? Image.network(
|
||||
merchant.imageUrl,
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return ImagePlaceholder(width: 60, height: 60);
|
||||
},
|
||||
)
|
||||
: Image.asset(
|
||||
merchant.imageUrl,
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return ImagePlaceholder(width: 60, height: 60);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Title and Category
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
merchant.name,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
Text(
|
||||
merchant.category,
|
||||
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Rating and Status
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
// Status Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: merchant.isOpen ? AppColor.success : AppColor.error,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
merchant.isOpen ? 'OPEN' : 'CLOSED',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(_radius),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Logo brand ada yang persegi: tampilkan utuh, sisa ruangnya
|
||||
// diisi versi blur dari gambar yang sama agar tidak terpotong.
|
||||
ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(
|
||||
sigmaX: 18,
|
||||
sigmaY: 18,
|
||||
tileMode: TileMode.clamp,
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Rating
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.star, size: 12, color: AppColor.warning),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
merchant.rating.toString(),
|
||||
style: AppStyle.xs.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: _image(BoxFit.cover),
|
||||
),
|
||||
_image(BoxFit.contain),
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(onTap: onTap),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,53 +1,51 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../sample/sample_data.dart';
|
||||
import '../../../../../router/app_router.gr.dart';
|
||||
import 'popular_merchant_card.dart';
|
||||
import 'section_header.dart';
|
||||
|
||||
/// Seksi "Brand Kami": banner brand partner yang bisa digeser.
|
||||
class HomePopularMerchantSection extends StatelessWidget {
|
||||
const HomePopularMerchantSection({super.key});
|
||||
|
||||
static const double _cardWidth = 240;
|
||||
static const double _cardHeight = 136;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Popular Merchants',
|
||||
style: AppStyle.xl.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Spacer(),
|
||||
InkWell(
|
||||
onTap: () => context.router.push(MerchantRoute()),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Lihat Semua',
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
HomeSectionHeader(
|
||||
eyebrow: 'Partner',
|
||||
title: 'Brand Kami',
|
||||
onSeeAll: () => context.router.push(MerchantRoute()),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
...List.generate(
|
||||
merchants.length,
|
||||
(index) => HomePopularMerchantCard(merchant: merchants[index]),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
// Ruang ekstra agar bayangan kartu tidak terpotong.
|
||||
height: _cardHeight + 16,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
|
||||
itemCount: merchants.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(width: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final merchant = merchants[index];
|
||||
return HomePopularMerchantCard(
|
||||
merchant: merchant,
|
||||
width: _cardWidth,
|
||||
height: _cardHeight,
|
||||
onTap: () => context.router.push(
|
||||
MerchantDetailRoute(merchant: merchant),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../../common/theme/theme.dart';
|
||||
|
||||
/// Judul seksi Home: pemisah abu-abu, label kecil merah, judul, dan
|
||||
/// "Lihat Semua" opsional.
|
||||
class HomeSectionHeader extends StatelessWidget {
|
||||
const HomeSectionHeader({
|
||||
super.key,
|
||||
required this.eyebrow,
|
||||
required this.title,
|
||||
this.onSeeAll,
|
||||
});
|
||||
|
||||
final String eyebrow;
|
||||
final String title;
|
||||
final VoidCallback? onSeeAll;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(height: 6, color: AppColor.borderLight),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 3,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
eyebrow.toUpperCase(),
|
||||
style: AppStyle.xs.copyWith(
|
||||
fontSize: 10,
|
||||
color: AppColor.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: AppStyle.xl.copyWith(
|
||||
height: 1.2,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (onSeeAll != null)
|
||||
TextButton(
|
||||
onPressed: onSeeAll,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColor.primaryDark,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Lihat Semua',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.primaryDark,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.arrow_right_alt_rounded, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,137 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../../common/data/service_data.dart';
|
||||
import '../../../../../../application/auth/auth_bloc.dart';
|
||||
import '../../../../../../common/constant/app_constant.dart';
|
||||
import '../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../components/card/gradient_card.dart';
|
||||
import '../../../../../components/assets/assets.gen.dart';
|
||||
import '../../../../../components/toast/flushbar.dart';
|
||||
import '../../../../../router/app_router.gr.dart';
|
||||
|
||||
class _HomeMenu {
|
||||
const _HomeMenu({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
this.route,
|
||||
this.requiresLogin = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final AssetGenImage icon;
|
||||
|
||||
/// `null` berarti fitur belum tersedia.
|
||||
final PageRouteInfo? route;
|
||||
final bool requiresLogin;
|
||||
}
|
||||
|
||||
class HomeServiceSection extends StatelessWidget {
|
||||
const HomeServiceSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: List.generate(services.length, (index) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: index == 0 ? 12 : 0),
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
context.router.push(MenuRoute(service: services[index])),
|
||||
child: GradientCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: _content(services[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
static final List<_HomeMenu> _menus = [
|
||||
_HomeMenu(
|
||||
label: 'EnakGame',
|
||||
icon: Assets.icons.enakgameIcon,
|
||||
route: WebViewRoute(
|
||||
title: 'EnakGame',
|
||||
url: AppConstant.gameUrl,
|
||||
fullscreen: true,
|
||||
backgroundColor: AppConstant.gameBackgroundColor,
|
||||
),
|
||||
);
|
||||
),
|
||||
_HomeMenu(
|
||||
label: 'EnakTransfer',
|
||||
icon: Assets.icons.enaktransferIcon,
|
||||
route: const TransferRoute(),
|
||||
requiresLogin: true,
|
||||
),
|
||||
_HomeMenu(
|
||||
label: 'EnakReward',
|
||||
icon: Assets.icons.enakrewardIcon,
|
||||
route: const RewardRoute(),
|
||||
),
|
||||
_HomeMenu(
|
||||
label: 'EnakPromo',
|
||||
icon: Assets.icons.enakpromoIcon,
|
||||
route: ComingSoonRoute(
|
||||
title: 'EnakPromo',
|
||||
iconPath: Assets.icons.enakpromoIcon.path,
|
||||
),
|
||||
),
|
||||
_HomeMenu(
|
||||
label: 'EnakShop',
|
||||
icon: Assets.icons.enakshopIcon,
|
||||
route: ComingSoonRoute(
|
||||
title: 'EnakShop',
|
||||
iconPath: Assets.icons.enakshopIcon.path,
|
||||
),
|
||||
),
|
||||
_HomeMenu(
|
||||
label: 'EnakNews',
|
||||
icon: Assets.icons.enaknewsIcon,
|
||||
route: ComingSoonRoute(
|
||||
title: 'EnakNews',
|
||||
iconPath: Assets.icons.enaknewsIcon.path,
|
||||
),
|
||||
),
|
||||
_HomeMenu(
|
||||
label: 'EnakCommunity',
|
||||
icon: Assets.icons.enakcommunityIcon,
|
||||
route: const CommunityRoute(),
|
||||
),
|
||||
];
|
||||
|
||||
void _open(BuildContext context, _HomeMenu menu) {
|
||||
final route = menu.route;
|
||||
if (route == null) {
|
||||
AppFlushbar.showSuccess(context, '${menu.label} segera hadir');
|
||||
return;
|
||||
}
|
||||
|
||||
final isAuthenticated = context.read<AuthBloc>().state.isAuthenticated;
|
||||
if (menu.requiresLogin && !isAuthenticated) {
|
||||
context.router.push(OnboardingRoute());
|
||||
return;
|
||||
}
|
||||
|
||||
context.router.push(route);
|
||||
}
|
||||
|
||||
Stack _content(Service service) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
right: 4,
|
||||
top: 4,
|
||||
child: Image.asset(service.imagePath, width: 60, height: 60),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 32, 16, 16),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisExtent: 76,
|
||||
),
|
||||
itemCount: _menus.length,
|
||||
itemBuilder: (context, index) {
|
||||
final menu = _menus[index];
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(AppValue.borderRadius),
|
||||
onTap: () => _open(context, menu),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
service.name,
|
||||
style: AppStyle.lg.copyWith(
|
||||
color: AppColor.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
service.description,
|
||||
style: AppStyle.md.copyWith(
|
||||
color: AppColor.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
const SizedBox(height: 4),
|
||||
menu.icon.image(width: 44, height: 44, fit: BoxFit.contain),
|
||||
const SizedBox(height: 6),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
menu.label,
|
||||
maxLines: 1,
|
||||
style: AppStyle.xs.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,355 +1,215 @@
|
||||
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/auth/auth_bloc.dart';
|
||||
import '../../../../../application/order/order_loader/order_loader_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../../injection.dart';
|
||||
import '../../../../components/button/button.dart';
|
||||
import '../../../../components/toast/flushbar.dart';
|
||||
import '../../../../router/app_router.gr.dart';
|
||||
import '../../../wallet/wallet_ui.dart';
|
||||
import 'widgets/order_card.dart';
|
||||
|
||||
// Model untuk Order
|
||||
class Order {
|
||||
final String id;
|
||||
final String customerName;
|
||||
final DateTime orderDate;
|
||||
final List<OrderItem> items;
|
||||
final double totalAmount;
|
||||
final OrderStatus status;
|
||||
final String? notes;
|
||||
final String? phoneNumber;
|
||||
final String? address;
|
||||
|
||||
Order({
|
||||
required this.id,
|
||||
required this.customerName,
|
||||
required this.orderDate,
|
||||
required this.items,
|
||||
required this.totalAmount,
|
||||
required this.status,
|
||||
this.notes,
|
||||
this.phoneNumber,
|
||||
this.address,
|
||||
});
|
||||
}
|
||||
|
||||
class OrderItem {
|
||||
final String name;
|
||||
final int quantity;
|
||||
final double price;
|
||||
final String? imageUrl;
|
||||
final String? notes;
|
||||
|
||||
OrderItem({
|
||||
required this.name,
|
||||
required this.quantity,
|
||||
required this.price,
|
||||
this.imageUrl,
|
||||
this.notes,
|
||||
});
|
||||
}
|
||||
|
||||
enum OrderStatus { pending, processing, completed, cancelled }
|
||||
|
||||
/// Riwayat order customer (§4.5). Order hanya bisa dilihat; app tidak membuat
|
||||
/// atau membayar order.
|
||||
@RoutePage()
|
||||
class OrderPage extends StatefulWidget {
|
||||
class OrderPage extends StatefulWidget implements AutoRouteWrapper {
|
||||
const OrderPage({super.key});
|
||||
|
||||
@override
|
||||
State<OrderPage> createState() => _OrderPageState();
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) =>
|
||||
BlocProvider(create: (_) => getIt<OrderLoaderBloc>(), child: this);
|
||||
}
|
||||
|
||||
class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
bool _isLoading = true;
|
||||
List<Order> _orders = [];
|
||||
|
||||
// Filter states
|
||||
class _OrderPageState extends State<OrderPage> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 5, vsync: this);
|
||||
_loadOrders();
|
||||
if (context.read<AuthBloc>().state.isAuthenticated) _refresh();
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _loadOrders() {
|
||||
// Simulate loading
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
// Uncomment untuk testing dengan data
|
||||
_orders = _generateSampleOrders();
|
||||
});
|
||||
});
|
||||
void _refresh() {
|
||||
context.read<OrderLoaderBloc>().add(const OrderLoaderEvent.refreshed());
|
||||
}
|
||||
|
||||
List<Order> _generateSampleOrders() {
|
||||
return [
|
||||
Order(
|
||||
id: "ORD-001",
|
||||
customerName: "John Doe",
|
||||
orderDate: DateTime.now().subtract(const Duration(hours: 2)),
|
||||
address: "Jl. Malioboro No. 123, Yogyakarta",
|
||||
items: [
|
||||
OrderItem(
|
||||
name: "Nasi Gudeg",
|
||||
quantity: 2,
|
||||
price: 25000,
|
||||
notes: "Pedas sedang",
|
||||
),
|
||||
OrderItem(name: "Es Teh Manis", quantity: 2, price: 8000),
|
||||
OrderItem(name: "Kerupuk", quantity: 1, price: 5000),
|
||||
],
|
||||
totalAmount: 71000,
|
||||
status: OrderStatus.pending,
|
||||
notes: "Tolong diantar sebelum jam 2 siang",
|
||||
),
|
||||
Order(
|
||||
id: "ORD-002",
|
||||
customerName: "Jane Smith",
|
||||
orderDate: DateTime.now().subtract(const Duration(hours: 1)),
|
||||
address: "Jl. Sultan Agung No. 45, Yogyakarta",
|
||||
items: [
|
||||
OrderItem(
|
||||
name: "Ayam Bakar",
|
||||
quantity: 1,
|
||||
price: 35000,
|
||||
notes: "Tidak pedas",
|
||||
),
|
||||
OrderItem(name: "Nasi Putih", quantity: 1, price: 5000),
|
||||
OrderItem(name: "Lalapan", quantity: 1, price: 8000),
|
||||
],
|
||||
totalAmount: 48000,
|
||||
status: OrderStatus.processing,
|
||||
),
|
||||
Order(
|
||||
id: "ORD-003",
|
||||
customerName: "Bob Wilson",
|
||||
orderDate: DateTime.now().subtract(const Duration(minutes: 30)),
|
||||
phoneNumber: "+62 811-2345-6789",
|
||||
items: [
|
||||
OrderItem(name: "Gado-gado", quantity: 2, price: 20000),
|
||||
OrderItem(name: "Lontong", quantity: 2, price: 3000),
|
||||
OrderItem(name: "Es Jeruk", quantity: 2, price: 10000),
|
||||
],
|
||||
totalAmount: 66000,
|
||||
status: OrderStatus.completed,
|
||||
),
|
||||
];
|
||||
void _onScroll() {
|
||||
final position = _scrollController.position;
|
||||
if (position.pixels >= position.maxScrollExtent - 200) {
|
||||
context.read<OrderLoaderBloc>().add(const OrderLoaderEvent.loadedMore());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isAuthenticated = context.select<AuthBloc, bool>(
|
||||
(bloc) => bloc.state.isAuthenticated,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: _buildAppBar(),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildTabBar(),
|
||||
Expanded(
|
||||
child: _isLoading ? _buildLoadingState() : _buildOrderContent(),
|
||||
),
|
||||
],
|
||||
backgroundColor: AppColor.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('Pesanan'),
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: isAuthenticated ? _buildOrders() : _buildGuest(),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
return AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: AppColor.white,
|
||||
automaticallyImplyLeading: false,
|
||||
title: Text('Pesanan'),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _showFilterDialog,
|
||||
icon: const Icon(Icons.filter_list, color: AppColor.textSecondary),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _refreshOrders,
|
||||
icon: const Icon(Icons.refresh, color: AppColor.textSecondary),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
color: AppColor.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
labelColor: AppColor.primary,
|
||||
unselectedLabelColor: AppColor.textSecondary,
|
||||
indicatorColor: AppColor.primary,
|
||||
indicatorWeight: 3,
|
||||
labelStyle: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
|
||||
tabAlignment: TabAlignment.start,
|
||||
unselectedLabelStyle: AppStyle.md,
|
||||
tabs: const [
|
||||
Tab(text: 'Semua'),
|
||||
Tab(text: 'Menunggu'),
|
||||
Tab(text: 'Diproses'),
|
||||
Tab(text: 'Selesai'),
|
||||
Tab(text: 'Dibatalkan'),
|
||||
],
|
||||
Widget _buildOrders() {
|
||||
return BlocConsumer<OrderLoaderBloc, OrderLoaderState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.failureOption != c.failureOption && c.items.isNotEmpty,
|
||||
listener: (context, state) => state.failureOption.fold(
|
||||
() {},
|
||||
(f) => AppFlushbar.showError(context, walletFailureMessage(f)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOrderContent() {
|
||||
if (_orders.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildOrderList(_orders),
|
||||
_buildOrderList(
|
||||
_orders.where((o) => o.status == OrderStatus.pending).toList(),
|
||||
),
|
||||
_buildOrderList(
|
||||
_orders.where((o) => o.status == OrderStatus.processing).toList(),
|
||||
),
|
||||
_buildOrderList(
|
||||
_orders.where((o) => o.status == OrderStatus.completed).toList(),
|
||||
),
|
||||
_buildOrderList(
|
||||
_orders.where((o) => o.status == OrderStatus.cancelled).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOrderList(List<Order> orders) {
|
||||
if (orders.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshOrders,
|
||||
color: AppColor.primary,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: orders.length,
|
||||
itemBuilder: (context, index) {
|
||||
return OrderCard(order: orders[index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(60),
|
||||
builder: (context, state) {
|
||||
if (!state.hasLoaded) {
|
||||
return state.failureOption.fold(
|
||||
() => const Center(
|
||||
child: SpinKitFadingCircle(color: AppColor.primary, size: 36),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.receipt_long,
|
||||
size: 60,
|
||||
color: AppColor.primary.withOpacity(0.5),
|
||||
(f) => _Message(
|
||||
icon: Icons.wifi_off_rounded,
|
||||
title: 'Gagal memuat pesanan',
|
||||
message: walletFailureMessage(f),
|
||||
actionLabel: 'Coba lagi',
|
||||
onAction: _refresh,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Belum Ada Pesanan',
|
||||
style: AppStyle.h6.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Pesanan akan muncul di sini setelah\npelanggan mulai memesan.',
|
||||
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _refreshOrders,
|
||||
icon: const Icon(Icons.refresh, size: 20),
|
||||
label: const Text('Muat Ulang'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primary,
|
||||
foregroundColor: AppColor.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColor.primary),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Memuat pesanan...',
|
||||
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFilterDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Filter Pesanan',
|
||||
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Add filter options here
|
||||
Text('Opsi filter akan segera hadir...', style: AppStyle.md),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Tutup',
|
||||
style: AppStyle.md.copyWith(color: AppColor.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
return RefreshIndicator(
|
||||
color: AppColor.primary,
|
||||
onRefresh: () async {
|
||||
_refresh();
|
||||
await context.read<OrderLoaderBloc>().stream.firstWhere(
|
||||
(s) => !s.isFetching,
|
||||
);
|
||||
},
|
||||
child: state.items.isEmpty
|
||||
? ListView(
|
||||
children: const [
|
||||
SizedBox(height: 80),
|
||||
_Message(
|
||||
icon: Icons.receipt_long_rounded,
|
||||
title: 'Belum ada pesanan',
|
||||
message:
|
||||
'Pesanan akan muncul di sini setelah kasir '
|
||||
'mengaitkannya ke akunmu.',
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
||||
itemCount: state.items.length + (state.hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= state.items.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: SpinKitThreeBounce(
|
||||
color: AppColor.primary,
|
||||
size: 18,
|
||||
),
|
||||
);
|
||||
}
|
||||
return OrderCard(order: state.items[index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refreshOrders() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
// Uncomment untuk testing dengan data
|
||||
_orders = _generateSampleOrders();
|
||||
});
|
||||
Widget _buildGuest() {
|
||||
return _Message(
|
||||
icon: Icons.receipt_long_rounded,
|
||||
title: 'Lihat pesananmu',
|
||||
message:
|
||||
'Masuk untuk melihat riwayat pesanan dan EnakPoint yang kamu '
|
||||
'dapat dari setiap belanja.',
|
||||
actionLabel: 'Masuk',
|
||||
onAction: () => context.router.push(OnboardingRoute()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Message extends StatelessWidget {
|
||||
const _Message({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.message,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String message;
|
||||
final String? actionLabel;
|
||||
final VoidCallback? onAction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 96,
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryWithOpacity(0.08),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 44,
|
||||
color: AppColor.primaryWithOpacity(0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.xl.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
if (actionLabel != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
AppElevatedButton(
|
||||
title: actionLabel!,
|
||||
width: 180,
|
||||
onPressed: onAction,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +1,140 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../../../../common/extension/extension.dart';
|
||||
import '../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../domain/wallet/wallet.dart';
|
||||
import '../../../../../router/app_router.gr.dart';
|
||||
import '../order_page.dart';
|
||||
import '../../../../order/order_ui.dart';
|
||||
import '../../../../wallet/wallet_ui.dart';
|
||||
|
||||
class OrderCard extends StatelessWidget {
|
||||
final Order order;
|
||||
final CustomerOrder order;
|
||||
const OrderCard({super.key, required this.order});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => context.router.push(OrderDetailRoute(order: order)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: AppColor.border.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left side - Order info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Order ID
|
||||
Text(
|
||||
order.id,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Order Date
|
||||
Text(
|
||||
DateFormat('dd MMM yyyy • HH:mm').format(order.orderDate),
|
||||
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Right side - Status and Total
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white,
|
||||
borderRadius: BorderRadius.circular(AppValue.borderRadius),
|
||||
),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppValue.borderRadius),
|
||||
onTap: () => context.router.push(OrderDetailRoute(order: order)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
children: [
|
||||
// Status
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor().withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: _getStatusColor().withOpacity(0.2),
|
||||
width: 1,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryWithOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: orderTypeIcon(order).image(fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusText(),
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: _getStatusColor(),
|
||||
fontWeight: FontWeight.w600,
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
order.outletName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${order.orderNumber} · '
|
||||
'${formatOrderTime(order.createdAt)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${order.itemCount} item · '
|
||||
'${orderTypeLabel(order.orderType)}',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OrderStatusBadge(order: order),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// Total Amount
|
||||
Text(
|
||||
'Rp ${_formatCurrency(order.totalAmount)}',
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Divider(height: 1, color: AppColor.borderLight),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
order.totalAmount.currencyFormatRp,
|
||||
style: AppStyle.md.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const Spacer(),
|
||||
if (order.pointsEarned > 0)
|
||||
_Earned(
|
||||
currency: WalletCurrency.point,
|
||||
amount: order.pointsEarned,
|
||||
),
|
||||
if (order.pointsEarned > 0 && order.coinsEarned > 0)
|
||||
const SizedBox(width: 10),
|
||||
if (order.coinsEarned > 0)
|
||||
_Earned(
|
||||
currency: WalletCurrency.coin,
|
||||
amount: order.coinsEarned,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusColor() {
|
||||
switch (order.status) {
|
||||
case OrderStatus.pending:
|
||||
return AppColor.warning;
|
||||
case OrderStatus.processing:
|
||||
return AppColor.info;
|
||||
case OrderStatus.completed:
|
||||
return AppColor.success;
|
||||
case OrderStatus.cancelled:
|
||||
return AppColor.error;
|
||||
}
|
||||
}
|
||||
/// "+865" dengan ikon mata uang: saldo yang didapat dari order.
|
||||
class _Earned extends StatelessWidget {
|
||||
const _Earned({required this.currency, required this.amount});
|
||||
|
||||
String _getStatusText() {
|
||||
switch (order.status) {
|
||||
case OrderStatus.pending:
|
||||
return 'Menunggu';
|
||||
case OrderStatus.processing:
|
||||
return 'Diproses';
|
||||
case OrderStatus.completed:
|
||||
return 'Selesai';
|
||||
case OrderStatus.cancelled:
|
||||
return 'Dibatalkan';
|
||||
}
|
||||
}
|
||||
final WalletCurrency currency;
|
||||
final int amount;
|
||||
|
||||
String _formatCurrency(double amount) {
|
||||
final formatter = NumberFormat('#,###');
|
||||
return formatter.format(amount);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
WalletCurrencyIcon(currency: currency, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'+${amount.thousandFormat}',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.success,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/auth/auth_bloc.dart';
|
||||
import '../../../../../application/auth/logout_form/logout_form_bloc.dart';
|
||||
import '../../../../../application/outlet/outlet_loader/outlet_loader_bloc.dart';
|
||||
import '../../../../../application/wallet/wallet_loader/wallet_loader_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../../injection.dart';
|
||||
import '../../../../components/card/gradient_card.dart';
|
||||
@@ -22,7 +24,15 @@ class ProfilePage extends StatelessWidget implements AutoRouteWrapper {
|
||||
() => null,
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showAuthFailureToast(context, f),
|
||||
(_) => context.router.replaceAll([OnboardingRoute()]),
|
||||
(_) {
|
||||
context.read<WalletLoaderBloc>().add(
|
||||
const WalletLoaderEvent.cleared(),
|
||||
);
|
||||
context.read<OutletLoaderBloc>().add(
|
||||
const OutletLoaderEvent.cleared(),
|
||||
);
|
||||
context.router.replaceAll([OnboardingRoute()]);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -146,6 +156,12 @@ class ProfilePage extends StatelessWidget implements AutoRouteWrapper {
|
||||
title: 'Pembayaran',
|
||||
onTap: () => context.router.push(PaymentRoute()),
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.lock_outline,
|
||||
title: 'PIN',
|
||||
onTap: () =>
|
||||
context.router.push(const PinSettingsRoute()),
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.help_outline,
|
||||
title: 'Pusat Bantuan',
|
||||
|
||||
Reference in New Issue
Block a user