feat: add total count at order page
Build Android APK (split per ABI) / build-apk (push) Canceled after 0s
Build & Deploy iOS to TestFlight / build-and-deploy (push) Canceled after 0s

This commit is contained in:
efrilm
2026-10-19 23:04:03 +07:00
parent 243b7e02aa
commit 7c8b36b65d
17 changed files with 652 additions and 27 deletions
@@ -14,6 +14,7 @@ import '../../../components/appbar/appbar.dart';
import '../../../components/widgets/empty_widget.dart';
import '../../../router/app_router.gr.dart';
import 'widgets/filter_header_delegate.dart';
import 'widgets/order_summary.dart';
import 'widgets/order_tile.dart';
@RoutePage()
@@ -29,7 +30,8 @@ class OrderPage extends StatefulWidget implements AutoRouteWrapper {
BlocProvider(
create: (_) =>
getIt<OrderLoaderBloc>()
..add(OrderLoaderEvent.fetched(isRefresh: true)),
..add(OrderLoaderEvent.fetched(isRefresh: true))
..add(const OrderLoaderEvent.summaryFetched()),
),
BlocProvider(
create: (_) =>
@@ -192,9 +194,9 @@ class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
BlocListener<OrderLoaderBloc, OrderLoaderState>(
listenWhen: (p, c) => p.outletId != c.outletId,
listener: (context, state) {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(isRefresh: true),
);
context.read<OrderLoaderBloc>()
..add(OrderLoaderEvent.fetched(isRefresh: true))
..add(const OrderLoaderEvent.summaryFetched());
},
),
BlocListener<OrderLoaderBloc, OrderLoaderState>(
@@ -210,9 +212,9 @@ class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
previous.dateFrom != current.dateFrom ||
previous.dateTo != current.dateTo,
listener: (context, state) {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(isRefresh: true),
);
context.read<OrderLoaderBloc>()
..add(OrderLoaderEvent.fetched(isRefresh: true))
..add(const OrderLoaderEvent.summaryFetched());
},
),
],
@@ -301,6 +303,14 @@ class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
position: _slideAnimation,
child: Column(
children: [
OrderSummary(
totalOrder: state.totalOrder,
totalPendingOrder:
state.totalPendingOrder,
isLoading: state.isFetchingSummary,
),
const SizedBox(height: 16),
// Show filtered transaction count
if (state.status != 'all' &&
!state.isFetching)
@@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../../../../common/extension/extension.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../components/spacer/spacer.dart';
import '../../../../components/widgets/particle_card.dart';
/// Aksen pesanan pending — amber supaya kontras di atas gradient merah.
const Color _pendingColor = Color(0xFFFFB300);
class OrderSummary extends StatelessWidget {
final int totalOrder;
final int totalPendingOrder;
final bool isLoading;
const OrderSummary({
super.key,
required this.totalOrder,
required this.totalPendingOrder,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
final pendingRatio = totalOrder > 0
? (totalPendingOrder / totalOrder).clamp(0.0, 1.0)
: 0.0;
return ParticleCard(
borderRadius: 18,
decorationOpacity: 0.8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IntrinsicHeight(
child: Row(
children: [
Expanded(
child: _SummaryStat(
title: context.lang.total_orders,
value: totalOrder,
icon: Icons.receipt_long_rounded,
iconBackground: AppColor.white.withOpacity(0.2),
isLoading: isLoading,
),
),
VerticalDivider(
width: 28,
thickness: 1,
color: AppColor.white.withOpacity(0.25),
),
Expanded(
child: _SummaryStat(
title: context.lang.pending_order,
value: totalPendingOrder,
icon: Icons.hourglass_top_rounded,
iconBackground: _pendingColor,
isLoading: isLoading,
),
),
],
),
),
const SpaceHeight(16),
_PendingRatioBar(ratio: pendingRatio),
const SpaceHeight(8),
Text(
context.lang.order_unpaid_ratio(
(pendingRatio * 100).round().toString(),
),
style: AppStyle.xs.copyWith(
color: AppColor.white.withOpacity(0.8),
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}
class _SummaryStat extends StatelessWidget {
final String title;
final int value;
final IconData icon;
final Color iconBackground;
final bool isLoading;
const _SummaryStat({
required this.title,
required this.value,
required this.icon,
required this.iconBackground,
required this.isLoading,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: iconBackground,
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: AppColor.white, size: 16),
),
const SpaceWidth(8),
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppStyle.sm.copyWith(
color: AppColor.white.withOpacity(0.9),
fontWeight: FontWeight.w600,
),
),
),
],
),
const SpaceHeight(12),
if (isLoading)
Shimmer.fromColors(
baseColor: AppColor.white.withOpacity(0.2),
highlightColor: AppColor.white.withOpacity(0.45),
child: Container(
width: 56,
height: 30,
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(6),
),
),
)
else
TweenAnimationBuilder<int>(
tween: IntTween(begin: 0, end: value),
duration: const Duration(milliseconds: 900),
curve: Curves.easeOutCubic,
builder: (context, animatedValue, _) => Text(
animatedValue.thousandFormat,
style: AppStyle.h1.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w900,
fontSize: 28,
height: 1.1,
shadows: [
Shadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
),
),
],
);
}
}
class _PendingRatioBar extends StatelessWidget {
final double ratio;
const _PendingRatioBar({required this.ratio});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Container(
height: 6,
color: AppColor.white.withOpacity(0.2),
alignment: Alignment.centerLeft,
child: TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: ratio),
duration: const Duration(milliseconds: 900),
curve: Curves.easeOutCubic,
builder: (context, value, _) => FractionallySizedBox(
widthFactor: value,
heightFactor: 1,
child: Container(
decoration: BoxDecoration(
color: _pendingColor,
borderRadius: BorderRadius.circular(4),
),
),
),
),
),
);
}
}