Compare commits

..

2 Commits

Author SHA1 Message Date
Efril
b98462ee8c config: logo, app name
Some checks are pending
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-06-23 21:34:41 +07:00
Efril
83f4e065ed feat: update home ui 2026-06-23 21:27:27 +07:00
75 changed files with 2135 additions and 1604 deletions

View File

@ -13,7 +13,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<application
android:label="Enaklo Owner"
android:label="Grow Food"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
<activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 775 B

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Enaklo Owner</string>
<string>Grow Food</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>

View File

@ -3,15 +3,15 @@
flutter_launcher_icons:
android: "launcher_icon"
ios: true
image_path: "assets/images/logo.png"
image_path: "assets/images/ic_launcher.png"
remove_alpha_ios: true
min_sdk_android: 21 # android min sdk min:16, default 21
adaptive_icon_background: "#ffffff"
adaptive_icon_foreground: "assets/images/logo.png"
adaptive_icon_foreground: "assets/images/ic_launcher.png"
web:
generate: true
image_path: "assets/images/logo.png"
image_path: "assets/images/ic_launcher.png"
windows:
generate: true
image_path: "assets/images/logo.png"
image_path: "assets/images/ic_launcher.png"
icon_size: 48

View File

@ -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 {}

View File

@ -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"
}
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -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<AppLocalizations> {

View File

@ -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';
}
}

View File

@ -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';
}
}

View File

@ -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<AssetGenImage> get values => [
icReportExclusiveSummary,
@ -41,6 +45,7 @@ class $AssetsIconsGen {
icReportProfitLoss,
icReportPurchase,
icReportSales,
icReportStock,
];
}

View File

@ -1 +0,0 @@
// TODO: define your code

View File

@ -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<AboutAppPage>
deviceInfo = device;
});
} catch (e) {
print('Error loading app info: $e');
if (kDebugMode) {
print('Error loading app info: $e');
}
}
}

View File

@ -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<LoginPage> createState() => _LoginPageState();
@override
Widget wrappedRoute(BuildContext context) =>
BlocProvider(create: (_) => getIt<LoginFormBloc>(), child: this);
}
class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _slideController;
late AnimationController _backgroundController;
late AnimationController _floatingController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _backgroundAnimation;
late Animation<double> _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<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
);
_backgroundAnimation = Tween<double>(
begin: 0.0,
end: 2 * math.pi,
).animate(_backgroundController);
_floatingAnimation = Tween<double>(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<void> _handleLogin() async {
context.read<LoginFormBloc>().add(LoginFormEvent.submitted());
}
@override
Widget build(BuildContext context) {
@ -117,83 +43,63 @@ class _LoginPageState extends State<LoginPage> 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<LoginFormBloc, LoginFormState>(
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<LoginFormBloc, LoginFormState>(
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<LoginPage> 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<LoginPage> 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<LoginPage> 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<LoginPage> with TickerProviderStateMixin {
autovalidateMode: showErrorMessages
? AutovalidateMode.always
: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -363,7 +243,7 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
const SpaceHeight(16),
_buildForgetPassword(context),
const SpaceHeight(32),
_buildLoginButton(isLoading),
_buildLoginButton(context, isLoading),
],
),
),
@ -386,11 +266,13 @@ class _LoginPageState extends State<LoginPage> 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<LoginFormBloc>().add(LoginFormEvent.submitted());
},
);
}
}

View File

@ -462,6 +462,8 @@ class _ErrorPageState extends State<ErrorPage> with TickerProviderStateMixin {
// Usage Example dengan berbagai variasi
class ErrorPageExamples extends StatelessWidget {
const ErrorPageExamples({super.key});
@override
Widget build(BuildContext context) {
return Column(

View File

@ -191,7 +191,7 @@ class _DailyTasksFormPageState extends State<DailyTasksFormPage>
const SizedBox(height: 16),
...section.questions.map((question) {
return _buildQuestionCard(question);
}).toList(),
}),
],
),
);

View File

@ -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<HomeBloc>()..add(HomeEvent.fetchedDashboard()),
),
BlocProvider(
create: (context) => getIt<OutletListLoaderBloc>()
..add(const OutletListLoaderEvent.fetched()),
create: (context) =>
getIt<OutletListLoaderBloc>()
..add(const OutletListLoaderEvent.fetched()),
),
BlocProvider(
create: (context) => getIt<SelectedOutletBloc>()
..add(const SelectedOutletEvent.loaded()),
create: (context) =>
getIt<SelectedOutletBloc>()
..add(const SelectedOutletEvent.loaded()),
),
BlocProvider(create: (context) => getIt<ExclusiveSummaryLoaderBloc>()),
],
child: this,
);
@ -89,129 +94,132 @@ class _HomePageState extends State<HomePage> 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<HomeBloc, HomeState>(
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<HomeBloc, HomeState>(
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
}
}

View File

@ -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(

View File

@ -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,

View File

@ -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<HomeHeader> createState() => _HomeHeaderState();
}
class _HomeHeaderState extends State<HomeHeader> with SingleTickerProviderStateMixin {
class _HomeHeaderState extends State<HomeHeader>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeInAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _scaleAnimation;
/// 0 = Hari Ini, 1 = MTD (Bulan)
int _selectedDateFilter = 0;
bool _isValueVisible = true;
@override
void initState() {
@ -42,21 +48,37 @@ class _HomeHeaderState extends State<HomeHeader> with SingleTickerProviderStateM
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.5), end: Offset.zero).animate(
Tween<Offset>(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<double>(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<ExclusiveSummaryLoaderBloc>()
..add(ExclusiveSummaryLoaderEvent.rangeDateChanged(dateFrom, dateTo))
..add(const ExclusiveSummaryLoaderEvent.fetched());
}
@override
@ -67,279 +89,205 @@ class _HomeHeaderState extends State<HomeHeader> with SingleTickerProviderStateM
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
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<SelectedOutletBloc, SelectedOutletState>(
listenWhen: (prev, curr) =>
prev.selectedOutletId != curr.selectedOutletId,
listener: (context, state) => _fetchSummary(),
child: BlocBuilder<AuthBloc, AuthState>(
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];
}
}

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
class HeaderDateFilter extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> 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,
),
),
),
),
);
}
}

View File

@ -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<OutletListLoaderBloc>()),
BlocProvider.value(value: context.read<SelectedOutletBloc>()),
],
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<OutletListLoaderBloc, OutletListLoaderState>(
builder: (context, outletListState) {
if (outletListState.isFetching &&
outletListState.outlets.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: CircularProgressIndicator(),
),
);
}
return BlocBuilder<SelectedOutletBloc, SelectedOutletState>(
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<SelectedOutletBloc>().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<SelectedOutletBloc>().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,
),
),
),
],
),
),
);
}
}

View File

@ -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<SelectedOutletBloc, SelectedOutletState>(
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,
),
],
),
),
);
},
);
}
}

View File

@ -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<ExclusiveSummaryDaily> 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<int>(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(),
),
);
}
}

View File

@ -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<HeaderSummarySlider> createState() => _HeaderSummarySliderState();
}
class _HeaderSummarySliderState extends State<HeaderSummarySlider> {
final PageController _pageController = PageController(viewportFraction: 0.92);
int _currentPage = 0;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<ExclusiveSummaryLoaderBloc, ExclusiveSummaryLoaderState>(
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),
),
);
}),
);
}
}

View File

@ -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() : '';
}
}

View File

@ -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<DashboardTopProduct> 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,
),
),
],
),
],
),
);
}
}

View File

@ -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,
});
}

View File

@ -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<HomeOmsetBalance> createState() => _HomeOmsetBalanceState();
}
class _HomeOmsetBalanceState extends State<HomeOmsetBalance> {
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<Offset>(
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<SelectedOutletBloc, SelectedOutletState>(
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,
),
],
),
);
},
),
);
}
}

View File

@ -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<OutletListLoaderBloc, OutletListLoaderState>(
builder: (context, outletListState) {
if (outletListState.isFetching && outletListState.outlets.isEmpty) {
return const _PromoBannerSkeleton();
}
if (outletListState.outlets.isEmpty) {
return const SizedBox.shrink();
}
return BlocBuilder<SelectedOutletBloc, SelectedOutletState>(
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<SelectedOutletBloc>()
.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<SelectedOutletBloc>()
.add(const SelectedOutletEvent.cleared());
} else {
context
.read<SelectedOutletBloc>()
.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),
),
);
}
}

View File

@ -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,
),
),
],
),
],
),
),
);
}
}

View File

@ -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<DashboardTopProduct> 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,
);
},
),
],
),
);
}
}

View File

@ -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,
),
),
],
),
);
}
}

View File

@ -139,7 +139,7 @@ class _SplashPageState extends State<SplashPage> with TickerProviderStateMixin {
child: Opacity(
opacity: logoOpacity,
child: Container(
width: 150,
width: 200,
height: 150,
decoration: BoxDecoration(
boxShadow: [

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 835 B

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB