diff --git a/android/gradle.properties b/android/gradle.properties index f018a61..475a628 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,7 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/assets/icons/ic-report-stock.png b/assets/icons/ic-report-stock.png new file mode 100644 index 0000000..5f0a54a Binary files /dev/null and b/assets/icons/ic-report-stock.png differ diff --git a/assets/images/ic_launcher.png b/assets/images/ic_launcher.png new file mode 100644 index 0000000..295e67c Binary files /dev/null and b/assets/images/ic_launcher.png differ diff --git a/assets/images/logo.png b/assets/images/logo.png index ddc7ef9..a9c659f 100644 Binary files a/assets/images/logo.png and b/assets/images/logo.png differ diff --git a/lib/injection.config.dart b/lib/injection.config.dart index f56e5e9..dfd7db4 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -138,9 +138,9 @@ extension GetItInjectableX on _i174.GetIt { final gh = _i526.GetItHelper(this, environment, environmentFilter); final firebaseDi = _$FirebaseDi(); final sharedPreferencesDi = _$SharedPreferencesDi(); - final dioDi = _$DioDi(); final autoRouteDi = _$AutoRouteDi(); final connectivityDi = _$ConnectivityDi(); + final dioDi = _$DioDi(); final packageInfoDi = _$PackageInfoDi(); await gh.factoryAsync<_i982.FirebaseApp>( () => firebaseDi.firebaseApp, @@ -150,9 +150,9 @@ extension GetItInjectableX on _i174.GetIt { () => sharedPreferencesDi.prefs, preResolve: true, ); - gh.lazySingleton<_i361.Dio>(() => dioDi.dio); gh.lazySingleton<_i258.AppRouter>(() => autoRouteDi.appRouter); gh.lazySingleton<_i895.Connectivity>(() => connectivityDi.connectivity); + gh.lazySingleton<_i361.Dio>(() => dioDi.dio); await gh.lazySingletonAsync<_i655.PackageInfo>( () => packageInfoDi.packageInfo, preResolve: true, @@ -176,29 +176,29 @@ extension GetItInjectableX on _i174.GetIt { () => _i115.ApiClient(gh<_i361.Dio>(), gh<_i6.Env>()), ); gh.factory<_i6.Env>(() => _i6.ProdEnv(), registerFor: {_prod}); - gh.factory<_i130.OrderRemoteDataProvider>( - () => _i130.OrderRemoteDataProvider(gh<_i115.ApiClient>()), - ); - gh.factory<_i333.CategoryRemoteDataProvider>( - () => _i333.CategoryRemoteDataProvider(gh<_i115.ApiClient>()), + gh.factory<_i866.AnalyticRemoteDataProvider>( + () => _i866.AnalyticRemoteDataProvider(gh<_i115.ApiClient>()), ); gh.factory<_i17.AuthRemoteDataProvider>( () => _i17.AuthRemoteDataProvider(gh<_i115.ApiClient>()), ); - gh.factory<_i785.UserRemoteDataProvider>( - () => _i785.UserRemoteDataProvider(gh<_i115.ApiClient>()), + gh.factory<_i333.CategoryRemoteDataProvider>( + () => _i333.CategoryRemoteDataProvider(gh<_i115.ApiClient>()), ); - gh.factory<_i823.ProductRemoteDataProvider>( - () => _i823.ProductRemoteDataProvider(gh<_i115.ApiClient>()), + gh.factory<_i1006.CustomerRemoteDataProvider>( + () => _i1006.CustomerRemoteDataProvider(gh<_i115.ApiClient>()), + ); + gh.factory<_i130.OrderRemoteDataProvider>( + () => _i130.OrderRemoteDataProvider(gh<_i115.ApiClient>()), ); gh.factory<_i27.OutletRemoteDataProvider>( () => _i27.OutletRemoteDataProvider(gh<_i115.ApiClient>()), ); - gh.factory<_i866.AnalyticRemoteDataProvider>( - () => _i866.AnalyticRemoteDataProvider(gh<_i115.ApiClient>()), + gh.factory<_i823.ProductRemoteDataProvider>( + () => _i823.ProductRemoteDataProvider(gh<_i115.ApiClient>()), ); - gh.factory<_i1006.CustomerRemoteDataProvider>( - () => _i1006.CustomerRemoteDataProvider(gh<_i115.ApiClient>()), + gh.factory<_i785.UserRemoteDataProvider>( + () => _i785.UserRemoteDataProvider(gh<_i115.ApiClient>()), ); gh.factory<_i48.ICustomerRepository>( () => _i550.CustomerRepository(gh<_i1006.CustomerRemoteDataProvider>()), @@ -252,14 +252,14 @@ extension GetItInjectableX on _i174.GetIt { gh<_i850.OutletLocalDataProvider>(), ), ); - gh.factory<_i473.HomeBloc>( - () => _i473.HomeBloc(gh<_i477.IAnalyticRepository>()), + gh.factory<_i755.PurchasingAnalyticLoaderBloc>( + () => _i755.PurchasingAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), ); gh.factory<_i889.SalesLoaderBloc>( () => _i889.SalesLoaderBloc(gh<_i477.IAnalyticRepository>()), ); - gh.factory<_i755.PurchasingAnalyticLoaderBloc>( - () => _i755.PurchasingAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), + gh.factory<_i473.HomeBloc>( + () => _i473.HomeBloc(gh<_i477.IAnalyticRepository>()), ); gh.factory<_i877.OutletListLoaderBloc>( () => _i877.OutletListLoaderBloc(gh<_i197.IOutletRepository>()), @@ -267,8 +267,14 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i337.CurrentOutletLoaderBloc>( () => _i337.CurrentOutletLoaderBloc(gh<_i197.IOutletRepository>()), ); - gh.factory<_i221.ProductAnalyticLoaderBloc>( - () => _i221.ProductAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), + gh.factory<_i1038.CategoryAnalyticLoaderBloc>( + () => _i1038.CategoryAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), + ); + gh.factory<_i516.DashboardAnalyticLoaderBloc>( + () => _i516.DashboardAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), + ); + gh.factory<_i702.ExclusiveSummaryLoaderBloc>( + () => _i702.ExclusiveSummaryLoaderBloc(gh<_i477.IAnalyticRepository>()), ); gh.factory<_i785.InventoryAnalyticLoaderBloc>( () => _i785.InventoryAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), @@ -278,18 +284,12 @@ extension GetItInjectableX on _i174.GetIt { gh<_i477.IAnalyticRepository>(), ), ); - gh.factory<_i1038.CategoryAnalyticLoaderBloc>( - () => _i1038.CategoryAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), + gh.factory<_i221.ProductAnalyticLoaderBloc>( + () => _i221.ProductAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), ); gh.factory<_i11.ProfitLossLoaderBloc>( () => _i11.ProfitLossLoaderBloc(gh<_i477.IAnalyticRepository>()), ); - gh.factory<_i516.DashboardAnalyticLoaderBloc>( - () => _i516.DashboardAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), - ); - gh.factory<_i702.ExclusiveSummaryLoaderBloc>( - () => _i702.ExclusiveSummaryLoaderBloc(gh<_i477.IAnalyticRepository>()), - ); gh.factory<_i945.AuthBloc>( () => _i945.AuthBloc(gh<_i49.IAuthRepository>()), ); @@ -299,12 +299,12 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i1058.OrderLoaderBloc>( () => _i1058.OrderLoaderBloc(gh<_i219.IOrderRepository>()), ); - gh.factory<_i147.UserEditFormBloc>( - () => _i147.UserEditFormBloc(gh<_i635.IUserRepository>()), - ); gh.factory<_i1030.ChangePasswordFormBloc>( () => _i1030.ChangePasswordFormBloc(gh<_i635.IUserRepository>()), ); + gh.factory<_i147.UserEditFormBloc>( + () => _i147.UserEditFormBloc(gh<_i635.IUserRepository>()), + ); gh.factory<_i775.LoginFormBloc>( () => _i775.LoginFormBloc( gh<_i49.IAuthRepository>(), @@ -312,14 +312,14 @@ extension GetItInjectableX on _i174.GetIt { gh<_i179.FcmService>(), ), ); - gh.factory<_i605.TransactionReportBloc>( - () => _i605.TransactionReportBloc( + gh.factory<_i346.InventoryReportBloc>( + () => _i346.InventoryReportBloc( gh<_i477.IAnalyticRepository>(), gh<_i197.IOutletRepository>(), ), ); - gh.factory<_i346.InventoryReportBloc>( - () => _i346.InventoryReportBloc( + gh.factory<_i605.TransactionReportBloc>( + () => _i605.TransactionReportBloc( gh<_i477.IAnalyticRepository>(), gh<_i197.IOutletRepository>(), ), @@ -332,10 +332,10 @@ class _$FirebaseDi extends _i73.FirebaseDi {} class _$SharedPreferencesDi extends _i402.SharedPreferencesDi {} -class _$DioDi extends _i103.DioDi {} - class _$AutoRouteDi extends _i311.AutoRouteDi {} class _$ConnectivityDi extends _i586.ConnectivityDi {} +class _$DioDi extends _i103.DioDi {} + class _$PackageInfoDi extends _i227.PackageInfoDi {} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b85a943..49c3d5f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -455,5 +455,46 @@ "operational_expenses": "Operational Expenses", "@operational_expenses": {}, "total_cost": "Total Cost", - "@total_cost": {} + "@total_cost": {}, + "warning_title": "Warnings", + "@warning_title": {}, + "warning_desc": "Activities deviating from standards — needs review.", + "@warning_desc": {}, + "no_warning": "No warnings", + "@no_warning": {}, + "no_warning_desc": "All activities are running normally.", + "@no_warning_desc": {}, + "severity_high": "High", + "@severity_high": {}, + "severity_medium": "Medium", + "@severity_medium": {}, + "compared_to_previous_period": "Compared to previous period", + "@compared_to_previous_period": {}, + "summary_today": "Today's Summary", + "@summary_today": {}, + "summary_mtd": "MTD Summary", + "@summary_mtd": {}, + "total_sales_label": "Total sales", + "@total_sales_label": {}, + "total_raw_material": "Total raw material cost", + "@total_raw_material": {}, + "net_profit_label": "Net profit", + "@net_profit_label": {}, + "items_sold": "Items Sold", + "@items_sold": {}, + "low_stock_warning": "Low Stock", + "@low_stock_warning": {}, + "active_products": "Active Products", + "@active_products": {}, + "today_condition": "Today's Condition", + "@today_condition": {}, + "portion_sold": "{count} portions sold", + "@portion_sold": { + "placeholders": { + "count": { + "type": "int", + "example": "48" + } + } + } } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 4615078..6c91c48 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -48,11 +48,11 @@ "@reports": {}, "profile": "Profil", "@profile": {}, - "sales_today": "Penjualan hari ini", + "sales_today": "Omset hari ini", "@sales_today": {}, "order": "Pesanan", "@order": {}, - "sales": "Penjualan", + "sales": "Omset", "@sales": {}, "finance": "Keuangan", "@finance": {}, @@ -62,7 +62,7 @@ "@form": {}, "schedule": "Jadwal", "@schedule": {}, - "inventory": "Inventaris", + "inventory": "Stok", "@inventory": {}, "customer": "Pelanggan", "@customer": {}, @@ -455,5 +455,46 @@ "operational_expenses": "Biaya Operasional", "@operational_expenses": {}, "total_cost": "Total Biaya", - "@total_cost": {} + "@total_cost": {}, + "warning_title": "Peringatan", + "@warning_title": {}, + "warning_desc": "Aktivitas yang menyimpang dari standar — perlu ditinjau.", + "@warning_desc": {}, + "no_warning": "Tidak ada peringatan", + "@no_warning": {}, + "no_warning_desc": "Semua aktivitas berjalan normal.", + "@no_warning_desc": {}, + "severity_high": "Tinggi", + "@severity_high": {}, + "severity_medium": "Sedang", + "@severity_medium": {}, + "compared_to_previous_period": "Dibanding periode lalu", + "@compared_to_previous_period": {}, + "summary_today": "Ringkasan Hari Ini", + "@summary_today": {}, + "summary_mtd": "Ringkasan MTD", + "@summary_mtd": {}, + "total_sales_label": "Total penjualan", + "@total_sales_label": {}, + "total_raw_material": "Total biaya bahan baku", + "@total_raw_material": {}, + "net_profit_label": "Laba bersih", + "@net_profit_label": {}, + "items_sold": "Item Terjual", + "@items_sold": {}, + "low_stock_warning": "Stok Menipis", + "@low_stock_warning": {}, + "active_products": "Produk Aktif", + "@active_products": {}, + "today_condition": "Kondisi Hari Ini", + "@today_condition": {}, + "portion_sold": "{count} porsi terjual", + "@portion_sold": { + "placeholders": { + "count": { + "type": "int", + "example": "48" + } + } + } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0afcc88..45104a9 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1378,6 +1378,108 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Total Cost'** String get total_cost; + + /// No description provided for @warning_title. + /// + /// In en, this message translates to: + /// **'Warnings'** + String get warning_title; + + /// No description provided for @warning_desc. + /// + /// In en, this message translates to: + /// **'Activities deviating from standards — needs review.'** + String get warning_desc; + + /// No description provided for @no_warning. + /// + /// In en, this message translates to: + /// **'No warnings'** + String get no_warning; + + /// No description provided for @no_warning_desc. + /// + /// In en, this message translates to: + /// **'All activities are running normally.'** + String get no_warning_desc; + + /// No description provided for @severity_high. + /// + /// In en, this message translates to: + /// **'High'** + String get severity_high; + + /// No description provided for @severity_medium. + /// + /// In en, this message translates to: + /// **'Medium'** + String get severity_medium; + + /// No description provided for @compared_to_previous_period. + /// + /// In en, this message translates to: + /// **'Compared to previous period'** + String get compared_to_previous_period; + + /// No description provided for @summary_today. + /// + /// In en, this message translates to: + /// **'Today\'s Summary'** + String get summary_today; + + /// No description provided for @summary_mtd. + /// + /// In en, this message translates to: + /// **'MTD Summary'** + String get summary_mtd; + + /// No description provided for @total_sales_label. + /// + /// In en, this message translates to: + /// **'Total sales'** + String get total_sales_label; + + /// No description provided for @total_raw_material. + /// + /// In en, this message translates to: + /// **'Total raw material cost'** + String get total_raw_material; + + /// No description provided for @net_profit_label. + /// + /// In en, this message translates to: + /// **'Net profit'** + String get net_profit_label; + + /// No description provided for @items_sold. + /// + /// In en, this message translates to: + /// **'Items Sold'** + String get items_sold; + + /// No description provided for @low_stock_warning. + /// + /// In en, this message translates to: + /// **'Low Stock'** + String get low_stock_warning; + + /// No description provided for @active_products. + /// + /// In en, this message translates to: + /// **'Active Products'** + String get active_products; + + /// No description provided for @today_condition. + /// + /// In en, this message translates to: + /// **'Today\'s Condition'** + String get today_condition; + + /// No description provided for @portion_sold. + /// + /// In en, this message translates to: + /// **'{count} portions sold'** + String portion_sold(int count); } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 493d2e0..bd9cf39 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -657,4 +657,57 @@ class AppLocalizationsEn extends AppLocalizations { @override String get total_cost => 'Total Cost'; + + @override + String get warning_title => 'Warnings'; + + @override + String get warning_desc => 'Activities deviating from standards — needs review.'; + + @override + String get no_warning => 'No warnings'; + + @override + String get no_warning_desc => 'All activities are running normally.'; + + @override + String get severity_high => 'High'; + + @override + String get severity_medium => 'Medium'; + + @override + String get compared_to_previous_period => 'Compared to previous period'; + + @override + String get summary_today => 'Today\'s Summary'; + + @override + String get summary_mtd => 'MTD Summary'; + + @override + String get total_sales_label => 'Total sales'; + + @override + String get total_raw_material => 'Total raw material cost'; + + @override + String get net_profit_label => 'Net profit'; + + @override + String get items_sold => 'Items Sold'; + + @override + String get low_stock_warning => 'Low Stock'; + + @override + String get active_products => 'Active Products'; + + @override + String get today_condition => 'Today\'s Condition'; + + @override + String portion_sold(int count) { + return '$count portions sold'; + } } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 3af50e6..7c9b2de 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -81,13 +81,13 @@ class AppLocalizationsId extends AppLocalizations { String get profile => 'Profil'; @override - String get sales_today => 'Penjualan hari ini'; + String get sales_today => 'Omset hari ini'; @override String get order => 'Pesanan'; @override - String get sales => 'Penjualan'; + String get sales => 'Omset'; @override String get finance => 'Keuangan'; @@ -102,7 +102,7 @@ class AppLocalizationsId extends AppLocalizations { String get schedule => 'Jadwal'; @override - String get inventory => 'Inventaris'; + String get inventory => 'Stok'; @override String get customer => 'Pelanggan'; @@ -657,4 +657,57 @@ class AppLocalizationsId extends AppLocalizations { @override String get total_cost => 'Total Biaya'; + + @override + String get warning_title => 'Peringatan'; + + @override + String get warning_desc => 'Aktivitas yang menyimpang dari standar — perlu ditinjau.'; + + @override + String get no_warning => 'Tidak ada peringatan'; + + @override + String get no_warning_desc => 'Semua aktivitas berjalan normal.'; + + @override + String get severity_high => 'Tinggi'; + + @override + String get severity_medium => 'Sedang'; + + @override + String get compared_to_previous_period => 'Dibanding periode lalu'; + + @override + String get summary_today => 'Ringkasan Hari Ini'; + + @override + String get summary_mtd => 'Ringkasan MTD'; + + @override + String get total_sales_label => 'Total penjualan'; + + @override + String get total_raw_material => 'Total biaya bahan baku'; + + @override + String get net_profit_label => 'Laba bersih'; + + @override + String get items_sold => 'Item Terjual'; + + @override + String get low_stock_warning => 'Stok Menipis'; + + @override + String get active_products => 'Produk Aktif'; + + @override + String get today_condition => 'Kondisi Hari Ini'; + + @override + String portion_sold(int count) { + return '$count porsi terjual'; + } } diff --git a/lib/presentation/components/assets/assets.gen.dart b/lib/presentation/components/assets/assets.gen.dart index 4a1d7ba..9e9480a 100644 --- a/lib/presentation/components/assets/assets.gen.dart +++ b/lib/presentation/components/assets/assets.gen.dart @@ -34,6 +34,10 @@ class $AssetsIconsGen { AssetGenImage get icReportSales => const AssetGenImage('assets/icons/ic-report-sales.png'); + /// File path: assets/icons/ic-report-stock.png + AssetGenImage get icReportStock => + const AssetGenImage('assets/icons/ic-report-stock.png'); + /// List of all assets List get values => [ icReportExclusiveSummary, @@ -41,6 +45,7 @@ class $AssetsIconsGen { icReportProfitLoss, icReportPurchase, icReportSales, + icReportStock, ]; } diff --git a/lib/presentation/components/component.dart b/lib/presentation/components/component.dart deleted file mode 100644 index 7199fdd..0000000 --- a/lib/presentation/components/component.dart +++ /dev/null @@ -1 +0,0 @@ - // TODO: define your code diff --git a/lib/presentation/pages/about_app/about_app_page.dart b/lib/presentation/pages/about_app/about_app_page.dart index 9b42104..d965acb 100644 --- a/lib/presentation/pages/about_app/about_app_page.dart +++ b/lib/presentation/pages/about_app/about_app_page.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -75,7 +76,9 @@ class _AboutAppPageState extends State deviceInfo = device; }); } catch (e) { - print('Error loading app info: $e'); + if (kDebugMode) { + print('Error loading app info: $e'); + } } } diff --git a/lib/presentation/pages/auth/login/login_page.dart b/lib/presentation/pages/auth/login/login_page.dart index 76b5e2a..968fd7f 100644 --- a/lib/presentation/pages/auth/login/login_page.dart +++ b/lib/presentation/pages/auth/login/login_page.dart @@ -16,86 +16,12 @@ import 'widgets/email_field.dart'; import 'widgets/password_field.dart'; @RoutePage() -class LoginPage extends StatefulWidget implements AutoRouteWrapper { +class LoginPage extends StatelessWidget implements AutoRouteWrapper { const LoginPage({super.key}); - @override - State createState() => _LoginPageState(); - @override Widget wrappedRoute(BuildContext context) => BlocProvider(create: (_) => getIt(), child: this); -} - -class _LoginPageState extends State with TickerProviderStateMixin { - late AnimationController _fadeController; - late AnimationController _slideController; - late AnimationController _backgroundController; - late AnimationController _floatingController; - - late Animation _fadeAnimation; - late Animation _slideAnimation; - late Animation _backgroundAnimation; - late Animation _floatingAnimation; - - @override - void initState() { - super.initState(); - - _fadeController = AnimationController( - duration: const Duration(milliseconds: 1500), - vsync: this, - ); - - _slideController = AnimationController( - duration: const Duration(milliseconds: 1200), - vsync: this, - ); - - _backgroundController = AnimationController( - duration: const Duration(seconds: 10), - vsync: this, - )..repeat(); - - _floatingController = AnimationController( - duration: const Duration(seconds: 6), - vsync: this, - )..repeat(reverse: true); - - _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut), - ); - - _slideAnimation = - Tween(begin: const Offset(0, 0.3), end: Offset.zero).animate( - CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic), - ); - - _backgroundAnimation = Tween( - begin: 0.0, - end: 2 * math.pi, - ).animate(_backgroundController); - - _floatingAnimation = Tween(begin: -20.0, end: 20.0).animate( - CurvedAnimation(parent: _floatingController, curve: Curves.easeInOut), - ); - - _fadeController.forward(); - _slideController.forward(); - } - - @override - void dispose() { - _fadeController.dispose(); - _slideController.dispose(); - _backgroundController.dispose(); - _floatingController.dispose(); - super.dispose(); - } - - Future _handleLogin() async { - context.read().add(LoginFormEvent.submitted()); - } @override Widget build(BuildContext context) { @@ -117,83 +43,63 @@ class _LoginPageState extends State with TickerProviderStateMixin { ); }, child: Scaffold( - body: AnimatedBuilder( - animation: Listenable.merge([ - _backgroundController, - _floatingController, - ]), - builder: (context, child) { - return Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: AppColor.primaryGradient, - ), - ), - child: Stack( - children: [ - // Animated background elements - _buildAnimatedBackground(), - - // Main content - SafeArea( - child: Center( - child: SingleChildScrollView( - padding: EdgeInsets.symmetric( - horizontal: AppValue.padding, - ), - child: FadeTransition( - opacity: _fadeAnimation, - child: SlideTransition( - position: _slideAnimation, - child: BlocBuilder( - builder: (context, state) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildLogo(context), - SpaceHeight(48), - _buildLoginCard( - context, - state.isSubmitting, - state.showErrorMessages, - ), - ], - ); - }, + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: AppColor.primaryGradient, + ), + ), + child: Stack( + children: [ + _buildStaticBackground(context), + SafeArea( + child: Center( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: AppValue.padding), + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildLogo(context), + const SpaceHeight(48), + _buildLoginCard( + context, + state.isSubmitting, + state.showErrorMessages, ), - ), - ), - ), + ], + ); + }, ), ), - ], + ), ), - ); - }, + ], + ), ), ), ); } - Widget _buildAnimatedBackground() { + Widget _buildStaticBackground(BuildContext context) { + final size = MediaQuery.of(context).size; return Stack( children: [ - // Floating circles + // Static circles ...List.generate(6, (index) { - final double size = 80 + (index * 40); - final double left = - (index * 60.0) % MediaQuery.of(context).size.width; - final double top = - (index * 120.0) % MediaQuery.of(context).size.height; + final double circleSize = 80 + (index * 40); + final double left = (index * 60.0) % size.width; + final double top = (index * 120.0) % size.height; return Positioned( - left: left + math.sin(_backgroundAnimation.value + index) * 30, - top: top + _floatingAnimation.value + (index * 10), + left: left, + top: top, child: Container( - width: size, - height: size, + width: circleSize, + height: circleSize, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withOpacity(0.1), @@ -206,12 +112,12 @@ class _LoginPageState extends State with TickerProviderStateMixin { ); }), - // Rotating geometric shapes + // Geometric shapes Positioned( top: 100, right: 50, child: Transform.rotate( - angle: _backgroundAnimation.value, + angle: math.pi / 4, child: Container( width: 60, height: 60, @@ -230,46 +136,37 @@ class _LoginPageState extends State with TickerProviderStateMixin { Positioned( bottom: 150, left: 30, - child: Transform.rotate( - angle: -_backgroundAnimation.value * 0.5, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.06), - shape: BoxShape.circle, - border: Border.all( - color: Colors.white.withOpacity(0.12), - width: 1, - ), + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.06), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withOpacity(0.12), + width: 1, ), ), ), ), - // Floating particles + // Static particles ...List.generate(8, (index) { return Positioned( - left: (index * 45.0) % MediaQuery.of(context).size.width, - top: (index * 80.0) % MediaQuery.of(context).size.height, - child: Transform.translate( - offset: Offset( - math.sin(_backgroundAnimation.value + index * 0.5) * 20, - math.cos(_backgroundAnimation.value + index * 0.3) * 15, - ), - child: Container( - width: 4 + (index % 3) * 2, - height: 4 + (index % 3) * 2, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.white.withOpacity(0.3), - ), + left: (index * 45.0) % size.width, + top: (index * 80.0) % size.height, + child: Container( + width: 4.0 + (index % 3) * 2, + height: 4.0 + (index % 3) * 2, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withOpacity(0.3), ), ), ); }), - // Gradient overlay for better text readability + // Gradient overlay Container( decoration: BoxDecoration( gradient: LinearGradient( @@ -295,29 +192,13 @@ class _LoginPageState extends State with TickerProviderStateMixin { style: AppStyle.h1.copyWith( fontWeight: FontWeight.bold, color: AppColor.white, - shadows: [ - Shadow( - offset: const Offset(0, 2), - blurRadius: 10, - color: Colors.black.withOpacity(0.3), - ), - ], ), textAlign: TextAlign.center, ), const SpaceHeight(8), Text( context.lang.login_desc, - style: AppStyle.lg.copyWith( - color: AppColor.textLight, - shadows: [ - Shadow( - offset: const Offset(0, 1), - blurRadius: 5, - color: Colors.black.withOpacity(0.2), - ), - ], - ), + style: AppStyle.lg.copyWith(color: AppColor.textLight), ), ], ); @@ -353,7 +234,6 @@ class _LoginPageState extends State with TickerProviderStateMixin { autovalidateMode: showErrorMessages ? AutovalidateMode.always : AutovalidateMode.disabled, - child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -363,7 +243,7 @@ class _LoginPageState extends State with TickerProviderStateMixin { const SpaceHeight(16), _buildForgetPassword(context), const SpaceHeight(32), - _buildLoginButton(isLoading), + _buildLoginButton(context, isLoading), ], ), ), @@ -386,11 +266,13 @@ class _LoginPageState extends State with TickerProviderStateMixin { ); } - Widget _buildLoginButton(bool isLoading) { + Widget _buildLoginButton(BuildContext context, bool isLoading) { return AppElevatedButton( text: context.lang.sign_in, isLoading: isLoading, - onPressed: _handleLogin, + onPressed: () { + context.read().add(LoginFormEvent.submitted()); + }, ); } } diff --git a/lib/presentation/pages/error/error_page.dart b/lib/presentation/pages/error/error_page.dart index 0ad68ac..42d38ae 100644 --- a/lib/presentation/pages/error/error_page.dart +++ b/lib/presentation/pages/error/error_page.dart @@ -462,6 +462,8 @@ class _ErrorPageState extends State with TickerProviderStateMixin { // Usage Example dengan berbagai variasi class ErrorPageExamples extends StatelessWidget { + const ErrorPageExamples({super.key}); + @override Widget build(BuildContext context) { return Column( diff --git a/lib/presentation/pages/form/daily_task_form_page.dart b/lib/presentation/pages/form/daily_task_form_page.dart index 8862ecb..01c453d 100644 --- a/lib/presentation/pages/form/daily_task_form_page.dart +++ b/lib/presentation/pages/form/daily_task_form_page.dart @@ -191,7 +191,7 @@ class _DailyTasksFormPageState extends State const SizedBox(height: 16), ...section.questions.map((question) { return _buildQuestionCard(question); - }).toList(), + }), ], ), ); diff --git a/lib/presentation/pages/home/home_page.dart b/lib/presentation/pages/home/home_page.dart index a811baf..9bcf5ad 100644 --- a/lib/presentation/pages/home/home_page.dart +++ b/lib/presentation/pages/home/home_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:line_icons/line_icons.dart'; +import '../../../application/analytic/exclusive_summary_loader/exclusive_summary_loader_bloc.dart'; import '../../../application/home/home_bloc.dart'; import '../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart'; import '../../../application/outlet/selected_outlet/selected_outlet_bloc.dart'; @@ -13,7 +14,8 @@ import '../../components/button/button.dart'; import '../../components/spacer/spacer.dart'; import 'widgets/feature.dart'; import 'widgets/header.dart'; -import 'widgets/promo_banner.dart'; +import 'widgets/home_top_products.dart'; +import 'widgets/home_warnings.dart'; import 'widgets/stats.dart'; @RoutePage() @@ -31,13 +33,16 @@ class HomePage extends StatefulWidget implements AutoRouteWrapper { getIt()..add(HomeEvent.fetchedDashboard()), ), BlocProvider( - create: (context) => getIt() - ..add(const OutletListLoaderEvent.fetched()), + create: (context) => + getIt() + ..add(const OutletListLoaderEvent.fetched()), ), BlocProvider( - create: (context) => getIt() - ..add(const SelectedOutletEvent.loaded()), + create: (context) => + getIt() + ..add(const SelectedOutletEvent.loaded()), ), + BlocProvider(create: (context) => getIt()), ], child: this, ); @@ -89,129 +94,132 @@ class _HomePageState extends State with TickerProviderStateMixin { previous.selectedOutletId != current.selectedOutletId, listener: (context, state) => _refetchDashboard(context), child: Scaffold( - backgroundColor: AppColor.background, - body: RefreshIndicator( - backgroundColor: AppColor.white, - color: AppColor.primary, - onRefresh: () { - _refetchDashboard(context); - return Future.value(); - }, - child: BlocBuilder( - builder: (context, state) { - return CustomScrollView( - physics: const BouncingScrollPhysics( - parent: ClampingScrollPhysics(), - ), - slivers: [ - // SliverAppBar with HomeHeader as background - SliverAppBar( - expandedHeight: 300, // Adjust based on HomeHeader height - floating: false, - pinned: true, - snap: false, - elevation: 0, - scrolledUnderElevation: 8, - backgroundColor: AppColor.primary, - surfaceTintColor: Colors.transparent, - flexibleSpace: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - // Calculate collapse progress (0.0 = expanded, 1.0 = collapsed) - final double expandedHeight = 200; - final double collapsedHeight = - kToolbarHeight + MediaQuery.of(context).padding.top; - final double currentHeight = constraints.maxHeight; + backgroundColor: AppColor.background, + body: RefreshIndicator( + backgroundColor: AppColor.white, + color: AppColor.primary, + onRefresh: () { + _refetchDashboard(context); + return Future.value(); + }, + child: BlocBuilder( + builder: (context, state) { + return CustomScrollView( + physics: const BouncingScrollPhysics( + parent: ClampingScrollPhysics(), + ), + slivers: [ + // SliverAppBar with HomeHeader as background + SliverAppBar( + expandedHeight: 440, // Adjusted for new header with slider + floating: false, + pinned: true, + snap: false, + elevation: 0, + scrolledUnderElevation: 8, + backgroundColor: AppColor.primary, + surfaceTintColor: Colors.transparent, + flexibleSpace: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + // Calculate collapse progress (0.0 = expanded, 1.0 = collapsed) + final double expandedHeight = 200; + final double collapsedHeight = + kToolbarHeight + MediaQuery.of(context).padding.top; + final double currentHeight = constraints.maxHeight; - double collapseProgress = - 1.0 - - ((currentHeight - collapsedHeight) / - (expandedHeight - collapsedHeight)); - collapseProgress = collapseProgress.clamp(0.0, 1.0); + double collapseProgress = + 1.0 - + ((currentHeight - collapsedHeight) / + (expandedHeight - collapsedHeight)); + collapseProgress = collapseProgress.clamp(0.0, 1.0); - return FlexibleSpaceBar( - title: Opacity( - opacity: - collapseProgress, // Title muncul saat collapse - child: Row( - children: [ - Expanded( - child: Text( - AppConstant.appName, - style: AppStyle.xl.copyWith( - fontWeight: FontWeight.w700, - fontSize: 18, - letterSpacing: -0.5, - color: AppColor.white, + return FlexibleSpaceBar( + title: Opacity( + opacity: + collapseProgress, // Title muncul saat collapse + child: Row( + children: [ + Expanded( + child: Text( + AppConstant.appName, + style: AppStyle.xl.copyWith( + fontWeight: FontWeight.w700, + fontSize: 18, + letterSpacing: -0.5, + color: AppColor.white, + ), ), ), - ), - ActionIconButton( - onTap: () {}, - icon: LineIcons.bell, - ), - ], - ), - ), - titlePadding: const EdgeInsets.only( - left: 20, - right: 12, - bottom: 16, - ), - background: AnimatedBuilder( - animation: _headerAnimationController, - builder: (context, child) { - return Transform.translate( - offset: Offset( - 0, - 50 * (1 - _headerAnimationController.value), - ), - child: Opacity( - opacity: _headerAnimationController.value, - child: HomeHeader( - totalRevenue: - state.dashboard.overview.totalSales, + ActionIconButton( + onTap: () {}, + icon: LineIcons.bell, ), - ), - ); - }, - ), - ); - }, - ), - ), - - // Main Content - SliverToBoxAdapter( - child: AnimatedBuilder( - animation: _contentAnimationController, - builder: (context, child) { - return Transform.translate( - offset: Offset( - 0, - 30 * (1 - _contentAnimationController.value), - ), - child: Opacity( - opacity: _contentAnimationController.value, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const HomePromoBanner(), - HomeFeature(), - HomeStats(overview: state.dashboard.overview), - const SpaceHeight(40), - ], + ], + ), ), - ), - ); - }, + titlePadding: const EdgeInsets.only( + left: 20, + right: 12, + bottom: 16, + ), + background: AnimatedBuilder( + animation: _headerAnimationController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + 0, + 50 * (1 - _headerAnimationController.value), + ), + child: Opacity( + opacity: _headerAnimationController.value, + child: HomeHeader( + totalRevenue: + state.dashboard.overview.totalSales, + ), + ), + ); + }, + ), + ); + }, + ), ), - ), - ], - ); - }, + + // Main Content + SliverToBoxAdapter( + child: AnimatedBuilder( + animation: _contentAnimationController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + 0, + 30 * (1 - _contentAnimationController.value), + ), + child: Opacity( + opacity: _contentAnimationController.value, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HomeFeature(), + HomeWarnings(), + HomeStats(overview: state.dashboard.overview), + HomeTopProducts( + products: state.dashboard.topProducts, + ), + const SpaceHeight(40), + ], + ), + ), + ); + }, + ), + ), + ], + ); + }, + ), ), - ), - ), // Scaffold + ), // Scaffold ); // BlocListener } } diff --git a/lib/presentation/pages/home/widgets/feature.dart b/lib/presentation/pages/home/widgets/feature.dart index 4cc4a45..64303b6 100644 --- a/lib/presentation/pages/home/widgets/feature.dart +++ b/lib/presentation/pages/home/widgets/feature.dart @@ -13,14 +13,10 @@ class HomeFeature extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - margin: const EdgeInsets.symmetric( - vertical: 24, - horizontal: AppValue.padding, - ).copyWith(bottom: 0), + margin: const EdgeInsets.symmetric().copyWith(bottom: 0), padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 10), decoration: BoxDecoration( color: AppColor.white, - borderRadius: BorderRadius.circular(AppValue.radius), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), @@ -34,7 +30,7 @@ class HomeFeature extends StatelessWidget { children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, children: [ HomeFeatureTile( title: context.lang.sales, @@ -43,24 +39,25 @@ class HomeFeature extends StatelessWidget { ), HomeFeatureTile( title: context.lang.purchase, - iconPath: Assets.icons.icReportPurchase.path, + iconPath: Assets.icons.icReportPurchase.path, onTap: () => context.router.push(PurchaseRoute()), ), HomeFeatureTile( title: context.lang.profit_loss, - iconPath: Assets.icons.icReportProfitLoss.path, + iconPath: Assets.icons.icReportProfitLoss.path, onTap: () => context.router.push(FinanceRoute()), + isHighlighted: true, ), HomeFeatureTile( - title: context.lang.exclusive_summary, - iconPath: Assets.icons.icReportExclusiveSummary.path, - onTap: () => context.router.push(ExclusiveSummaryRoute()), + title: context.lang.stock, + iconPath: Assets.icons.icReportStock.path, + onTap: () => context.router.push(InventoryRoute()), + ), + HomeFeatureTile( + title: context.lang.product, + iconPath: Assets.icons.icReportProduct.path, + onTap: () => context.router.push(ProductRoute()), ), - // HomeFeatureTile( - // title: context.lang.inventory, - // iconPath: Assets.icons.icReportProduct.path, - // onTap: () => context.router.push(InventoryRoute()), - // ), ], ), // Row( diff --git a/lib/presentation/pages/home/widgets/feature_tile.dart b/lib/presentation/pages/home/widgets/feature_tile.dart index da66151..58730f5 100644 --- a/lib/presentation/pages/home/widgets/feature_tile.dart +++ b/lib/presentation/pages/home/widgets/feature_tile.dart @@ -7,15 +7,21 @@ class HomeFeatureTile extends StatelessWidget { final String title; final String iconPath; final Function() onTap; + final bool isHighlighted; + const HomeFeatureTile({ super.key, required this.title, required this.iconPath, required this.onTap, + this.isHighlighted = false, }); @override Widget build(BuildContext context) { + final double iconSize = isHighlighted ? 72 : 56; + final double borderRadius = isHighlighted ? 20 : 16; + return Expanded( child: InkWell( onTap: onTap, @@ -26,24 +32,29 @@ class HomeFeatureTile extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Container( - width: 56, - height: 56, + width: iconSize, + height: iconSize, decoration: BoxDecoration( gradient: LinearGradient( - colors: [AppColor.primary.withOpacity(0.1), AppColor.primary.withOpacity(0.05)], + colors: [ + AppColor.primary.withOpacity(0.1), + AppColor.primary.withOpacity(0.05), + ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(borderRadius), ), child: Image.asset(iconPath), ), const SpaceHeight(12), Text( title, - style: AppStyle.sm.copyWith( - fontWeight: FontWeight.w600, - color: AppColor.textPrimary, + style: AppStyle.xs.copyWith( + fontWeight: isHighlighted ? FontWeight.w700 : FontWeight.w600, + color: isHighlighted + ? const Color(0xFF388E3C) + : AppColor.textPrimary, letterSpacing: -0.2, ), textAlign: TextAlign.center, diff --git a/lib/presentation/pages/home/widgets/header.dart b/lib/presentation/pages/home/widgets/header.dart index b7ee1f6..5a6daf3 100644 --- a/lib/presentation/pages/home/widgets/header.dart +++ b/lib/presentation/pages/home/widgets/header.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../application/analytic/exclusive_summary_loader/exclusive_summary_loader_bloc.dart'; import '../../../../application/auth/auth_bloc.dart'; -import '../../../../common/constant/app_constant.dart'; +import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart'; import '../../../../common/extension/extension.dart'; import '../../../../common/painter/wave_painter.dart'; import '../../../../common/theme/theme.dart'; -import '../../../../domain/user/user.dart'; import '../../../components/spacer/spacer.dart'; -import 'omset_balance.dart'; +import 'header_date_filter.dart'; +import 'header_outlet_selector.dart'; +import 'header_summary_slider.dart'; +import 'header_top_bar.dart'; class HomeHeader extends StatefulWidget { final int totalRevenue; @@ -18,12 +21,15 @@ class HomeHeader extends StatefulWidget { State createState() => _HomeHeaderState(); } -class _HomeHeaderState extends State with SingleTickerProviderStateMixin { +class _HomeHeaderState extends State + with SingleTickerProviderStateMixin { late AnimationController _animationController; - late Animation _fadeInAnimation; late Animation _slideAnimation; - late Animation _scaleAnimation; + + /// 0 = Hari Ini, 1 = MTD (Bulan) + int _selectedDateFilter = 0; + bool _isValueVisible = true; @override void initState() { @@ -42,21 +48,37 @@ class _HomeHeaderState extends State with SingleTickerProviderStateM ); _slideAnimation = - Tween(begin: const Offset(0, 0.5), end: Offset.zero).animate( + Tween(begin: const Offset(0, 0.3), end: Offset.zero).animate( CurvedAnimation( parent: _animationController, curve: const Interval(0.2, 0.8, curve: Curves.easeOutCubic), ), ); - _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate( - CurvedAnimation( - parent: _animationController, - curve: const Interval(0.0, 0.7, curve: Curves.elasticOut), - ), - ); - _animationController.forward(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _fetchSummary(); + }); + } + + void _fetchSummary() { + final now = DateTime.now(); + DateTime dateFrom; + DateTime dateTo; + + if (_selectedDateFilter == 0) { + dateFrom = DateTime(now.year, now.month, now.day); + dateTo = DateTime(now.year, now.month, now.day); + } else { + // MTD: tanggal 1 s/d hari ini + dateFrom = DateTime(now.year, now.month, 1); + dateTo = DateTime(now.year, now.month, now.day); + } + + context.read() + ..add(ExclusiveSummaryLoaderEvent.rangeDateChanged(dateFrom, dateTo)) + ..add(const ExclusiveSummaryLoaderEvent.fetched()); } @override @@ -67,279 +89,205 @@ class _HomeHeaderState extends State with SingleTickerProviderStateM @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Container( - height: 280, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppColor.primary, - AppColor.primaryLight, - AppColor.primaryLight.withOpacity(0.8), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - stops: const [0.0, 0.7, 1.0], + return BlocListener( + listenWhen: (prev, curr) => + prev.selectedOutletId != curr.selectedOutletId, + listener: (context, state) => _fetchSummary(), + child: BlocBuilder( + builder: (context, authState) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColor.primary, + AppColor.primary.withOpacity(0.9), + AppColor.primaryLight.withOpacity(0.85), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + stops: const [0.0, 0.7, 1.0], + ), ), - boxShadow: [ - BoxShadow( - color: AppColor.primary.withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: Stack( - children: [ - // Static decorative circles (right side) - Positioned( - top: -50, - right: -50, - child: Container( - width: 150, - height: 150, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AppColor.white.withOpacity(0.10), - ), - ), - ), - Positioned( - top: 80, - right: -20, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AppColor.white.withOpacity(0.05), - ), - ), - ), - Positioned( - top: 150, - right: 30, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AppColor.white.withOpacity(0.07), - ), - ), - ), + child: Stack( + children: [ + // Decorative circles + _buildDecorations(), - // Static decorative circles (left side) - Positioned( - top: 60, - left: -30, - child: Container( - width: 100, - height: 100, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AppColor.white.withOpacity(0.03), - ), - ), - ), - Positioned( - bottom: 20, - left: -20, - child: Container( - width: 60, - height: 60, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AppColor.white.withOpacity(0.04), - ), - ), - ), - - // Static sparkle icons - ...List.generate(8, (index) { - return Positioned( - left: (index * 60.0) % (MediaQuery.of(context).size.width), - top: 30 + (index * 25.0), - child: Icon( - Icons.auto_awesome, - size: 8 + (index % 3) * 3, - color: AppColor.white.withOpacity(0.25), - ), - ); - }), - - // Wave pattern (static) - Positioned.fill( - child: CustomPaint( - painter: WavePainter( - animation: 0.0, - color: AppColor.white.withOpacity(0.08), - ), - ), - ), - - // Gradient overlay for depth - Container( - decoration: BoxDecoration( - gradient: RadialGradient( - center: const Alignment(0.8, -0.3), - radius: 1.5, - colors: [ - Colors.transparent, - AppColor.primary.withOpacity(0.1), - Colors.transparent, - ], - ), - ), - ), - - // Main content - SafeArea(child: _buildContent(context, state.user)), - ], - ), - ); - }, - ); - } - - Widget _buildContent(BuildContext context, User user) { - String greeting(BuildContext context) { - final hour = DateTime.now().hour; - - if (hour >= 4 && hour < 10) { - return context.lang.good_morning; - } else if (hour >= 10 && hour < 15) { - return context.lang.good_afternoon; - } else if (hour >= 15 && hour < 18) { - return context.lang.good_evening; - } else { - return context.lang.good_night; - } - } - - return Padding( - padding: EdgeInsets.all(AppValue.padding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Top bar with enhanced animation - SlideTransition( - position: _slideAnimation, - child: FadeTransition( - opacity: _fadeInAnimation, - child: Transform.scale( - scale: _scaleAnimation.value, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppConstant.appName, - style: AppStyle.lg.copyWith( - color: AppColor.white.withOpacity(0.9), - fontWeight: FontWeight.w600, - letterSpacing: 0.3, - shadows: [ - Shadow( - color: Colors.black.withOpacity(0.2), - offset: const Offset(0, 1), - blurRadius: 2, - ), - ], - ), - ), - const SpaceHeight(2), - Text( - user.role.toTitleCase, - style: AppStyle.sm.copyWith( - color: AppColor.white.withOpacity(0.7), - fontSize: 11, - fontWeight: FontWeight.w400, - ), - ), - ], - ), + // Wave pattern + Positioned.fill( + child: CustomPaint( + painter: WavePainter( + animation: 0.0, + color: AppColor.white.withOpacity(0.08), ), - // Notification icon - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColor.white.withOpacity(0.25), - borderRadius: BorderRadius.circular(14), - border: Border.all( - color: AppColor.white.withOpacity(0.3), - width: 1, + ), + ), + + // Main content + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Top bar + SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _fadeInAnimation, + child: HeaderTopBar(user: authState.user), + ), ), - boxShadow: [ - BoxShadow( - color: AppColor.white.withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 2), + + const SpaceHeight(16), + + // Outlet selector + SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _fadeInAnimation, + child: const HeaderOutletSelector(), ), - ], - ), - child: const Icon( - Icons.notifications_none_rounded, - color: AppColor.white, - size: 20, - ), - ), - ], - ), - ), - ), - ), + ), - const SpaceHeight(24), + const SpaceHeight(12), - // Greeting Section with enhanced animations - SlideTransition( - position: _slideAnimation, - child: FadeTransition( - opacity: _fadeInAnimation, - child: Transform.scale( - scale: _scaleAnimation.value, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${greeting(context)}, ${user.name}! 👋', - style: AppStyle.md.copyWith( - color: AppColor.white, - fontWeight: FontWeight.w500, - shadows: [ - Shadow( - color: Colors.black.withOpacity(0.2), - offset: const Offset(0, 1), - blurRadius: 2, + // Date filter tabs + FadeTransition( + opacity: _fadeInAnimation, + child: HeaderDateFilter( + selectedIndex: _selectedDateFilter, + onChanged: (index) { + setState(() => _selectedDateFilter = index); + _fetchSummary(); + }, ), - ], - ), + ), + + const SpaceHeight(12), + + // Ringkasan label + FadeTransition( + opacity: _fadeInAnimation, + child: _buildRingkasanLabel(), + ), + + const SpaceHeight(12), + + // Sliding summary cards + FadeTransition( + opacity: _fadeInAnimation, + child: HeaderSummarySlider( + isValueVisible: _isValueVisible, + ), + ), + ], ), - ], + ), ), - ), + ], ), - ), - - const SpaceHeight(16), - - - // Today's highlight - FadeTransition( - opacity: _fadeInAnimation, - child: SlideTransition( - position: _slideAnimation, - child: HomeOmsetBalance(totalOmset: widget.totalRevenue, user: user), - ), - ), - ], + ); + }, ), ); } + + Widget _buildDecorations() { + return Stack( + children: [ + Positioned( + top: -50, + right: -50, + child: Container( + width: 150, + height: 150, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.white.withOpacity(0.10), + ), + ), + ), + Positioned( + top: 80, + right: -20, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.white.withOpacity(0.05), + ), + ), + ), + Positioned( + top: 60, + left: -30, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.white.withOpacity(0.03), + ), + ), + ), + ], + ); + } + + Widget _buildRingkasanLabel() { + final now = DateTime.now(); + final dateLabel = _selectedDateFilter == 0 + ? '${context.lang.summary_today} · ${now.day} ${_monthName(now.month)} ${now.year}' + : '${context.lang.summary_mtd} · 1 - ${now.day} ${_monthName(now.month)} ${now.year}'; + + return Row( + children: [ + Expanded( + child: Text( + dateLabel, + style: AppStyle.sm.copyWith( + color: AppColor.white.withOpacity(0.9), + fontWeight: FontWeight.w600, + ), + ), + ), + GestureDetector( + onTap: () { + setState(() => _isValueVisible = !_isValueVisible); + }, + child: Icon( + _isValueVisible + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + color: AppColor.white.withOpacity(0.7), + size: 20, + ), + ), + ], + ); + } + + String _monthName(int month) { + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'Mei', + 'Jun', + 'Jul', + 'Agu', + 'Sep', + 'Okt', + 'Nov', + 'Des', + ]; + return months[month - 1]; + } } diff --git a/lib/presentation/pages/home/widgets/header_date_filter.dart b/lib/presentation/pages/home/widgets/header_date_filter.dart new file mode 100644 index 0000000..97cfc8e --- /dev/null +++ b/lib/presentation/pages/home/widgets/header_date_filter.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; + +class HeaderDateFilter extends StatelessWidget { + final int selectedIndex; + final ValueChanged onChanged; + + const HeaderDateFilter({ + super.key, + required this.selectedIndex, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + _buildTabItem('Hari Ini', 0), + _buildTabItem('MTD (Bulan)', 1), + ], + ), + ); + } + + Widget _buildTabItem(String label, int index) { + final isSelected = selectedIndex == index; + return Expanded( + child: GestureDetector( + onTap: () { + if (selectedIndex != index) { + onChanged(index); + } + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: isSelected ? AppColor.white : Colors.transparent, + borderRadius: BorderRadius.circular(10), + ), + alignment: Alignment.center, + child: Text( + label, + style: AppStyle.sm.copyWith( + color: isSelected ? AppColor.primary : AppColor.white, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/header_outlet_bottom_sheet.dart b/lib/presentation/pages/home/widgets/header_outlet_bottom_sheet.dart new file mode 100644 index 0000000..b8141a4 --- /dev/null +++ b/lib/presentation/pages/home/widgets/header_outlet_bottom_sheet.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart'; +import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class HeaderOutletBottomSheet extends StatelessWidget { + const HeaderOutletBottomSheet({super.key}); + + static void show(BuildContext context) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider.value(value: context.read()), + BlocProvider.value(value: context.read()), + ], + child: const HeaderOutletBottomSheet(), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.6, + ), + decoration: const BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Handle bar + const SizedBox(height: 12), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppColor.border, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 16), + + // Title + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + Text( + 'Pilih Outlet', + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + ), + ), + const Spacer(), + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Icon( + Icons.close_rounded, + color: AppColor.textSecondary, + size: 24, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // List + Flexible( + child: BlocBuilder( + builder: (context, outletListState) { + if (outletListState.isFetching && + outletListState.outlets.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(24), + child: CircularProgressIndicator(), + ), + ); + } + + return BlocBuilder( + builder: (context, selectedState) { + return ListView( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 20), + children: [ + // Semua Outlet + _OutletTile( + title: 'Semua Outlet', + subtitle: '${outletListState.outlets.length} outlet', + icon: Icons.store_rounded, + isSelected: selectedState.isAllOutlets, + onTap: () { + context.read().add( + const SelectedOutletEvent.cleared(), + ); + Navigator.pop(context); + }, + ), + + const SizedBox(height: 8), + + // Individual outlets + ...outletListState.outlets.map((outlet) { + final isSelected = + selectedState.selectedOutletId == outlet.id; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _OutletTile( + title: outlet.name, + subtitle: outlet.isActive + ? 'Aktif' + : 'Tidak aktif', + icon: Icons.storefront_rounded, + isSelected: isSelected, + isActive: outlet.isActive, + onTap: () { + context.read().add( + SelectedOutletEvent.selected(outlet), + ); + Navigator.pop(context); + }, + ), + ); + }), + + const SizedBox(height: 16), + ], + ); + }, + ); + }, + ), + ), + ], + ), + ); + } +} + +class _OutletTile extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + final bool isSelected; + final bool isActive; + final VoidCallback onTap; + + const _OutletTile({ + required this.title, + required this.subtitle, + required this.icon, + required this.isSelected, + this.isActive = true, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: isSelected + ? AppColor.primary.withOpacity(0.08) + : AppColor.background, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? AppColor.primary : AppColor.border, + width: isSelected ? 1.5 : 1, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: isSelected + ? AppColor.primary.withOpacity(0.15) + : AppColor.border.withOpacity(0.5), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + icon, + color: isSelected ? AppColor.primary : AppColor.textSecondary, + size: 20, + ), + ), + const SpaceWidth(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + subtitle, + style: AppStyle.xs.copyWith(color: AppColor.textSecondary), + ), + ], + ), + ), + if (isSelected) + Icon( + Icons.check_circle_rounded, + color: AppColor.primary, + size: 22, + ), + if (!isSelected && !isActive) + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: AppColor.error.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + 'Off', + style: AppStyle.xs.copyWith( + color: AppColor.error, + fontWeight: FontWeight.w600, + fontSize: 10, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/header_outlet_selector.dart b/lib/presentation/pages/home/widgets/header_outlet_selector.dart new file mode 100644 index 0000000..590d45b --- /dev/null +++ b/lib/presentation/pages/home/widgets/header_outlet_selector.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; +import 'header_outlet_bottom_sheet.dart'; + +class HeaderOutletSelector extends StatelessWidget { + const HeaderOutletSelector({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return GestureDetector( + onTap: () => HeaderOutletBottomSheet.show(context), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColor.white.withOpacity(0.3)), + ), + child: Row( + children: [ + Icon( + Icons.location_on_outlined, + color: AppColor.white, + size: 18, + ), + const SpaceWidth(8), + Expanded( + child: Text( + state.displayName, + style: AppStyle.md.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Icon( + Icons.keyboard_arrow_down_rounded, + color: AppColor.white, + size: 20, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/presentation/pages/home/widgets/header_summary_card.dart b/lib/presentation/pages/home/widgets/header_summary_card.dart new file mode 100644 index 0000000..aa8f8c7 --- /dev/null +++ b/lib/presentation/pages/home/widgets/header_summary_card.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/extension/extension.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../../domain/analytic/analytic.dart'; +import '../../../components/spacer/spacer.dart'; + +class HeaderSummaryCard extends StatelessWidget { + final IconData icon; + final Color iconColor; + final String title; + final int value; + final String subtitle; + final List dailyData; + final double? percentage; + final bool isValueVisible; + final VoidCallback? onTap; + + const HeaderSummaryCard({ + super.key, + required this.icon, + required this.iconColor, + required this.title, + required this.value, + required this.subtitle, + required this.dailyData, + this.percentage, + this.isValueVisible = true, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColor.white.withOpacity(0.2)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top: Icon + Title + Percentage + Chevron + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: iconColor, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: Colors.white, size: 20), + ), + const SpaceWidth(10), + Text( + title, + style: AppStyle.md.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + if (percentage != null) _buildPercentageBadge(), + const SpaceWidth(6), + Icon( + Icons.chevron_right_rounded, + color: AppColor.white.withOpacity(0.7), + size: 20, + ), + ], + ), + + const SpaceHeight(12), + + // Value (hidden or visible) + isValueVisible + ? Text( + value.currencyFormatRp, + style: AppStyle.h1.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w900, + fontSize: 26, + ), + ) + : Text( + 'Rp ••••••', + style: AppStyle.h1.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w900, + fontSize: 26, + letterSpacing: 2, + ), + ), + + const SpaceHeight(4), + + // Subtitle + Text( + subtitle, + style: AppStyle.xs.copyWith( + color: AppColor.white.withOpacity(0.7), + fontWeight: FontWeight.w400, + fontStyle: FontStyle.italic, + ), + ), + + const Spacer(), + + // Mini bar chart + _buildMiniBarChart(), + ], + ), + ), + ); + } + + Widget _buildPercentageBadge() { + final isPositive = (percentage ?? 0) >= 0; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isPositive + ? const Color(0xFF4CAF50).withOpacity(0.25) + : const Color(0xFFE53E3E).withOpacity(0.25), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isPositive + ? Icons.trending_up_rounded + : Icons.trending_down_rounded, + color: isPositive + ? const Color(0xFF4CAF50) + : const Color(0xFFE53E3E), + size: 12, + ), + const SizedBox(width: 3), + Text( + '${percentage!.toStringAsFixed(1)}%', + style: AppStyle.xs.copyWith( + color: isPositive + ? const Color(0xFF4CAF50) + : const Color(0xFFE53E3E), + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ); + } + + Widget _buildMiniBarChart() { + if (dailyData.isEmpty) return const SizedBox.shrink(); + + final maxVal = dailyData + .map((d) => d.totalCost) + .fold(0, (a, b) => a > b ? a : b); + + return SizedBox( + height: 24, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: dailyData.map((d) { + final ratio = maxVal > 0 ? d.totalCost / maxVal : 0.0; + return Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 1.5), + height: 6 + (18 * ratio), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.4), + borderRadius: BorderRadius.circular(3), + ), + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/header_summary_slider.dart b/lib/presentation/pages/home/widgets/header_summary_slider.dart new file mode 100644 index 0000000..198db2c --- /dev/null +++ b/lib/presentation/pages/home/widgets/header_summary_slider.dart @@ -0,0 +1,146 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../application/analytic/exclusive_summary_loader/exclusive_summary_loader_bloc.dart'; +import '../../../../common/extension/extension.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../router/app_router.gr.dart'; +import 'header_summary_card.dart'; + +class HeaderSummarySlider extends StatefulWidget { + final bool isValueVisible; + + const HeaderSummarySlider({super.key, this.isValueVisible = true}); + + @override + State createState() => _HeaderSummarySliderState(); +} + +class _HeaderSummarySliderState extends State { + final PageController _pageController = PageController(viewportFraction: 0.92); + int _currentPage = 0; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final summary = state.exclusiveSummary.summary; + final dailySummary = state.exclusiveSummary.dailySummary; + + return Column( + children: [ + SizedBox( + height: 170, + child: state.isFetching + ? _buildShimmer() + : PageView( + controller: _pageController, + onPageChanged: (index) { + setState(() => _currentPage = index); + }, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: HeaderSummaryCard( + icon: Icons.credit_card_rounded, + iconColor: const Color(0xFFB71C1C), + title: context.lang.sales, + value: summary.sales, + subtitle: context.lang.compared_to_previous_period, + dailyData: dailySummary, + isValueVisible: widget.isValueVisible, + percentage: summary.sales > 0 ? 12.5 : null, + onTap: () => + context.router.push(const SalesRoute()), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: HeaderSummaryCard( + icon: Icons.shopping_cart_outlined, + iconColor: const Color(0xFF00BCD4), + title: context.lang.purchase, + value: summary.hpp, + subtitle: context.lang.compared_to_previous_period, + dailyData: dailySummary, + isValueVisible: widget.isValueVisible, + percentage: summary.sales > 0 + ? (summary.hpp / summary.sales * 100) + : null, + onTap: () => + context.router.push(const PurchaseRoute()), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: HeaderSummaryCard( + icon: Icons.account_balance_wallet_outlined, + iconColor: const Color(0xFFFF9800), + title: context.lang.profit_loss, + value: summary.netProfit, + subtitle: context.lang.compared_to_previous_period, + dailyData: dailySummary, + isValueVisible: widget.isValueVisible, + percentage: summary.sales > 0 + ? (summary.netProfit / summary.sales * 100) + : null, + onTap: () => + context.router.push(const FinanceRoute()), + ), + ), + ], + ), + ), + + const SizedBox(height: 8), + + // Page indicator + _buildPageIndicator(), + ], + ); + }, + ); + } + + Widget _buildShimmer() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: CircularProgressIndicator( + color: AppColor.white.withOpacity(0.5), + strokeWidth: 2, + ), + ), + ); + } + + Widget _buildPageIndicator() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(3, (index) { + final isActive = _currentPage == index; + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.symmetric(horizontal: 3), + width: isActive ? 20 : 6, + height: 6, + decoration: BoxDecoration( + color: isActive ? AppColor.white : AppColor.white.withOpacity(0.4), + borderRadius: BorderRadius.circular(3), + ), + ); + }), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/header_top_bar.dart b/lib/presentation/pages/home/widgets/header_top_bar.dart new file mode 100644 index 0000000..41efaa6 --- /dev/null +++ b/lib/presentation/pages/home/widgets/header_top_bar.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/extension/extension.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../../domain/user/user.dart'; +import '../../../components/assets/assets.gen.dart'; +import '../../../components/spacer/spacer.dart'; + +class HeaderTopBar extends StatelessWidget { + final User user; + const HeaderTopBar({super.key, required this.user}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + // Logo + Greeting + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Assets.images.logo.image(width: 64), + Text( + '${_greeting(context)}, ${user.name}', + style: AppStyle.md.copyWith( + color: AppColor.white.withOpacity(0.9), + fontWeight: FontWeight.w700, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ), + ), + + // Notification + GestureDetector( + onTap: () {}, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.notifications_none_rounded, + color: AppColor.white, + size: 20, + ), + ), + ), + const SpaceWidth(8), + + // Avatar + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(12), + ), + alignment: Alignment.center, + child: Text( + _getInitials(user.name), + style: AppStyle.sm.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ); + } + + String _greeting(BuildContext context) { + final hour = DateTime.now().hour; + if (hour >= 4 && hour < 10) return context.lang.good_morning; + if (hour >= 10 && hour < 15) return context.lang.good_afternoon; + if (hour >= 15 && hour < 18) return context.lang.good_evening; + return context.lang.good_night; + } + + String _getInitials(String name) { + final parts = name.trim().split(' '); + if (parts.length >= 2) { + return '${parts[0][0]}${parts[1][0]}'.toUpperCase(); + } + return parts[0].isNotEmpty ? parts[0][0].toUpperCase() : ''; + } +} diff --git a/lib/presentation/pages/home/widgets/home_top_products.dart b/lib/presentation/pages/home/widgets/home_top_products.dart new file mode 100644 index 0000000..e27cf1a --- /dev/null +++ b/lib/presentation/pages/home/widgets/home_top_products.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/extension/extension.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../../domain/analytic/analytic.dart'; +import '../../../components/spacer/spacer.dart'; + +class HomeTopProducts extends StatelessWidget { + final List products; + const HomeTopProducts({super.key, required this.products}); + + // Colors for product icon backgrounds + static const _iconBgColors = [ + Color(0xFFB9F6CA), // green light + Color(0xFFB3E5FC), // blue light + Color(0xFFFFF9C4), // yellow light + Color(0xFFFFCDD2), // red light + Color(0xFFE1BEE7), // purple light + ]; + + @override + Widget build(BuildContext context) { + if (products.isEmpty) return const SizedBox.shrink(); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppValue.padding, + vertical: 24, + ).copyWith(bottom: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.lang.best_selling_products, + style: AppStyle.xl.copyWith( + fontWeight: FontWeight.w800, + color: AppColor.textPrimary, + ), + ), + const SpaceHeight(16), + + // Product list card + Container( + 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: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: products.length > 5 ? 5 : products.length, + separatorBuilder: (_, __) => Divider( + height: 1, + color: AppColor.border.withOpacity(0.4), + indent: 76, + endIndent: 16, + ), + itemBuilder: (context, index) { + return _ProductTile( + product: products[index], + bgColor: _iconBgColors[index % _iconBgColors.length], + ); + }, + ), + ), + ], + ), + ); + } +} + +class _ProductTile extends StatelessWidget { + final DashboardTopProduct product; + final Color bgColor; + + const _ProductTile({required this.product, required this.bgColor}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + // Product icon placeholder + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(14), + ), + child: Center( + child: Text( + product.productName.isNotEmpty + ? product.productName[0].toUpperCase() + : '?', + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + ), + ), + ), + ), + const SpaceWidth(12), + + // Name + qty + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.productName, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + context.lang.portion_sold(product.quantitySold), + style: AppStyle.xs.copyWith(color: AppColor.textSecondary), + ), + ], + ), + ), + const SpaceWidth(8), + + // Revenue + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + product.revenue.currencyFormatRp, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/home_warnings.dart b/lib/presentation/pages/home/widgets/home_warnings.dart new file mode 100644 index 0000000..5e7cbad --- /dev/null +++ b/lib/presentation/pages/home/widgets/home_warnings.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/extension/extension.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class HomeWarnings extends StatelessWidget { + const HomeWarnings({super.key}); + + @override + Widget build(BuildContext context) { + // TODO: Integrate with actual warning data from backend + final warnings = <_WarningItem>[]; + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppValue.padding, + vertical: 24, + ).copyWith(bottom: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Text( + context.lang.warning_title, + style: AppStyle.xl.copyWith( + fontWeight: FontWeight.w800, + color: AppColor.textPrimary, + ), + ), + const SpaceWidth(8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: AppColor.error, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${warnings.length}', + style: AppStyle.xs.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + const SpaceHeight(4), + Text( + context.lang.warning_desc, + style: AppStyle.sm.copyWith(color: AppColor.textSecondary), + ), + const SpaceHeight(16), + + // Warning list / empty state + Container( + width: double.infinity, + 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: warnings.isEmpty + ? _buildEmptyState(context) + : ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: warnings.length, + separatorBuilder: (_, __) => Divider( + height: 1, + color: AppColor.border.withOpacity(0.5), + indent: 72, + ), + itemBuilder: (context, index) { + final item = warnings[index]; + return _WarningTile(item: item); + }, + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16), + child: Column( + children: [ + Icon( + Icons.check_circle_outline_rounded, + color: AppColor.success.withOpacity(0.6), + size: 48, + ), + const SpaceHeight(12), + Text( + context.lang.no_warning, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + ), + const SpaceHeight(4), + Text( + context.lang.no_warning_desc, + style: AppStyle.sm.copyWith(color: AppColor.textSecondary), + ), + ], + ), + ); + } +} + +class _WarningTile extends StatelessWidget { + final _WarningItem item; + const _WarningTile({required this.item}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () {}, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + // Warning icon + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: item.severity == _WarningSeverity.tinggi + ? AppColor.error.withOpacity(0.1) + : AppColor.warning.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.warning_amber_rounded, + color: item.severity == _WarningSeverity.tinggi + ? AppColor.error + : AppColor.warning, + size: 22, + ), + ), + const SpaceWidth(12), + + // Title + subtitle + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + item.subtitle, + style: AppStyle.xs.copyWith(color: AppColor.textSecondary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SpaceWidth(8), + + // Severity badge + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: item.severity == _WarningSeverity.tinggi + ? AppColor.error.withOpacity(0.1) + : AppColor.warning.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + item.severity == _WarningSeverity.tinggi + ? context.lang.severity_high + : context.lang.severity_medium, + style: AppStyle.xs.copyWith( + color: item.severity == _WarningSeverity.tinggi + ? AppColor.error + : AppColor.warning, + fontWeight: FontWeight.w700, + ), + ), + ), + const SpaceWidth(4), + + // Chevron + Icon( + Icons.chevron_right_rounded, + color: AppColor.textSecondary.withOpacity(0.5), + size: 20, + ), + ], + ), + ), + ); + } +} + +// ignore: unused_element +enum _WarningSeverity { tinggi } + +// ignore: unused_element +class _WarningItem { + final String title; + final String subtitle; + final _WarningSeverity severity; + + const _WarningItem({ + required this.title, + required this.subtitle, + required this.severity, + }); +} diff --git a/lib/presentation/pages/home/widgets/omset_balance.dart b/lib/presentation/pages/home/widgets/omset_balance.dart deleted file mode 100644 index c760e81..0000000 --- a/lib/presentation/pages/home/widgets/omset_balance.dart +++ /dev/null @@ -1,233 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:line_icons/line_icon.dart'; -import 'package:line_icons/line_icons.dart'; - -import '../../../../../../common/theme/theme.dart'; -import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart'; -import '../../../../common/extension/extension.dart'; -import '../../../../domain/user/user.dart'; -import '../../../components/spacer/spacer.dart'; - -class HomeOmsetBalance extends StatefulWidget { - final int totalOmset; - final User user; - const HomeOmsetBalance({super.key, required this.totalOmset, required this.user}); - - @override - State createState() => _HomeOmsetBalanceState(); -} - -class _HomeOmsetBalanceState extends State { - late DateTime _now; - late Timer _timer; - bool _isBalanceVisible = true; - - @override - void initState() { - super.initState(); - _now = DateTime.now(); - _timer = Timer.periodic(const Duration(seconds: 1), (_) { - setState(() => _now = DateTime.now()); - }); - } - - @override - void dispose() { - _timer.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - clipBehavior: Clip.none, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColor.white.withOpacity(0.3), width: 1), - boxShadow: [ - BoxShadow( - color: AppColor.white.withOpacity(0.1), - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: [_top(context), _middle(context), _bottom(context)], - ), - ); - } - - Widget _bottom(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () {}, - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: AppColor.white, - border: Border(top: BorderSide(color: AppColor.border)), - borderRadius: const BorderRadius.vertical( - bottom: Radius.circular(16), - ), - ), - child: Row( - children: [ - LineIcon(LineIcons.calendar, color: AppColor.black, size: 14), - SpaceWidth(6), - Expanded( - child: Text( - _now.toDate, - style: AppStyle.md.copyWith( - color: AppColor.black, - fontWeight: FontWeight.w600, - letterSpacing: 0.3, - ), - ), - ), - LineIcon(LineIcons.clock, color: AppColor.textSecondary, size: 14), - SpaceWidth(4), - Text( - _now.toHourMinuteSecond, - style: AppStyle.md.copyWith( - color: AppColor.textSecondary, - fontWeight: FontWeight.w500, - fontFeatures: [const FontFeature.tabularFigures()], - ), - ), - ], - ), - ), - ); - } - - Container _middle(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration(color: AppColor.white), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () {}, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.lang.sales_today, - style: AppStyle.sm.copyWith( - color: AppColor.black, - fontWeight: FontWeight.w400, - letterSpacing: 0.3, - ), - ), - SpaceHeight(2), - AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: (child, animation) { - return FadeTransition( - opacity: animation, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 0.3), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.easeOutCubic, - )), - child: child, - ), - ); - }, - child: _isBalanceVisible - ? Text( - widget.totalOmset.currencyFormatRp, - key: const ValueKey('visible'), - style: AppStyle.xxl.copyWith( - color: AppColor.black, - fontWeight: FontWeight.w900, - letterSpacing: 0.3, - ), - ) - : Text( - 'Rp ••••••••', - key: const ValueKey('hidden'), - style: AppStyle.xl.copyWith( - color: AppColor.black, - fontWeight: FontWeight.w900, - letterSpacing: 2, - ), - ), - ), - ], - ), - ), - ), - GestureDetector( - onTap: () => setState(() => _isBalanceVisible = !_isBalanceVisible), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: LineIcon( - _isBalanceVisible ? LineIcons.eye : LineIcons.eyeSlash, - key: ValueKey(_isBalanceVisible), - color: AppColor.primary, - size: 16, - ), - ), - ), - ], - ), - ); - } - - GestureDetector _top(BuildContext context) { - return GestureDetector( - onTap: () {}, - child: BlocBuilder( - builder: (context, state) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - gradient: LinearGradient(colors: AppColor.primaryGradient), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(16), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - state.displayName, - style: AppStyle.sm.copyWith( - color: AppColor.white, - fontWeight: FontWeight.w600, - letterSpacing: 0.3, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - SpaceWidth(6), - LineIcon( - LineIcons.alternateExchange, - color: AppColor.white, - size: 14, - ), - ], - ), - ); - }, - ), - ); - } -} \ No newline at end of file diff --git a/lib/presentation/pages/home/widgets/promo_banner.dart b/lib/presentation/pages/home/widgets/promo_banner.dart deleted file mode 100644 index c302732..0000000 --- a/lib/presentation/pages/home/widgets/promo_banner.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart'; -import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart'; -import '../../../../common/theme/theme.dart'; -import '../../../../domain/outlet/outlet.dart'; -import '../../../components/spacer/spacer.dart'; -import '../../../components/widgets/particle_card.dart'; - -class HomePromoBanner extends StatelessWidget { - const HomePromoBanner({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, outletListState) { - if (outletListState.isFetching && outletListState.outlets.isEmpty) { - return const _PromoBannerSkeleton(); - } - - if (outletListState.outlets.isEmpty) { - return const SizedBox.shrink(); - } - - return BlocBuilder( - builder: (context, selectedState) { - return Padding( - padding: const EdgeInsets.fromLTRB( - AppValue.padding, - 24, - AppValue.padding, - 0, - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - // Card "Semua Outlet" di posisi pertama - _AllOutletCard( - isSelected: selectedState.isAllOutlets, - onTap: () { - if (!selectedState.isAllOutlets) { - context - .read() - .add(const SelectedOutletEvent.cleared()); - } - }, - ), - const SpaceWidth(12), - for (int i = 0; i < outletListState.outlets.length; i++) ...[ - _OutletCard( - outlet: outletListState.outlets[i], - isSelected: selectedState.selectedOutletId == - outletListState.outlets[i].id, - onTap: () { - final tapped = outletListState.outlets[i]; - if (selectedState.selectedOutletId == tapped.id) { - // Tap outlet yang sama → deselect (Semua Outlet) - context - .read() - .add(const SelectedOutletEvent.cleared()); - } else { - context - .read() - .add(SelectedOutletEvent.selected(tapped)); - } - }, - ), - if (i < outletListState.outlets.length - 1) - const SpaceWidth(12), - ], - ], - ), - ), - ); - }, - ); - }, - ); - } -} - -class _AllOutletCard extends StatelessWidget { - final bool isSelected; - final VoidCallback onTap; - - const _AllOutletCard({ - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 130, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isSelected ? AppColor.white : Colors.transparent, - width: 2, - ), - boxShadow: isSelected - ? [ - BoxShadow( - color: AppColor.white.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ] - : [], - ), - child: Opacity( - opacity: isSelected ? 1.0 : 0.55, - child: ParticleCard( - height: 110, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - decorationOpacity: 0.8, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - const Spacer(), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: AppColor.white.withOpacity(0.25), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.store_rounded, - color: AppColor.white, - size: 12, - ), - const SpaceWidth(4), - Text( - 'Semua', - style: AppStyle.xs.copyWith( - color: AppColor.white, - fontWeight: FontWeight.w700, - fontSize: 9, - ), - ), - ], - ), - ), - ], - ), - const SpaceHeight(6), - Text( - 'Semua Outlet', - style: AppStyle.sm.copyWith( - color: AppColor.white, - fontWeight: FontWeight.w800, - height: 1.25, - ), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ), - ), - ); - } -} - -class _OutletCard extends StatelessWidget { - final Outlet outlet; - final bool isSelected; - final VoidCallback onTap; - - const _OutletCard({ - required this.outlet, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 130, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isSelected ? AppColor.white : Colors.transparent, - width: 2, - ), - boxShadow: isSelected - ? [ - BoxShadow( - color: AppColor.white.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ] - : [], - ), - child: Opacity( - opacity: isSelected ? 1.0 : 0.55, - child: ParticleCard( - height: 110, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - decorationOpacity: 0.8, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - const Spacer(), - _buildStatusBadge(outlet.isActive), - ], - ), - const SpaceHeight(6), - Text( - outlet.name, - style: AppStyle.sm.copyWith( - color: AppColor.white, - fontWeight: FontWeight.w800, - height: 1.25, - ), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildStatusBadge(bool isActive) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: isActive - ? AppColor.success.withOpacity(0.9) - : AppColor.error.withOpacity(0.9), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: (isActive ? AppColor.success : AppColor.error) - .withOpacity(0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - isActive ? Icons.check_circle : Icons.warning_rounded, - color: AppColor.white, - size: 12, - ), - const SpaceWidth(4), - Text( - isActive ? 'Sehat' : 'Tidak Sehat', - style: AppStyle.xs.copyWith( - color: AppColor.white, - fontWeight: FontWeight.w700, - fontSize: 9, - ), - ), - ], - ), - ); - } -} - -class _PromoBannerSkeleton extends StatelessWidget { - const _PromoBannerSkeleton(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB( - AppValue.padding, - 24, - AppValue.padding, - 0, - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - _skeletonCard(), - const SpaceWidth(12), - _skeletonCard(), - const SpaceWidth(12), - _skeletonCard(), - ], - ), - ), - ); - } - - Widget _skeletonCard() { - return Container( - width: 130, - height: 110, - decoration: BoxDecoration( - color: AppColor.border, - borderRadius: BorderRadius.circular(12), - ), - ); - } -} diff --git a/lib/presentation/pages/home/widgets/stats.dart b/lib/presentation/pages/home/widgets/stats.dart index 1b91665..b63b0a1 100644 --- a/lib/presentation/pages/home/widgets/stats.dart +++ b/lib/presentation/pages/home/widgets/stats.dart @@ -1,12 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:line_icons/line_icons.dart'; import '../../../../common/extension/extension.dart'; import '../../../../common/theme/theme.dart'; import '../../../../domain/analytic/analytic.dart'; import '../../../components/spacer/spacer.dart'; -import 'stats_tile.dart'; -import 'title.dart'; class HomeStats extends StatelessWidget { final DashboardOverview overview; @@ -22,54 +19,57 @@ class HomeStats extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - HomeTitle(title: context.lang.today_summary), - const SpaceHeight(20), + Text( + context.lang.today_condition, + style: AppStyle.xl.copyWith( + fontWeight: FontWeight.w800, + color: AppColor.textPrimary, + ), + ), + const SpaceHeight(16), Row( children: [ Expanded( - child: HomeStatsTile( - title: context.lang.order, + child: _StatCard( + icon: Icons.credit_card_rounded, + iconColor: const Color(0xFF00BCD4), + blobColor: const Color(0xFF00BCD4), value: overview.totalOrders.toString(), - icon: Icons.receipt_long_rounded, - color: AppColor.info, - subtitle: context.lang.today, + label: context.lang.transaction, ), ), - - const SpaceWidth(16), + const SpaceWidth(12), Expanded( - child: HomeStatsTile( - title: context.lang.new_customer, - value: overview.totalCustomers.toString(), - icon: Icons.person_add_outlined, - color: AppColor.primary, - subtitle: overview.totalCustomers < 1 - ? context.lang.today - : context.lang.increase, + child: _StatCard( + icon: Icons.hexagon_outlined, + iconColor: const Color(0xFF4CAF50), + blobColor: const Color(0xFF4CAF50), + value: '0', // TODO: connect items sold data + label: context.lang.items_sold, ), ), ], ), - const SizedBox(height: 16), + const SpaceHeight(12), Row( children: [ Expanded( - child: HomeStatsTile( - title: context.lang.refund, - value: overview.refundedOrders.toString(), - icon: LineIcons.alternateExchange, - color: AppColor.warning, - subtitle: context.lang.today, + child: _StatCard( + icon: Icons.warning_amber_rounded, + iconColor: const Color(0xFFFF9800), + blobColor: const Color(0xFFFF9800), + value: '0', // TODO: connect low stock data + label: context.lang.low_stock_warning, ), ), - const SpaceWidth(16), + const SpaceWidth(12), Expanded( - child: HomeStatsTile( - title: context.lang.void_text, - value: overview.voidedOrders.toString(), - icon: Icons.cancel_rounded, - color: AppColor.error, - subtitle: context.lang.today, + child: _StatCard( + icon: Icons.hexagon_outlined, + iconColor: const Color(0xFFE53935), + blobColor: const Color(0xFFE53935), + value: '0', // TODO: connect active products data + label: context.lang.active_products, ), ), ], @@ -79,3 +79,97 @@ class HomeStats extends StatelessWidget { ); } } + +class _StatCard extends StatelessWidget { + final IconData icon; + final Color iconColor; + final Color blobColor; + final String value; + final String label; + + const _StatCard({ + required this.icon, + required this.iconColor, + required this.blobColor, + required this.value, + required this.label, + }); + + @override + Widget build(BuildContext context) { + return Container( + clipBehavior: Clip.hardEdge, + 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: Padding( + padding: const EdgeInsets.all(16), + child: Stack( + clipBehavior: Clip.none, + children: [ + // Decorative blob top-right (quarter circle) + Positioned( + top: -16, + right: -16, + child: Container( + width: 70, + height: 70, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(70), + ), + color: blobColor.withOpacity(0.10), + ), + ), + ), + + // Content + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Icon + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: iconColor, + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: Colors.white, size: 20), + ), + const SpaceHeight(16), + + // Value + Text( + value, + style: AppStyle.h1.copyWith( + fontWeight: FontWeight.w900, + color: AppColor.textPrimary, + fontSize: 28, + ), + ), + const SpaceHeight(4), + + // Label + Text( + label, + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/top_product.dart b/lib/presentation/pages/home/widgets/top_product.dart deleted file mode 100644 index b95b8a1..0000000 --- a/lib/presentation/pages/home/widgets/top_product.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../../common/extension/extension.dart'; -import '../../../../common/theme/theme.dart'; -import '../../../../domain/analytic/analytic.dart'; -import '../../../components/spacer/spacer.dart'; -import 'title.dart'; -import 'top_product_tile.dart'; - -class HomeTopProduct extends StatelessWidget { - final List products; - const HomeTopProduct({super.key, required this.products}); - - @override - Widget build(BuildContext context) { - if (products.isEmpty) { - return const SizedBox.shrink(); - } - - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 24, - horizontal: AppValue.padding, - ).copyWith(bottom: 0), - child: Column( - children: [ - HomeTitle(title: context.lang.today_top_product), - SpaceHeight(20), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: products.length, - padding: EdgeInsets.zero, - itemBuilder: (context, index) { - return HomeTopProductTile( - product: products[index], - ranking: index + 1, - ); - }, - ), - ], - ), - ); - } -} diff --git a/lib/presentation/pages/home/widgets/top_product_tile.dart b/lib/presentation/pages/home/widgets/top_product_tile.dart deleted file mode 100644 index 108163f..0000000 --- a/lib/presentation/pages/home/widgets/top_product_tile.dart +++ /dev/null @@ -1,287 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../../common/extension/extension.dart'; -import '../../../../common/theme/theme.dart'; -import '../../../../domain/analytic/analytic.dart'; - -class HomeTopProductTile extends StatelessWidget { - final DashboardTopProduct product; - final int ranking; - final VoidCallback? onTap; - - const HomeTopProductTile({ - super.key, - required this.product, - required this.ranking, - this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - child: Material( - elevation: 2, - borderRadius: BorderRadius.circular(16), - shadowColor: AppColor.primary.withOpacity(0.1), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(16), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [AppColor.white, AppColor.backgroundLight], - ), - border: Border.all(color: AppColor.borderLight, width: 1), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header Row - Ranking dan Revenue - Row( - children: [ - _buildRankingBadge(context), - const Spacer(), - _buildRevenueDisplay(), - ], - ), - - const SizedBox(height: 12), - - // Product Name - Text( - product.productName, - style: AppStyle.lg.copyWith( - fontWeight: FontWeight.w600, - color: AppColor.textPrimary, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - - const SizedBox(height: 8), - - // Category - _buildCategoryChip(), - - const SizedBox(height: 12), - - // Metrics dalam Grid 2x2 - _buildMetricsGrid(context), - ], - ), - ), - ), - ), - ); - } - - Widget _buildRankingBadge(BuildContext context) { - Color badgeColor; - IconData icon; - - switch (ranking) { - case 1: - badgeColor = const Color(0xFFFFD700); // Gold - icon = Icons.emoji_events; - break; - case 2: - badgeColor = const Color(0xFFC0C0C0); // Silver - icon = Icons.emoji_events; - break; - case 3: - badgeColor = const Color(0xFFCD7F32); // Bronze - icon = Icons.emoji_events; - break; - default: - badgeColor = AppColor.primary; - icon = Icons.star; - } - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: badgeColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: badgeColor.withOpacity(0.3), width: 1.5), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, color: badgeColor, size: 16), - const SizedBox(width: 6), - Text( - '${context.lang.rank} #$ranking', - style: AppStyle.sm.copyWith( - color: badgeColor, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - } - - Widget _buildCategoryChip() { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: AppColor.secondary.withOpacity(0.1), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: AppColor.secondary.withOpacity(0.3), - width: 1, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.category_outlined, size: 14, color: AppColor.secondary), - const SizedBox(width: 6), - Flexible( - child: Text( - product.categoryName, - style: AppStyle.sm.copyWith( - color: AppColor.secondary, - fontWeight: FontWeight.w500, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - } - - Widget _buildMetricsGrid(BuildContext context) { - return Row( - children: [ - Expanded( - child: Column( - children: [ - _buildMetricCard( - icon: Icons.shopping_cart_outlined, - label: context.lang.quantity_sold, - value: product.quantitySold.toString(), - color: AppColor.info, - ), - const SizedBox(height: 8), - _buildMetricCard( - icon: Icons.attach_money, - label: context.lang.average_price, - value: product.averagePrice.round().currencyFormatRp, - color: AppColor.success, - ), - ], - ), - ), - const SizedBox(width: 8), - Expanded( - child: Column( - children: [ - _buildMetricCard( - icon: Icons.receipt_outlined, - label: context.lang.total_orders, - value: product.orderCount.toString(), - color: AppColor.warning, - ), - const SizedBox(height: 8), - _buildMetricCard( - icon: Icons.trending_up, - label: context.lang.perfomance, - value: 'Top $ranking', - color: AppColor.primary, - ), - ], - ), - ), - ], - ); - } - - Widget _buildMetricCard({ - required IconData icon, - required String label, - required String value, - required Color color, - }) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: color.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.2), width: 1), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(icon, size: 16, color: color), - const SizedBox(width: 6), - Expanded( - child: Text( - label, - style: AppStyle.xs.copyWith( - color: AppColor.textSecondary, - fontWeight: FontWeight.w500, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 6), - Text( - value, - style: AppStyle.md.copyWith( - color: color, - fontWeight: FontWeight.bold, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ); - } - - Widget _buildRevenueDisplay() { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: AppColor.primaryGradient, - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: AppColor.primary.withOpacity(0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - product.revenue.currencyFormatRp, - style: AppStyle.md.copyWith( - color: AppColor.white, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index a966dd2..35437d4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -881,26 +881,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" line_icons: dependency: "direct main" description: @@ -937,26 +937,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.18.0" mime: dependency: transitive description: @@ -1526,10 +1526,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.11" time: dependency: transitive description: @@ -1662,10 +1662,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -1747,5 +1747,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.1 <4.0.0" + dart: ">=3.10.0-0 <4.0.0" flutter: ">=3.29.0"