import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:line_icons/line_icons.dart'; import 'package:shimmer/shimmer.dart'; import '../../../application/analytic/exclusive_summary_loader/exclusive_summary_loader_bloc.dart'; import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; import '../../../domain/analytic/analytic.dart'; import '../../../injection.dart'; import '../../components/appbar/appbar.dart'; import '../../components/field/date_range_picker_field.dart'; import '../../components/spacer/spacer.dart'; @RoutePage() class ExclusiveSummaryPage extends StatefulWidget implements AutoRouteWrapper { const ExclusiveSummaryPage({super.key}); @override State createState() => _ExclusiveSummaryPageState(); @override Widget wrappedRoute(BuildContext context) => BlocProvider( create: (context) => getIt() ..add(ExclusiveSummaryLoaderEvent.fetched()), child: this, ); } class _ExclusiveSummaryPageState extends State with TickerProviderStateMixin { late AnimationController _fadeController; late AnimationController _slideController; late Animation _fadeAnimation; late Animation _slideAnimation; @override void initState() { super.initState(); _fadeController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _slideController = AnimationController( duration: const Duration(milliseconds: 900), vsync: this, ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _fadeController, curve: Curves.easeOut), ); _slideAnimation = Tween(begin: const Offset(0, 0.3), end: Offset.zero).animate( CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic), ); _fadeController.forward(); _slideController.forward(); } @override void dispose() { _fadeController.dispose(); _slideController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.background, body: BlocListener( listenWhen: (prev, curr) => prev.dateFrom != curr.dateFrom || prev.dateTo != curr.dateTo, listener: (context, state) { context .read() .add(ExclusiveSummaryLoaderEvent.fetched()); }, child: BlocBuilder( builder: (context, state) { return RefreshIndicator( color: AppColor.primary, onRefresh: () async { context .read() .add(ExclusiveSummaryLoaderEvent.fetched()); await context .read() .stream .firstWhere((s) => !s.isFetching); }, child: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ // App Bar SliverAppBar( expandedHeight: 120, floating: false, pinned: true, backgroundColor: AppColor.primary, flexibleSpace: CustomAppBar( title: context.lang.exclusive_summary, ), ), // Date Range Picker SliverToBoxAdapter( child: FadeTransition( opacity: _fadeAnimation, child: Padding( padding: const EdgeInsets.all(16), child: DateRangePickerField( maxDate: DateTime.now(), startDate: state.dateFrom, endDate: state.dateTo, onChanged: (startDate, endDate) { context.read().add( ExclusiveSummaryLoaderEvent.rangeDateChanged( startDate!, endDate!, ), ); }, ), ), ), ), // Content SliverToBoxAdapter( child: SlideTransition( position: _slideAnimation, child: FadeTransition( opacity: _fadeAnimation, child: state.isFetching ? _buildShimmer() : _buildContent(state.exclusiveSummary), ), ), ), const SliverToBoxAdapter(child: SpaceHeight(80)), ], ), ); }, ), ), ); } // ─── SHIMMER ──────────────────────────────────────────────────────────────── Widget _buildShimmer() { return Padding( padding: const EdgeInsets.all(16), child: Column( children: [ _shimmerBox(height: 160), const SpaceHeight(16), Row(children: [ Expanded(child: _shimmerBox(height: 100)), const SpaceWidth(12), Expanded(child: _shimmerBox(height: 100)), ]), const SpaceHeight(16), _shimmerBox(height: 200), const SpaceHeight(16), _shimmerBox(height: 150), ], ), ); } Widget _shimmerBox({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(16), ), ), ); } // ─── CONTENT ──────────────────────────────────────────────────────────────── Widget _buildContent(ExclusiveSummary data) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildNetProfitCard(data.summary), const SpaceHeight(16), _buildSummaryGrid(data.summary), const SpaceHeight(16), _buildReimburseCard(data.reimburse), const SpaceHeight(16), if (data.hppBreakdown.isNotEmpty) ...[ _buildBreakdownSection( title: context.lang.hpp_breakdown, icon: LineIcons.shoppingBag, color: AppColor.error, items: data.hppBreakdown, ), const SpaceHeight(16), ], if (data.operationalExpenseBreakdown.isNotEmpty) ...[ _buildBreakdownSection( title: context.lang.operational_expense_breakdown, icon: LineIcons.receipt, color: AppColor.warning, items: data.operationalExpenseBreakdown, ), const SpaceHeight(16), ], if (data.dailySummary.isNotEmpty) ...[ _buildDailySummarySection(data.dailySummary), const SpaceHeight(16), ], if (data.dailyTransactions.isNotEmpty) ...[ _buildTransactionsSection(data.dailyTransactions), const SpaceHeight(16), ], ], ), ); } // ─── NET PROFIT HERO CARD ─────────────────────────────────────────────────── Widget _buildNetProfitCard(ExclusiveSummarySummary summary) { final isPositive = summary.netProfit >= 0; final gradientColors = isPositive ? AppColor.successGradient : [AppColor.error, AppColor.error.withOpacity(0.7)]; return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 900), curve: Curves.elasticOut, builder: (context, value, _) => Transform.scale( scale: value.clamp(0.0, 1.0), child: Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( gradient: LinearGradient( colors: gradientColors, begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: (isPositive ? AppColor.success : AppColor.error) .withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: Icon( isPositive ? LineIcons.lineChart : LineIcons.arrowDown, color: Colors.white, size: 22, ), ), const SpaceWidth(12), Text( context.lang.net_profit, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600, ), ), ], ), const SpaceHeight(16), Text( summary.netProfit.currencyFormatRp, style: const TextStyle( color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold, letterSpacing: -0.5, ), ), const SpaceHeight(8), Row( children: [ _buildHeroStat( context.lang.total_sales, summary.sales.currencyFormatRp, ), const SpaceWidth(24), _buildHeroStat( context.lang.total_cost, summary.totalCost.currencyFormatRp, ), ], ), ], ), ), ), ); } Widget _buildHeroStat(String label, String value) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 12, ), ), Text( value, style: const TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold, ), ), ], ); } // ─── SUMMARY GRID ─────────────────────────────────────────────────────────── Widget _buildSummaryGrid(ExclusiveSummarySummary summary) { return Column( children: [ Row( children: [ Expanded( child: _buildStatCard( icon: LineIcons.arrowUp, label: context.lang.gross_profit, value: summary.grossProfit.currencyFormatRp, color: AppColor.success, ), ), const SpaceWidth(12), Expanded( child: _buildStatCard( icon: LineIcons.shoppingBag, label: context.lang.hpp, value: summary.hpp.currencyFormatRp, color: AppColor.error, ), ), ], ), const SpaceHeight(12), Row( children: [ Expanded( child: _buildStatCard( icon: LineIcons.users, label: context.lang.salary_total, value: summary.salaryTotal.currencyFormatRp, color: AppColor.info, ), ), const SpaceWidth(12), Expanded( child: _buildStatCard( icon: LineIcons.receipt, label: context.lang.operational_expenses, value: summary.operationalExpensesTotal.currencyFormatRp, color: AppColor.warning, ), ), ], ), ], ); } Widget _buildStatCard({ required IconData icon, required String label, required String value, required Color color, }) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: color.withOpacity(0.08), blurRadius: 12, offset: const Offset(0, 4), ), ], border: Border.all(color: color.withOpacity(0.12)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, color: color, size: 18), ), const SpaceHeight(10), Text( label, style: AppStyle.sm.copyWith(color: AppColor.textSecondary), ), const SpaceHeight(4), Text( value, style: AppStyle.md.copyWith( color: AppColor.textPrimary, fontWeight: FontWeight.bold, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ); } // ─── REIMBURSE CARD ───────────────────────────────────────────────────────── Widget _buildReimburseCard(ExclusiveSummaryReimburse reimburse) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: AppColor.primary.withOpacity(0.08), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColor.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: const Icon( LineIcons.moneyBill, color: AppColor.primary, size: 18, ), ), const SpaceWidth(10), Text( context.lang.reimburse_summary, style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold), ), ], ), const SpaceHeight(16), _buildReimburseRow( context.lang.total_cost, reimburse.totalCost.currencyFormatRp, ), const Divider(height: 20), _buildReimburseRow( context.lang.excluded_salary_staff, reimburse.excludedSalaryStaff.currencyFormatRp, ), const Divider(height: 20), _buildReimburseRow( context.lang.total_reimburse, reimburse.totalReimburse.currencyFormatRp, isHighlighted: true, ), ], ), ); } Widget _buildReimburseRow( String label, String value, { bool isHighlighted = false, }) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: AppStyle.sm.copyWith( color: isHighlighted ? AppColor.textPrimary : AppColor.textSecondary, fontWeight: isHighlighted ? FontWeight.bold : FontWeight.normal, ), ), Text( value, style: AppStyle.sm.copyWith( color: isHighlighted ? AppColor.primary : AppColor.textPrimary, fontWeight: isHighlighted ? FontWeight.bold : FontWeight.w600, ), ), ], ); } // ─── BREAKDOWN SECTION ────────────────────────────────────────────────────── Widget _buildBreakdownSection({ required String title, required IconData icon, required Color color, required List items, }) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, color: color, size: 18), ), const SpaceWidth(10), Expanded( child: Text( title, style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold), ), ), ], ), const SpaceHeight(16), ...items.map((item) => _buildBreakdownItem(item, color)), ], ), ); } Widget _buildBreakdownItem( ExclusiveSummaryBreakdown item, Color color, ) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( item.categoryName, style: AppStyle.sm.copyWith( fontWeight: FontWeight.w600, color: AppColor.textPrimary, ), ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( item.amount.currencyFormatRp, style: AppStyle.sm.copyWith( fontWeight: FontWeight.bold, color: color, ), ), Text( '${item.percentage.toStringAsFixed(1)}%', style: AppStyle.xs.copyWith( color: AppColor.textSecondary, ), ), ], ), ], ), const SpaceHeight(6), ClipRRect( borderRadius: BorderRadius.circular(4), child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: item.percentage / 100), duration: const Duration(milliseconds: 800), curve: Curves.easeOutCubic, builder: (context, value, _) => LinearProgressIndicator( value: value.clamp(0.0, 1.0), backgroundColor: color.withOpacity(0.1), valueColor: AlwaysStoppedAnimation(color), minHeight: 6, ), ), ), ], ), ); } // ─── DAILY SUMMARY ────────────────────────────────────────────────────────── Widget _buildDailySummarySection(List items) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColor.info.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: const Icon( LineIcons.calendar, color: AppColor.info, size: 18, ), ), const SpaceWidth(10), Text( context.lang.daily_summary, style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold), ), ], ), const SpaceHeight(16), ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: items.length, separatorBuilder: (_, __) => const Divider(height: 16), itemBuilder: (context, index) { final item = items[index]; return Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColor.primary.withOpacity(0.08), borderRadius: BorderRadius.circular(8), ), child: Text( '${item.date.day}', style: AppStyle.md.copyWith( color: AppColor.primary, fontWeight: FontWeight.bold, ), ), ), const SpaceWidth(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.date.toDate, style: AppStyle.sm.copyWith( fontWeight: FontWeight.w600, ), ), Text( '${item.transactionCount} ${context.lang.transactions}', style: AppStyle.xs.copyWith( color: AppColor.textSecondary, ), ), ], ), ), Text( item.totalCost.currencyFormatRp, style: AppStyle.sm.copyWith( fontWeight: FontWeight.bold, color: AppColor.error, ), ), ], ); }, ), ], ), ); } // ─── DAILY TRANSACTIONS ───────────────────────────────────────────────────── Widget _buildTransactionsSection( List items) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColor.secondary.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: const Icon( LineIcons.list, color: AppColor.secondary, size: 18, ), ), const SpaceWidth(10), Text( context.lang.daily_transactions, style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold), ), ], ), const SpaceHeight(16), ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: items.length, separatorBuilder: (_, __) => const Divider(height: 16), itemBuilder: (context, index) { final tx = items[index]; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: _sourceColor(tx.source).withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( _sourceLabel(tx.source), style: AppStyle.xs.copyWith( color: _sourceColor(tx.source), fontWeight: FontWeight.w600, ), ), ), const SpaceWidth(10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tx.description, style: AppStyle.sm.copyWith( fontWeight: FontWeight.w600, ), ), Text( '${tx.categoryName} · ${tx.date.toShortDate}', style: AppStyle.xs.copyWith( color: AppColor.textSecondary, ), ), ], ), ), Text( tx.amount.currencyFormatRp, style: AppStyle.sm.copyWith( fontWeight: FontWeight.bold, color: AppColor.error, ), ), ], ); }, ), ], ), ); } Color _sourceColor(String source) { switch (source) { case 'purchase_order': return AppColor.info; case 'salary': return AppColor.warning; case 'operational': return AppColor.secondary; default: return AppColor.primary; } } String _sourceLabel(String source) { switch (source) { case 'purchase_order': return 'PO'; case 'salary': return 'Gaji'; case 'operational': return 'Ops'; default: return source; } } }