feat: update home ui

This commit is contained in:
Efril
2026-06-23 21:27:27 +07:00
parent e236d811ce
commit 83f4e065ed
34 changed files with 2128 additions and 1597 deletions
+128 -120
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
}
}
@@ -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(
@@ -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,
+223 -275
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];
}
}
@@ -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,
),
),
),
),
);
}
}
@@ -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,
),
),
),
],
),
),
);
}
}
@@ -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,
),
],
),
),
);
},
);
}
}
@@ -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(),
),
);
}
}
@@ -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),
),
);
}),
);
}
}
@@ -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() : '';
}
}
@@ -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,
),
),
],
),
],
),
);
}
}
@@ -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,
});
}
@@ -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,
),
],
),
);
},
),
);
}
}
@@ -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),
),
);
}
}
+128 -34
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,
),
),
],
),
],
),
),
);
}
}
@@ -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,
);
},
),
],
),
);
}
}
@@ -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,
),
),
],
),
);
}
}