From 75415cc6ff9f7e36b29df34d5a1a3d7eff3446ce Mon Sep 17 00:00:00 2001 From: Efril Date: Wed, 24 Jun 2026 11:03:28 +0700 Subject: [PATCH] feat: update main, exclusive summary and stock --- lib/l10n/app_en.arb | 20 +- lib/l10n/app_id.arb | 20 +- lib/l10n/app_localizations.dart | 54 + lib/l10n/app_localizations_en.dart | 27 + lib/l10n/app_localizations_id.dart | 27 + .../exclusive_summary_page.dart | 1344 ++++++++--------- .../widgets/daily_revenue_chart.dart | 186 +++ .../inventory/widgets/inventory_header.dart | 36 +- lib/presentation/pages/main/main_page.dart | 7 +- .../pages/main/widgets/bottom_navbar.dart | 10 +- lib/presentation/router/app_router.dart | 4 +- 11 files changed, 993 insertions(+), 742 deletions(-) create mode 100644 lib/presentation/pages/exclusive_summary/widgets/daily_revenue_chart.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ea96978..07f995e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -520,5 +520,23 @@ "net_profit_loss": "Net Profit/Loss", "@net_profit_loss": {}, "cost_breakdown": "Cost Breakdown", - "@cost_breakdown": {} + "@cost_breakdown": {}, + "all_outlets": "All Outlets", + "@all_outlets": {}, + "salary_dw": "DW Salary", + "@salary_dw": {}, + "salary_staff": "Staff Salary", + "@salary_staff": {}, + "salary_other": "Other Salary", + "@salary_other": {}, + "other_operational_expenses": "Other Operational Expenses", + "@other_operational_expenses": {}, + "daily_revenue": "Daily Revenue", + "@daily_revenue": {}, + "daily_revenue_desc": "Monday – Sunday · in thousands of Rupiah", + "@daily_revenue_desc": {}, + "no_data_yet": "No data yet", + "@no_data_yet": {}, + "total_inventory_value": "Total Inventory Value", + "@total_inventory_value": {} } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index a5febf2..564b972 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -520,5 +520,23 @@ "net_profit_loss": "Laba/Rugi Bersih", "@net_profit_loss": {}, "cost_breakdown": "Rincian Biaya", - "@cost_breakdown": {} + "@cost_breakdown": {}, + "all_outlets": "Semua Outlet", + "@all_outlets": {}, + "salary_dw": "Gaji DW", + "@salary_dw": {}, + "salary_staff": "Gaji Staff", + "@salary_staff": {}, + "salary_other": "Gaji Lainnya", + "@salary_other": {}, + "other_operational_expenses": "Biaya Ops Lainnya", + "@other_operational_expenses": {}, + "daily_revenue": "Omzet Harian", + "@daily_revenue": {}, + "daily_revenue_desc": "Senin – Minggu · dalam ribuan Rupiah", + "@daily_revenue_desc": {}, + "no_data_yet": "Belum ada data", + "@no_data_yet": {}, + "total_inventory_value": "Total Nilai Inventori", + "@total_inventory_value": {} } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2ec256f..3b333c2 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1510,6 +1510,60 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Cost Breakdown'** String get cost_breakdown; + + /// No description provided for @all_outlets. + /// + /// In en, this message translates to: + /// **'All Outlets'** + String get all_outlets; + + /// No description provided for @salary_dw. + /// + /// In en, this message translates to: + /// **'DW Salary'** + String get salary_dw; + + /// No description provided for @salary_staff. + /// + /// In en, this message translates to: + /// **'Staff Salary'** + String get salary_staff; + + /// No description provided for @salary_other. + /// + /// In en, this message translates to: + /// **'Other Salary'** + String get salary_other; + + /// No description provided for @other_operational_expenses. + /// + /// In en, this message translates to: + /// **'Other Operational Expenses'** + String get other_operational_expenses; + + /// No description provided for @daily_revenue. + /// + /// In en, this message translates to: + /// **'Daily Revenue'** + String get daily_revenue; + + /// No description provided for @daily_revenue_desc. + /// + /// In en, this message translates to: + /// **'Monday – Sunday · in thousands of Rupiah'** + String get daily_revenue_desc; + + /// No description provided for @no_data_yet. + /// + /// In en, this message translates to: + /// **'No data yet'** + String get no_data_yet; + + /// No description provided for @total_inventory_value. + /// + /// In en, this message translates to: + /// **'Total Inventory Value'** + String get total_inventory_value; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 60d1516..5aed076 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -729,4 +729,31 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cost_breakdown => 'Cost Breakdown'; + + @override + String get all_outlets => 'All Outlets'; + + @override + String get salary_dw => 'DW Salary'; + + @override + String get salary_staff => 'Staff Salary'; + + @override + String get salary_other => 'Other Salary'; + + @override + String get other_operational_expenses => 'Other Operational Expenses'; + + @override + String get daily_revenue => 'Daily Revenue'; + + @override + String get daily_revenue_desc => 'Monday – Sunday · in thousands of Rupiah'; + + @override + String get no_data_yet => 'No data yet'; + + @override + String get total_inventory_value => 'Total Inventory Value'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index f573dd0..89f8829 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -729,4 +729,31 @@ class AppLocalizationsId extends AppLocalizations { @override String get cost_breakdown => 'Rincian Biaya'; + + @override + String get all_outlets => 'Semua Outlet'; + + @override + String get salary_dw => 'Gaji DW'; + + @override + String get salary_staff => 'Gaji Staff'; + + @override + String get salary_other => 'Gaji Lainnya'; + + @override + String get other_operational_expenses => 'Biaya Ops Lainnya'; + + @override + String get daily_revenue => 'Omzet Harian'; + + @override + String get daily_revenue_desc => 'Senin – Minggu · dalam ribuan Rupiah'; + + @override + String get no_data_yet => 'Belum ada data'; + + @override + String get total_inventory_value => 'Total Nilai Inventori'; } diff --git a/lib/presentation/pages/exclusive_summary/exclusive_summary_page.dart b/lib/presentation/pages/exclusive_summary/exclusive_summary_page.dart index dfbc8b0..a023e40 100644 --- a/lib/presentation/pages/exclusive_summary/exclusive_summary_page.dart +++ b/lib/presentation/pages/exclusive_summary/exclusive_summary_page.dart @@ -1,70 +1,68 @@ 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/dashboard_analytic_loader/dashboard_analytic_loader_bloc.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'; +import 'widgets/daily_revenue_chart.dart'; @RoutePage() -class ExclusiveSummaryPage extends StatefulWidget - implements AutoRouteWrapper { +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, - ); + Widget wrappedRoute(BuildContext context) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + getIt() + ..add(ExclusiveSummaryLoaderEvent.fetched()), + ), + BlocProvider( + create: (context) => + getIt() + ..add(DashboardAnalyticLoaderEvent.fetched()), + ), + ], + child: this, + ); } class _ExclusiveSummaryPageState extends State - with TickerProviderStateMixin { + with SingleTickerProviderStateMixin { 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), + duration: const Duration(milliseconds: 1000), 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), - ); + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeIn)); _fadeController.forward(); - _slideController.forward(); } @override void dispose() { _fadeController.dispose(); - _slideController.dispose(); super.dispose(); } @@ -72,256 +70,253 @@ class _ExclusiveSummaryPageState extends State Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.background, - body: BlocListener( + body: BlocListener( listenWhen: (prev, curr) => prev.dateFrom != curr.dateFrom || prev.dateTo != curr.dateTo, listener: (context, state) { - context - .read() - .add(ExclusiveSummaryLoaderEvent.fetched()); + 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, - ), - ), + child: + BlocBuilder< + ExclusiveSummaryLoaderBloc, + ExclusiveSummaryLoaderState + >( + 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: [ + // Header with gradient background and summary + SliverToBoxAdapter( + child: FadeTransition( + opacity: _fadeAnimation, + child: _buildHeader(context, state), + ), + ), - // 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( + // 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), + // Report Table (like ProfitLossReport) + SliverToBoxAdapter( + child: FadeTransition( + opacity: _fadeAnimation, + child: state.isFetching + ? _buildShimmer() + : _buildReportTable( + context, + state.exclusiveSummary, + ), + ), ), - ), + + // Omzet Harian Chart (from Dashboard Recent Sales API) + SliverToBoxAdapter( + child: FadeTransition( + opacity: _fadeAnimation, + child: + BlocBuilder< + DashboardAnalyticLoaderBloc, + DashboardAnalyticLoaderState + >( + builder: (context, dashState) { + if (dashState.isFetching) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: _shimmerBox(height: 260), + ); + } + return Padding( + padding: const EdgeInsets.only(top: 16), + child: DailyRevenueChart( + salesData: dashState + .dashboardAnalytic + .recentSales, + ), + ); + }, + ), + ), + ), + + // Reimburse Section + if (!state.isFetching) + SliverToBoxAdapter( + child: FadeTransition( + opacity: _fadeAnimation, + child: _buildReimburseSection( + context, + state.exclusiveSummary.reimburse, + ), + ), + ), + + // Bottom spacing + const SliverToBoxAdapter(child: SizedBox(height: 100)), + ], ), + ); + }, + ), + ), + ); + } - const SliverToBoxAdapter(child: SpaceHeight(80)), - ], - ), - ); - }, + // ─── HEADER ───────────────────────────────────────────────────────────────── + + Widget _buildHeader(BuildContext context, ExclusiveSummaryLoaderState state) { + final summary = state.exclusiveSummary.summary; + final outletLabel = state.exclusiveSummary.outletName.isNotEmpty + ? state.exclusiveSummary.outletName + : context.lang.all_outlets; + + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: AppColor.primaryGradient, + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), ), ), - ); - } - - // ─── 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: SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Back button + Title row Row( children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), + if (context.router.canPop()) ...[ + GestureDetector( + onTap: () => context.router.maybePop(), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColor.textWhite.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.chevron_left_rounded, + color: AppColor.textWhite, + size: 24, + ), + ), ), - 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 SpaceWidth(12), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.lang.report, + style: AppStyle.xl.copyWith( + color: AppColor.textWhite, + fontWeight: FontWeight.w700, + fontSize: 20, + ), + ), + const SizedBox(height: 2), + Text( + outletLabel, + style: AppStyle.sm.copyWith( + color: AppColor.textWhite.withOpacity(0.75), + fontWeight: FontWeight.w400, + fontSize: 12, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), ), ], ), - const SpaceHeight(16), + + const SpaceHeight(24), + + // Big net profit value + state.isFetching + ? _buildHeaderValueShimmer() + : Text( + summary.netProfit.currencyFormatRp, + style: AppStyle.h1.copyWith( + color: summary.netProfit >= 0 + ? AppColor.textWhite + : AppColor.textWhite.withOpacity(0.7), + fontWeight: FontWeight.w900, + fontSize: 32, + ), + ), + + const SpaceHeight(4), Text( - summary.netProfit.currencyFormatRp, - style: const TextStyle( - color: Colors.white, - fontSize: 32, - fontWeight: FontWeight.bold, - letterSpacing: -0.5, + context.lang.net_profit_loss, + style: AppStyle.sm.copyWith( + color: AppColor.textWhite.withOpacity(0.75), + fontWeight: FontWeight.w400, + fontSize: 13, ), ), - const SpaceHeight(8), - Row( - children: [ - _buildHeroStat( - context.lang.total_sales, - summary.sales.currencyFormatRp, - ), - const SpaceWidth(24), - _buildHeroStat( - context.lang.total_cost, - summary.totalCost.currencyFormatRp, - ), - ], - ), + + const SpaceHeight(16), + + // Chips row (Sales + Total Biaya) + state.isFetching + ? _buildHeaderChipsShimmer() + : Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _buildChip( + '${context.lang.total_sales} ${summary.sales.currencyFormatRp}', + ), + _buildChip( + '${context.lang.total_cost} ${summary.totalCost.currencyFormatRp}', + ), + ], + ), ], ), ), @@ -329,183 +324,393 @@ class _ExclusiveSummaryPageState extends State ); } - 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, - }) { + Widget _buildChip(String label) { return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), 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)), + color: AppColor.textWhite.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColor.textWhite.withOpacity(0.25)), ), - 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, - ), - ], + child: Text( + label, + style: AppStyle.sm.copyWith( + color: AppColor.textWhite, + fontWeight: FontWeight.w600, + fontSize: 12, + ), ), ); } - // ─── REIMBURSE CARD ───────────────────────────────────────────────────────── + Widget _buildHeaderValueShimmer() { + return Shimmer.fromColors( + baseColor: AppColor.textWhite.withOpacity(0.3), + highlightColor: AppColor.textWhite.withOpacity(0.6), + child: Container( + width: 200, + height: 36, + decoration: BoxDecoration( + color: AppColor.textWhite.withOpacity(0.3), + borderRadius: BorderRadius.circular(8), + ), + ), + ); + } + + Widget _buildHeaderChipsShimmer() { + return Row( + children: List.generate( + 2, + (index) => Padding( + padding: const EdgeInsets.only(right: 8), + child: Shimmer.fromColors( + baseColor: AppColor.textWhite.withOpacity(0.15), + highlightColor: AppColor.textWhite.withOpacity(0.3), + child: Container( + width: 130, + height: 32, + decoration: BoxDecoration( + color: AppColor.textWhite.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ), + ), + ); + } + + // ─── REPORT TABLE (like ProfitLossReport) ─────────────────────────────────── + + Widget _buildReportTable(BuildContext context, ExclusiveSummary data) { + final summary = data.summary; + final sales = summary.sales; + final totalCost = summary.totalCost; + + // Calculate percentages relative to sales + double pct(int value) { + if (sales == 0) return 0; + return (value / sales) * 100; + } - Widget _buildReimburseCard(ExclusiveSummaryReimburse reimburse) { return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), 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), + color: AppColor.textLight.withOpacity(0.08), + spreadRadius: 1, + blurRadius: 10, + offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Title row Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, 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, + Text( + context.lang.report, + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, ), ), - const SpaceWidth(10), Text( - context.lang.reimburse_summary, - style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold), + _formatDateLabel(data.period.dateFrom, data.period.dateTo), + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w400, + ), ), ], ), - const SpaceHeight(16), + + const SizedBox(height: 20), + + // Sales (bold header) + _buildItemRow( + label: context.lang.total_sales, + nominal: sales, + percentage: 100, + isBold: true, + isSubItem: false, + ), + + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Divider(height: 1, color: AppColor.borderLight), + ), + + // HPP + _buildItemRow( + label: context.lang.hpp, + nominal: summary.hpp, + percentage: pct(summary.hpp), + isBold: false, + isSubItem: false, + ), + + // Gross Profit (bold) + _buildItemRow( + label: context.lang.gross_profit, + nominal: summary.grossProfit, + percentage: pct(summary.grossProfit), + isBold: true, + isSubItem: false, + ), + + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Divider(height: 1, color: AppColor.borderLight), + ), + + // Biaya Operasional (bold header) + _buildItemRow( + label: context.lang.operational_expenses, + nominal: summary.operationalExpensesTotal, + percentage: pct(summary.operationalExpensesTotal), + isBold: true, + isSubItem: false, + ), + + // Sub-items: Gaji + _buildItemRow( + label: context.lang.salary_total, + nominal: summary.salaryTotal, + percentage: pct(summary.salaryTotal), + isBold: false, + isSubItem: true, + ), + + // Sub-items: Gaji DW + _buildItemRow( + label: context.lang.salary_dw, + nominal: summary.salaryDw, + percentage: pct(summary.salaryDw), + isBold: false, + isSubItem: true, + ), + + // Sub-items: Gaji Staff + _buildItemRow( + label: context.lang.salary_staff, + nominal: summary.salaryStaff, + percentage: pct(summary.salaryStaff), + isBold: false, + isSubItem: true, + ), + + // Sub-items: Gaji Lainnya + _buildItemRow( + label: context.lang.salary_other, + nominal: summary.salaryOther, + percentage: pct(summary.salaryOther), + isBold: false, + isSubItem: true, + ), + + // Sub-items: Biaya Operasional Lainnya + _buildItemRow( + label: context.lang.other_operational_expenses, + nominal: summary.otherOperationalExpenses, + percentage: pct(summary.otherOperationalExpenses), + isBold: false, + isSubItem: true, + ), + + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Divider(height: 1, color: AppColor.borderLight), + ), + + // Total Biaya + _buildItemRow( + label: context.lang.total_cost, + nominal: totalCost, + percentage: pct(totalCost), + isBold: true, + isSubItem: false, + ), + + const SizedBox(height: 12), + + // Net Profit/Loss footer + _buildNetProfitFooter(context, summary), + ], + ), + ); + } + + Widget _buildItemRow({ + required String label, + required int nominal, + required double percentage, + required bool isBold, + required bool isSubItem, + }) { + final isNegative = nominal < 0; + final displayNominal = isNegative + ? '-${nominal.abs().currencyFormatRp}' + : nominal.currencyFormatRp; + final pctText = '${percentage.round()}%'; + + Color nominalColor; + if (isBold && isNegative) { + nominalColor = AppColor.error; + } else { + nominalColor = AppColor.textPrimary; + } + + return Padding( + padding: EdgeInsets.only(left: isSubItem ? 16 : 0, top: 6, bottom: 6), + child: Row( + children: [ + // Label + Expanded( + flex: 5, + child: Text( + label, + style: isBold + ? AppStyle.md.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + ) + : AppStyle.md.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w400, + ), + ), + ), + + // Nominal + Expanded( + flex: 3, + child: Text( + displayNominal, + textAlign: TextAlign.right, + style: isBold + ? AppStyle.md.copyWith( + fontWeight: FontWeight.w700, + color: nominalColor, + ) + : AppStyle.md.copyWith( + color: AppColor.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + ), + + // Percentage + SizedBox( + width: 48, + child: Text( + pctText, + textAlign: TextAlign.right, + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ); + } + + Widget _buildNetProfitFooter( + BuildContext context, + ExclusiveSummarySummary summary, + ) { + final netProfit = summary.netProfit; + final isNegative = netProfit < 0; + final displayValue = isNegative + ? '-${netProfit.abs().currencyFormatRp}' + : netProfit.currencyFormatRp; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: isNegative + ? AppColor.error.withOpacity(0.08) + : AppColor.success.withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Flexible( + child: Text( + context.lang.net_profit_loss, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w700, + color: isNegative ? AppColor.error : AppColor.success, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 12), + Text( + displayValue, + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w900, + color: isNegative ? AppColor.error : AppColor.success, + fontSize: 20, + ), + ), + ], + ), + ); + } + + // ─── REIMBURSE SECTION ────────────────────────────────────────────────────── + + Widget _buildReimburseSection( + BuildContext context, + ExclusiveSummaryReimburse reimburse, + ) { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppColor.textLight.withOpacity(0.08), + spreadRadius: 1, + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.lang.reimburse_summary, + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + ), + ), + const SizedBox(height: 16), _buildReimburseRow( context.lang.total_cost, reimburse.totalCost.currencyFormatRp, ), - const Divider(height: 20), + const Divider(height: 20, color: AppColor.borderLight), _buildReimburseRow( context.lang.excluded_salary_staff, reimburse.excludedSalaryStaff.currencyFormatRp, ), - const Divider(height: 20), + const Divider(height: 20, color: AppColor.borderLight), _buildReimburseRow( context.lang.total_reimburse, reimburse.totalReimburse.currencyFormatRp, @@ -526,365 +731,74 @@ class _ExclusiveSummaryPageState extends State children: [ Text( label, - style: AppStyle.sm.copyWith( + style: AppStyle.md.copyWith( color: isHighlighted ? AppColor.textPrimary : AppColor.textSecondary, - fontWeight: - isHighlighted ? FontWeight.bold : FontWeight.normal, + fontWeight: isHighlighted ? FontWeight.bold : FontWeight.normal, ), ), Text( value, - style: AppStyle.sm.copyWith( + style: AppStyle.md.copyWith( color: isHighlighted ? AppColor.primary : AppColor.textPrimary, - fontWeight: - isHighlighted ? FontWeight.bold : FontWeight.w600, + fontWeight: isHighlighted ? FontWeight.bold : FontWeight.w600, ), ), ], ); } - // ─── BREAKDOWN SECTION ────────────────────────────────────────────────────── + // ─── SHIMMER ──────────────────────────────────────────────────────────────── - 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, - ) { + Widget _buildShimmer() { return Padding( - padding: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.symmetric(horizontal: 16), 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), - ), - ], - ), + _shimmerBox(height: 300), 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, - ), - ), - ], - ); - }, - ), + _shimmerBox(height: 120), ], ), ); } - // ─── 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, - ), - ), - ], - ); - }, - ), - ], + 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), + ), ), ); } - 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; - } - } + // ─── HELPERS ──────────────────────────────────────────────────────────────── - String _sourceLabel(String source) { - switch (source) { - case 'purchase_order': - return 'PO'; - case 'salary': - return 'Gaji'; - case 'operational': - return 'Ops'; - default: - return source; + String _formatDateLabel(DateTime from, DateTime to) { + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'Mei', + 'Jun', + 'Jul', + 'Agu', + 'Sep', + 'Okt', + 'Nov', + 'Des', + ]; + + if (from.year == to.year && from.month == to.month && from.day == to.day) { + return '${from.day} ${months[from.month - 1]} ${from.year}'; } + return '${from.day} ${months[from.month - 1]} - ${to.day} ${months[to.month - 1]} ${to.year}'; } } diff --git a/lib/presentation/pages/exclusive_summary/widgets/daily_revenue_chart.dart b/lib/presentation/pages/exclusive_summary/widgets/daily_revenue_chart.dart new file mode 100644 index 0000000..941fb38 --- /dev/null +++ b/lib/presentation/pages/exclusive_summary/widgets/daily_revenue_chart.dart @@ -0,0 +1,186 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +import '../../../../common/extension/extension.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../../domain/analytic/analytic.dart'; + +class DailyRevenueChart extends StatelessWidget { + final List salesData; + + const DailyRevenueChart({super.key, required this.salesData}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppColor.textLight.withOpacity(0.08), + spreadRadius: 1, + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title + Text( + context.lang.daily_revenue, + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + ), + ), + const SizedBox(height: 4), + Text( + context.lang.daily_revenue_desc, + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w400, + ), + ), + + const SizedBox(height: 24), + + // Bar Chart + salesData.isEmpty + ? _buildEmptyState() + : SizedBox(height: 200, child: _buildBarChart()), + ], + ), + ); + } + + Widget _buildEmptyState() { + return SizedBox( + height: 200, + child: Center( + child: Builder( + builder: (context) => Text( + context.lang.no_data_yet, + style: AppStyle.md.copyWith(color: AppColor.textSecondary), + ), + ), + ), + ); + } + + Widget _buildBarChart() { + final maxValue = _getMaxValue(); + + return BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + maxY: maxValue, + minY: 0, + barTouchData: BarTouchData( + enabled: true, + touchTooltipData: BarTouchTooltipData( + tooltipPadding: const EdgeInsets.all(8), + getTooltipItem: (group, groupIndex, rod, rodIndex) { + if (groupIndex < salesData.length) { + final sale = salesData[groupIndex]; + return BarTooltipItem( + sale.sales.currencyFormatRp, + const TextStyle( + color: AppColor.textWhite, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ); + } + return null; + }, + ), + ), + titlesData: FlTitlesData( + show: true, + topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), + leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + getTitlesWidget: (value, meta) { + final index = value.toInt(); + if (index >= 0 && index < salesData.length) { + final date = DateTime.tryParse(salesData[index].date); + final dayName = date != null + ? _getShortDayName(date.weekday) + : ''; + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + dayName, + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + ), + gridData: FlGridData(show: false), + borderData: FlBorderData(show: false), + barGroups: _buildBarGroups(maxValue), + ), + ); + } + + List _buildBarGroups(double maxValue) { + return salesData.asMap().entries.map((entry) { + final index = entry.key; + final sale = entry.value; + final value = sale.sales.toDouble(); + + // Gradient from green to lighter green for higher values + final ratio = maxValue > 0 ? value / maxValue : 0.0; + final color = Color.lerp( + AppColor.success, + AppColor.success.withGreen(230), + ratio, + )!; + + return BarChartGroupData( + x: index, + barRods: [ + BarChartRodData( + toY: value, + width: 28, + borderRadius: BorderRadius.circular(8), + gradient: LinearGradient( + colors: [color, color.withOpacity(0.8)], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + ), + ), + ], + ); + }).toList(); + } + + double _getMaxValue() { + if (salesData.isEmpty) return 1000000; + final maxValue = salesData + .map((e) => e.sales.toDouble()) + .reduce((a, b) => a > b ? a : b); + return maxValue * 1.2; + } + + String _getShortDayName(int weekday) { + const days = ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min']; + return days[weekday - 1]; + } +} diff --git a/lib/presentation/pages/inventory/widgets/inventory_header.dart b/lib/presentation/pages/inventory/widgets/inventory_header.dart index 372730d..da43d7d 100644 --- a/lib/presentation/pages/inventory/widgets/inventory_header.dart +++ b/lib/presentation/pages/inventory/widgets/inventory_header.dart @@ -27,7 +27,7 @@ class InventoryHeader extends StatelessWidget { Widget build(BuildContext context) { final outletLabel = state.inventoryAnalytic.summary.outletName.isNotEmpty ? state.inventoryAnalytic.summary.outletName - : 'Semua Outlet'; + : context.lang.all_outlets; final dateLabel = _formatDateRange(state.dateFrom, state.dateTo); return Container( @@ -103,23 +103,25 @@ class InventoryHeader extends StatelessWidget { // Back button + Title row + Calendar button Row( children: [ - GestureDetector( - onTap: () => context.router.maybePop(), - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppColor.textWhite.withOpacity(0.15), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.chevron_left_rounded, - color: AppColor.textWhite, - size: 24, + if (context.router.canPop()) ...[ + GestureDetector( + onTap: () => context.router.maybePop(), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColor.textWhite.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.chevron_left_rounded, + color: AppColor.textWhite, + size: 24, + ), ), ), - ), - const SpaceWidth(12), + const SpaceWidth(12), + ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -176,7 +178,7 @@ class InventoryHeader extends StatelessWidget { // Total Value label Text( - 'Total Nilai Inventori', + context.lang.total_inventory_value, style: AppStyle.sm.copyWith( color: AppColor.textWhite.withOpacity(0.75), fontWeight: FontWeight.w400, diff --git a/lib/presentation/pages/main/main_page.dart b/lib/presentation/pages/main/main_page.dart index f180061..b26c209 100644 --- a/lib/presentation/pages/main/main_page.dart +++ b/lib/presentation/pages/main/main_page.dart @@ -11,7 +11,12 @@ class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return AutoTabsRouter.pageView( - routes: [HomeRoute(), OrderRoute(), ReportRoute(), ProfileRoute()], + routes: [ + HomeRoute(), + ExclusiveSummaryRoute(), + InventoryRoute(), + ProfileRoute(), + ], physics: const NeverScrollableScrollPhysics(), builder: (context, child, pageController) { final tabsRouter = AutoTabsRouter.of(context); diff --git a/lib/presentation/pages/main/widgets/bottom_navbar.dart b/lib/presentation/pages/main/widgets/bottom_navbar.dart index c9d19bb..a3a6a9e 100644 --- a/lib/presentation/pages/main/widgets/bottom_navbar.dart +++ b/lib/presentation/pages/main/widgets/bottom_navbar.dart @@ -30,16 +30,16 @@ class _MainBottomNavbarState extends State { label: context.lang.home, tooltip: context.lang.home, ), - BottomNavigationBarItem( - icon: HugeIcon(icon: HugeIcons.strokeRoundedBorderFull), - label: context.lang.order, - tooltip: context.lang.order, - ), BottomNavigationBarItem( icon: HugeIcon(icon: HugeIcons.strokeRoundedChart03), label: context.lang.report, tooltip: context.lang.report, ), + BottomNavigationBarItem( + icon: HugeIcon(icon: HugeIcons.strokeRoundedPackage), + label: context.lang.stock, + tooltip: context.lang.stock, + ), BottomNavigationBarItem( icon: HugeIcon(icon: HugeIcons.strokeRoundedUser), label: context.lang.profile, diff --git a/lib/presentation/router/app_router.dart b/lib/presentation/router/app_router.dart index cca2477..62d48c2 100644 --- a/lib/presentation/router/app_router.dart +++ b/lib/presentation/router/app_router.dart @@ -16,8 +16,8 @@ class AppRouter extends RootStackRouter { page: MainRoute.page, children: [ AutoRoute(page: HomeRoute.page), - AutoRoute(page: OrderRoute.page), - AutoRoute(page: ReportRoute.page), + AutoRoute(page: InventoryRoute.page), + AutoRoute(page: ExclusiveSummaryRoute.page), AutoRoute(page: ProfileRoute.page), ], ),