2025-08-15 23:53:05 +07:00
import 'package:auto_route/auto_route.dart' ;
2025-08-15 23:06:47 +07:00
import 'package:flutter/material.dart' ;
2026-05-19 23:33:12 +07:00
import 'package:flutter_bloc/flutter_bloc.dart' ;
2025-08-15 23:06:47 +07:00
import 'package:line_icons/line_icons.dart' ;
2026-05-19 23:33:12 +07:00
import 'package:shimmer/shimmer.dart' ;
2025-08-15 23:06:47 +07:00
2026-05-19 23:33:12 +07:00
import '../../../application/analytic/purchasing_analytic_loader/purchasing_analytic_loader_bloc.dart' ;
import '../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart' ;
2025-08-20 13:52:49 +07:00
import '../../../common/extension/extension.dart' ;
2025-08-15 23:53:05 +07:00
import '../../../common/theme/theme.dart' ;
2026-05-19 23:33:12 +07:00
import '../../../injection.dart' ;
2025-08-16 00:39:09 +07:00
import '../../components/appbar/appbar.dart' ;
2026-05-19 23:33:12 +07:00
import '../../components/field/date_range_picker_field.dart' ;
import '../../components/spacer/spacer.dart' ;
import 'widgets/ingredient_card.dart' ;
import 'widgets/outlet_selector_field.dart' ;
import 'widgets/purchase_daily_tile.dart' ;
2025-08-15 23:06:47 +07:00
import 'widgets/stat_card.dart' ;
2026-05-19 23:33:12 +07:00
import 'widgets/vendor_card.dart' ;
2025-08-15 23:06:47 +07:00
2025-08-15 23:53:05 +07:00
@ RoutePage ()
2026-05-19 23:33:12 +07:00
class PurchasePage extends StatefulWidget implements AutoRouteWrapper {
2025-08-15 23:53:05 +07:00
const PurchasePage ({ super . key });
2025-08-15 23:06:47 +07:00
@ override
State < PurchasePage > createState () => _PurchasePageState ();
2026-05-19 23:33:12 +07:00
@ override
Widget wrappedRoute ( BuildContext context ) => MultiBlocProvider (
providers: [
BlocProvider (
create: ( context ) => getIt < PurchasingAnalyticLoaderBloc > ()
.. add ( const PurchasingAnalyticLoaderEvent . fetched ()),
),
BlocProvider (
create: ( context ) => getIt < OutletListLoaderBloc > ()
.. add ( const OutletListLoaderEvent . fetched ()),
),
],
child: this ,
);
2025-08-15 23:06:47 +07:00
}
class _PurchasePageState extends State < PurchasePage >
with TickerProviderStateMixin {
2026-05-19 23:33:12 +07:00
late AnimationController _fadeController ;
late Animation < double > _fadeAnimation ;
2025-08-15 23:06:47 +07:00
@ override
void initState () {
super . initState ();
2026-05-19 23:33:12 +07:00
_fadeController = AnimationController (
duration: const Duration ( milliseconds: 600 ),
2025-08-15 23:06:47 +07:00
vsync: this ,
);
2026-05-19 23:33:12 +07:00
_fadeAnimation = CurvedAnimation (
parent: _fadeController ,
curve: Curves . easeOut ,
);
Future . delayed ( const Duration ( milliseconds: 200 ), () {
if ( mounted ) _fadeController . forward ();
});
2025-08-15 23:06:47 +07:00
}
@ override
void dispose () {
2026-05-19 23:33:12 +07:00
_fadeController . dispose ();
2025-08-15 23:06:47 +07:00
super . dispose ();
}
@ override
Widget build ( BuildContext context ) {
return Scaffold (
backgroundColor: AppColor . background ,
2026-05-19 23:33:12 +07:00
body: MultiBlocListener (
listeners: [
// Re-fetch when date range changes
BlocListener < PurchasingAnalyticLoaderBloc ,
PurchasingAnalyticLoaderState > (
listenWhen: ( prev , curr ) =>
prev . dateFrom != curr . dateFrom ||
prev . dateTo != curr . dateTo ,
listener: ( context , _ ) => context
. read < PurchasingAnalyticLoaderBloc > ()
. add ( const PurchasingAnalyticLoaderEvent . fetched ()),
2025-08-15 23:06:47 +07:00
),
2026-05-19 23:33:12 +07:00
// Re-fetch when outlet changes
BlocListener < PurchasingAnalyticLoaderBloc ,
PurchasingAnalyticLoaderState > (
listenWhen: ( prev , curr ) => prev . outletId != curr . outletId ,
listener: ( context , _ ) => context
. read < PurchasingAnalyticLoaderBloc > ()
. add ( const PurchasingAnalyticLoaderEvent . fetched ()),
2025-08-15 23:06:47 +07:00
),
2026-05-19 23:33:12 +07:00
],
child: BlocBuilder < OutletListLoaderBloc , OutletListLoaderState > (
builder: ( context , outletListState ) {
return BlocBuilder < PurchasingAnalyticLoaderBloc ,
PurchasingAnalyticLoaderState > (
builder: ( context , state ) {
return CustomScrollView (
slivers: [
// App Bar
SliverAppBar (
expandedHeight: 120.0 ,
floating: false ,
pinned: true ,
elevation: 0 ,
backgroundColor: AppColor . primary ,
flexibleSpace:
CustomAppBar ( title: context . lang . purchase ),
),
2025-08-15 23:06:47 +07:00
2026-05-19 23:33:12 +07:00
// Date Range + Outlet Picker
SliverToBoxAdapter (
child: FadeTransition (
opacity: _fadeAnimation ,
child: Padding (
padding: const EdgeInsets . fromLTRB ( 16 , 16 , 16 , 0 ),
child: Column (
children: [
DateRangePickerField (
maxDate: DateTime . now (),
startDate: state . dateFrom ,
endDate: state . dateTo ,
onChanged: ( startDate , endDate ) {
context
. read < PurchasingAnalyticLoaderBloc > ()
. add (
PurchasingAnalyticLoaderEvent
. rangeDateChanged (
startDate ! ,
endDate ! ,
),
);
},
),
const SpaceHeight ( 8 ),
PurchaseOutletSelectorField (
selectedOutletId: state . outletId ,
outlets: outletListState . outlets ,
isLoading: outletListState . isFetching ,
onOutletChanged: ( outletId ) {
context
. read < PurchasingAnalyticLoaderBloc > ()
. add (
PurchasingAnalyticLoaderEvent
. outletChanged ( outletId ),
);
},
),
],
),
2025-08-15 23:06:47 +07:00
),
),
2026-05-19 23:33:12 +07:00
),
const SliverToBoxAdapter ( child: SpaceHeight ( 16 )),
// Summary Section
SliverToBoxAdapter (
child: FadeTransition (
opacity: _fadeAnimation ,
child: Padding (
padding: const EdgeInsets . symmetric ( horizontal: 16 ),
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
context . lang . summary ,
style: AppStyle . xxl . copyWith (
fontWeight: FontWeight . bold ,
color: AppColor . textPrimary ,
),
),
const SpaceHeight ( 16 ),
state . isFetching
? _buildSummaryShimmer ()
: _buildSummaryCards ( state ),
],
2025-08-15 23:06:47 +07:00
),
),
),
2026-05-19 23:33:12 +07:00
),
const SliverToBoxAdapter ( child: SpaceHeight ( 24 )),
// Total Purchases Highlight Card
SliverToBoxAdapter (
child: FadeTransition (
opacity: _fadeAnimation ,
child: state . isFetching
? _buildHighlightShimmer ()
: _buildTotalPurchasesCard ( state ),
),
),
const SliverToBoxAdapter ( child: SpaceHeight ( 24 )),
// Daily Breakdown Header
SliverToBoxAdapter (
child: FadeTransition (
opacity: _fadeAnimation ,
child: Padding (
padding: const EdgeInsets . symmetric ( horizontal: 16 ),
child: Text (
context . lang . daily_breakdown ,
style: AppStyle . xxl . copyWith (
fontWeight: FontWeight . bold ,
color: AppColor . textPrimary ,
),
),
),
),
),
const SliverToBoxAdapter ( child: SpaceHeight ( 12 )),
state . isFetching
? _buildListShimmer ()
: _buildDailyList ( state ),
const SliverToBoxAdapter ( child: SpaceHeight ( 24 )),
// Ingredient Header
SliverToBoxAdapter (
child: FadeTransition (
opacity: _fadeAnimation ,
child: Padding (
padding: const EdgeInsets . symmetric ( horizontal: 16 ),
child: Text (
'Bahan Baku' ,
style: AppStyle . xxl . copyWith (
fontWeight: FontWeight . bold ,
color: AppColor . textPrimary ,
),
),
),
),
),
const SliverToBoxAdapter ( child: SpaceHeight ( 12 )),
state . isFetching
? _buildListShimmer ()
: _buildIngredientList ( state ),
const SliverToBoxAdapter ( child: SpaceHeight ( 24 )),
// Vendor Header
SliverToBoxAdapter (
child: FadeTransition (
opacity: _fadeAnimation ,
child: Padding (
padding: const EdgeInsets . symmetric ( horizontal: 16 ),
child: Text (
'Vendor' ,
style: AppStyle . xxl . copyWith (
fontWeight: FontWeight . bold ,
color: AppColor . textPrimary ,
),
),
),
),
),
const SliverToBoxAdapter ( child: SpaceHeight ( 12 )),
state . isFetching
? _buildListShimmer ()
: _buildVendorList ( state ),
const SliverToBoxAdapter ( child: SpaceHeight ( 80 )),
],
);
},
);
},
),
),
);
}
// ─── Summary Cards ────────────────────────────────────────────────────────
Widget _buildSummaryCards ( PurchasingAnalyticLoaderState state ) {
final s = state . purchasing . summary ;
return Column (
children: [
Row (
children: [
Expanded (
child: PurchaseStatCard (
title: 'Total Pembelian' ,
value: s . totalPurchases . currencyFormatRp ,
icon: LineIcons . shoppingCart ,
iconColor: AppColor . primary ,
cardAnimation: _fadeAnimation ,
),
),
const SpaceWidth ( 12 ),
Expanded (
child: PurchaseStatCard (
title: 'Total PO' ,
value: ' ${ s . totalPurchaseOrders } PO' ,
icon: LineIcons . fileAlt ,
iconColor: AppColor . info ,
cardAnimation: _fadeAnimation ,
),
),
],
),
const SpaceHeight ( 12 ),
2026-06-22 14:30:38 +07:00
Row (
children: [
Expanded (
child: PurchaseStatCard (
title: 'Bahan Baku' ,
value: s . rawMaterialPurchases . currencyFormatRp ,
icon: LineIcons . leaf ,
iconColor: AppColor . secondary ,
cardAnimation: _fadeAnimation ,
),
),
const SpaceWidth ( 12 ),
Expanded (
child: PurchaseStatCard (
title: 'Pengeluaran' ,
value: s . expensePurchases . currencyFormatRp ,
icon: LineIcons . receipt ,
iconColor: AppColor . warning ,
cardAnimation: _fadeAnimation ,
),
),
],
),
const SpaceHeight ( 12 ),
2026-05-19 23:33:12 +07:00
Row (
children: [
Expanded (
child: PurchaseStatCard (
title: 'Total Qty' ,
value: ' ${ s . totalQuantity } pcs' ,
icon: LineIcons . boxes ,
iconColor: AppColor . warning ,
cardAnimation: _fadeAnimation ,
),
),
const SpaceWidth ( 12 ),
Expanded (
child: PurchaseStatCard (
title: 'Rata-rata PO' ,
value: s . averagePurchaseOrderValue . round (). currencyFormatRp ,
icon: LineIcons . dollarSign ,
2026-06-22 14:30:38 +07:00
iconColor: AppColor . secondaryDark ,
2026-05-19 23:33:12 +07:00
cardAnimation: _fadeAnimation ,
),
),
],
),
const SpaceHeight ( 12 ),
Row (
children: [
Expanded (
child: PurchaseStatCard (
2026-06-22 14:30:38 +07:00
title: 'Item Bahan Baku' ,
2026-05-19 23:33:12 +07:00
value: ' ${ s . totalIngredients } item' ,
icon: LineIcons . leaf ,
iconColor: AppColor . secondaryDark ,
cardAnimation: _fadeAnimation ,
),
),
const SpaceWidth ( 12 ),
Expanded (
child: PurchaseStatCard (
title: 'Vendor' ,
value: ' ${ s . totalVendors } vendor' ,
icon: LineIcons . truck ,
iconColor: AppColor . primaryDark ,
cardAnimation: _fadeAnimation ,
),
),
],
),
],
);
}
Widget _buildTotalPurchasesCard ( PurchasingAnalyticLoaderState state ) {
return TweenAnimationBuilder < double > (
tween: Tween ( begin: 0.0 , end: 1.0 ),
duration: const Duration ( milliseconds: 900 ),
curve: Curves . bounceOut ,
builder: ( context , value , _ ) {
return Transform . scale (
scale: value ,
child: Container (
margin: const EdgeInsets . symmetric ( horizontal: 16 ),
padding: const EdgeInsets . all ( 20 ),
decoration: BoxDecoration (
gradient: const LinearGradient (
colors: AppColor . primaryGradient ,
begin: Alignment . topLeft ,
end: Alignment . bottomRight ,
),
borderRadius: BorderRadius . circular ( 16 ),
boxShadow: [
BoxShadow (
color: AppColor . primary . withOpacity ( 0.35 ),
blurRadius: 16 ,
offset: const Offset ( 0 , 6 ),
),
],
),
child: Row (
children: [
Container (
padding: const EdgeInsets . all ( 12 ),
decoration: BoxDecoration (
color: Colors . white . withOpacity ( 0.2 ),
borderRadius: BorderRadius . circular ( 12 ),
),
child: const Icon (
LineIcons . shoppingBag ,
color: AppColor . textWhite ,
size: 28 ,
),
),
const SpaceWidth ( 16 ),
Expanded (
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
context . lang . total_purchase ,
style: TextStyle (
color: AppColor . textWhite . withOpacity ( 0.9 ),
fontSize: 14 ,
fontWeight: FontWeight . w500 ,
),
),
const SpaceHeight ( 4 ),
Text (
state . purchasing . summary . totalPurchases
. currencyFormatRp ,
style: const TextStyle (
color: AppColor . textWhite ,
fontSize: 24 ,
fontWeight: FontWeight . bold ,
),
),
2025-08-15 23:06:47 +07:00
],
),
2026-05-19 23:33:12 +07:00
),
Column (
crossAxisAlignment: CrossAxisAlignment . end ,
children: [
Text (
' ${ state . purchasing . summary . totalPurchaseOrders } PO' ,
style: const TextStyle (
color: AppColor . textWhite ,
fontSize: 16 ,
fontWeight: FontWeight . bold ,
2025-08-15 23:06:47 +07:00
),
2026-05-19 23:33:12 +07:00
),
Text (
'purchase order' ,
style: TextStyle (
color: AppColor . textWhite . withOpacity ( 0.8 ),
fontSize: 12 ,
),
),
],
),
],
2025-08-15 23:06:47 +07:00
),
),
2026-05-19 23:33:12 +07:00
);
},
);
}
2025-08-15 23:06:47 +07:00
2026-05-19 23:33:12 +07:00
// ─── Lists ────────────────────────────────────────────────────────────────
Widget _buildDailyList ( PurchasingAnalyticLoaderState state ) {
if ( state . purchasing . data . isEmpty ) {
return SliverToBoxAdapter (
child: _buildEmptyState ( 'Tidak ada data harian' ));
}
return SliverList (
delegate: SliverChildBuilderDelegate (
( context , index ) => PurchaseDailyTile (
data: state . purchasing . data [ index ],
index: index ,
animation: _fadeAnimation ,
),
childCount: state . purchasing . data . length ,
),
);
}
Widget _buildIngredientList ( PurchasingAnalyticLoaderState state ) {
if ( state . purchasing . ingredientData . isEmpty ) {
return SliverToBoxAdapter (
child: _buildEmptyState ( 'Tidak ada data bahan baku' ));
}
return SliverList (
delegate: SliverChildBuilderDelegate (
( context , index ) => PurchaseIngredientCard (
data: state . purchasing . ingredientData [ index ],
index: index ,
animation: _fadeAnimation ,
),
childCount: state . purchasing . ingredientData . length ,
),
);
}
Widget _buildVendorList ( PurchasingAnalyticLoaderState state ) {
if ( state . purchasing . vendorData . isEmpty ) {
return SliverToBoxAdapter (
child: _buildEmptyState ( 'Tidak ada data vendor' ));
}
return SliverList (
delegate: SliverChildBuilderDelegate (
( context , index ) => PurchaseVendorCard (
data: state . purchasing . vendorData [ index ],
index: index ,
animation: _fadeAnimation ,
),
childCount: state . purchasing . vendorData . length ,
),
);
}
// ─── Empty State ──────────────────────────────────────────────────────────
Widget _buildEmptyState ( String message ) {
return Container (
margin: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ),
padding: const EdgeInsets . all ( 24 ),
decoration: BoxDecoration (
color: AppColor . surface ,
borderRadius: BorderRadius . circular ( 12 ),
border: Border . all ( color: AppColor . border . withOpacity ( 0.3 )),
),
child: Center (
child: Text (
message ,
style: AppStyle . md . copyWith ( color: AppColor . textSecondary ),
),
),
);
}
// ─── Shimmer Loaders ──────────────────────────────────────────────────────
Widget _buildSummaryShimmer () {
return Column (
children: [
Row (
children: [
Expanded ( child: _shimmerCard ( height: 100 )),
const SpaceWidth ( 12 ),
Expanded ( child: _shimmerCard ( height: 100 )),
],
),
const SpaceHeight ( 12 ),
Row (
children: [
Expanded ( child: _shimmerCard ( height: 100 )),
const SpaceWidth ( 12 ),
Expanded ( child: _shimmerCard ( height: 100 )),
],
),
const SpaceHeight ( 12 ),
Row (
children: [
2026-06-22 14:30:38 +07:00
Expanded ( child: _shimmerCard ( height: 100 )),
const SpaceWidth ( 12 ),
Expanded ( child: _shimmerCard ( height: 100 )),
],
),
const SpaceHeight ( 12 ),
Row (
children: [
2026-05-19 23:33:12 +07:00
Expanded ( child: _shimmerCard ( height: 100 )),
const SpaceWidth ( 12 ),
Expanded ( child: _shimmerCard ( height: 100 )),
],
),
],
);
}
Widget _buildHighlightShimmer () {
return Container (
margin: const EdgeInsets . symmetric ( horizontal: 16 ),
child: _shimmerCard ( height: 88 ),
);
}
Widget _buildListShimmer () {
return SliverList (
delegate: SliverChildBuilderDelegate (
( _ , __ ) => Container (
margin: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 6 ),
child: _shimmerCard ( height: 72 ),
),
childCount: 4 ,
),
);
}
Widget _shimmerCard ({ required double height }) {
return Shimmer . fromColors (
baseColor: Colors . grey [ 300 ] ! ,
highlightColor: Colors . grey [ 100 ] ! ,
child: Container (
height: height ,
decoration: BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . circular ( 12 ),
),
2025-08-15 23:06:47 +07:00
),
);
}
}