feat: home bloc

This commit is contained in:
efrilm
2025-08-19 12:23:53 +07:00
parent 9b51bf2bee
commit b731704a3d
15 changed files with 934 additions and 664 deletions
@@ -1,95 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
import 'activity_tile.dart';
class HomeActivity extends StatelessWidget {
const HomeActivity({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: AppValue.padding,
).copyWith(bottom: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Aktivitas Terkini',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
letterSpacing: -0.5,
),
),
TextButton.icon(
onPressed: () {},
icon: const Icon(Icons.arrow_forward_rounded, size: 16),
label: const Text('Lihat Semua'),
style: TextButton.styleFrom(
foregroundColor: AppColor.primary,
textStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
),
const SpaceHeight(16),
Container(
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.border.withOpacity(0.5)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
HomeActivityTile(
title: 'Transaksi Berhasil',
subtitle: 'Kasir-01 • Rp 125.000',
time: '2 menit lalu',
icon: Icons.check_circle_rounded,
color: AppColor.success,
isHighlighted: true,
),
const Divider(height: 1, color: AppColor.border),
HomeActivityTile(
title: 'Stok Menipis',
subtitle: 'Kopi Arabica • 5 unit tersisa',
time: '15 menit lalu',
icon: Icons.warning_amber_rounded,
color: AppColor.warning,
isHighlighted: false,
),
const Divider(height: 1, color: AppColor.border),
HomeActivityTile(
title: 'Login Kasir',
subtitle: 'Sari masuk shift pagi',
time: '1 Jam lalu',
icon: Icons.login_rounded,
color: AppColor.info,
isHighlighted: false,
),
],
),
),
],
),
);
}
}
@@ -1,103 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
class HomeActivityTile extends StatelessWidget {
final String title;
final String subtitle;
final String time;
final IconData icon;
final Color color;
final bool isHighlighted;
const HomeActivityTile({
super.key,
required this.title,
required this.subtitle,
required this.time,
required this.icon,
required this.color,
required this.isHighlighted,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isHighlighted ? color.withOpacity(0.02) : Colors.transparent,
borderRadius: isHighlighted ? BorderRadius.circular(16) : null,
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [color.withOpacity(0.1), color.withOpacity(0.05)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2), width: 1),
),
child: Icon(icon, color: color, size: 20),
),
const SpaceWidth(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
letterSpacing: -0.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SpaceHeight(4),
Text(
subtitle,
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
time,
style: AppStyle.xs.copyWith(
fontSize: 11,
color: AppColor.textLight,
fontWeight: FontWeight.w500,
),
),
if (isHighlighted) ...[
const SpaceHeight(4),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
],
],
),
],
),
);
}
}
@@ -11,7 +11,8 @@ import '../../../../domain/auth/auth.dart';
import '../../../components/spacer/spacer.dart';
class HomeHeader extends StatefulWidget {
const HomeHeader({super.key});
final int totalRevenue;
const HomeHeader({super.key, required this.totalRevenue});
@override
State<HomeHeader> createState() => _HomeHeaderState();
@@ -467,7 +468,7 @@ class _HomeHeaderState extends State<HomeHeader> with TickerProviderStateMixin {
),
const SizedBox(width: 6),
Text(
'${context.lang.sales_today} +25%',
'${context.lang.sales_today} ${widget.totalRevenue.currencyFormatRp}',
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w600,
@@ -1,281 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
class HomePerformance extends StatelessWidget {
const HomePerformance({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: AppValue.padding,
).copyWith(bottom: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Performa Minggu Ini',
style: AppStyle.h6.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
letterSpacing: -0.5,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColor.success.withOpacity(0.1),
AppColor.success.withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColor.success.withOpacity(0.2),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.arrow_upward_rounded,
color: AppColor.success,
size: 14,
),
const SpaceWidth(4),
Text(
'89%',
style: AppStyle.sm.copyWith(
color: AppColor.success,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
],
),
),
],
),
const SpaceHeight(20),
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.border.withOpacity(0.5)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildPerformanceBar(
'Sen',
0.8,
AppColor.primary,
'Rp 2.1M',
),
_buildPerformanceBar(
'Sel',
0.6,
AppColor.primary,
'Rp 1.8M',
),
_buildPerformanceBar(
'Rab',
0.9,
AppColor.success,
'Rp 2.4M',
),
_buildPerformanceBar(
'Kam',
0.7,
AppColor.primary,
'Rp 1.9M',
),
_buildPerformanceBar(
'Jum',
1.0,
AppColor.success,
'Rp 2.5M',
),
_buildPerformanceBar(
'Sab',
0.85,
AppColor.success,
'Rp 2.2M',
),
_buildPerformanceBar(
'Min',
0.4,
AppColor.textLight,
'Rp 1.2M',
),
],
),
const SpaceHeight(24),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColor.primary.withOpacity(0.05),
AppColor.primary.withOpacity(0.02),
],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColor.primary.withOpacity(0.1),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Target Minggu Ini',
style: TextStyle(
fontSize: 12,
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SpaceHeight(4),
Text(
'Rp 15.000.000',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Tercapai',
style: TextStyle(
fontSize: 12,
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SpaceHeight(4),
Row(
children: [
Text(
'89%',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColor.success,
),
),
const SpaceWidth(4),
Icon(
Icons.trending_up_rounded,
color: AppColor.success,
size: 16,
),
],
),
],
),
],
),
),
],
),
),
],
),
);
}
Widget _buildPerformanceBar(
String day,
double percentage,
Color color,
String amount,
) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Amount label
Text(
amount,
style: AppStyle.xs.copyWith(
fontSize: 10,
fontWeight: FontWeight.w600,
color: AppColor.textSecondary,
),
),
const SpaceHeight(8),
// Performance bar
Container(
height: 80,
width: 12,
decoration: BoxDecoration(
color: AppColor.border.withOpacity(0.3),
borderRadius: BorderRadius.circular(6),
),
child: Align(
alignment: Alignment.bottomCenter,
child: AnimatedContainer(
duration: Duration(milliseconds: 800 + (day.hashCode % 400)),
curve: Curves.easeOutCubic,
height: 80 * percentage,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [color, color.withOpacity(0.7)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.circular(6),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
),
),
),
const SpaceHeight(12),
// Day label
Text(
day,
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w600,
),
),
],
);
}
}
+27 -69
View File
@@ -1,11 +1,15 @@
import 'package:flutter/material.dart';
import 'package:line_icons/line_icons.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 {
const HomeStats({super.key});
final DashboardOverview overview;
const HomeStats({super.key, required this.overview});
@override
Widget build(BuildContext context) {
@@ -17,74 +21,30 @@ class HomeStats extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Ringkasan Hari Ini',
style: AppStyle.h6.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
letterSpacing: -0.5,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: AppColor.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColor.success.withOpacity(0.2),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.trending_up_rounded,
color: AppColor.success,
size: 14,
),
const SpaceWidth(4),
Text(
'Live',
style: AppStyle.sm.copyWith(
color: AppColor.success,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
HomeTitle(title: 'Ringkasan Hari Ini'),
const SpaceHeight(20),
Row(
children: [
Expanded(
child: HomeStatsTile(
title: 'Total Penjualan',
value: 'Rp 2.450.000',
icon: Icons.trending_up_rounded,
color: AppColor.success,
change: '+12%',
subtitle: 'dari kemarin',
title: 'Pesanan',
value: overview.totalOrders.toString(),
icon: Icons.receipt_long_rounded,
color: AppColor.info,
subtitle: 'Hari ini',
),
),
const SpaceWidth(16),
Expanded(
child: HomeStatsTile(
title: 'Transaksi',
value: '85',
icon: Icons.receipt_long_rounded,
color: AppColor.info,
change: '+8%',
subtitle: 'lebih tinggi',
title: 'Pelanggan Baru',
value: overview.totalCustomers.toString(),
icon: Icons.person_add_outlined,
color: AppColor.primary,
subtitle: overview.totalCustomers < 1
? 'Hari ini'
: 'bertambah',
),
),
],
@@ -94,23 +54,21 @@ class HomeStats extends StatelessWidget {
children: [
Expanded(
child: HomeStatsTile(
title: 'Profit Bersih',
value: 'Rp 735.000',
icon: Icons.account_balance_wallet_rounded,
title: 'Refund',
value: overview.refundedOrders.toString(),
icon: LineIcons.alternateExchange,
color: AppColor.warning,
change: '+15%',
subtitle: 'margin sehat',
subtitle: 'Hari ini',
),
),
const SpaceWidth(16),
Expanded(
child: HomeStatsTile(
title: 'Pelanggan Baru',
value: '42',
icon: Icons.person_add_rounded,
color: AppColor.primary,
change: '+3%',
subtitle: 'bertambah',
title: 'Void',
value: overview.voidedOrders.toString(),
icon: Icons.cancel_rounded,
color: AppColor.error,
subtitle: 'Hari ini',
),
),
],
@@ -8,7 +8,6 @@ class HomeStatsTile extends StatelessWidget {
final String value;
final IconData icon;
final Color color;
final String change;
final String subtitle;
const HomeStatsTile({
super.key,
@@ -16,7 +15,6 @@ class HomeStatsTile extends StatelessWidget {
required this.value,
required this.icon,
required this.color,
required this.change,
required this.subtitle,
});
@@ -56,20 +54,6 @@ class HomeStatsTile extends StatelessWidget {
),
child: Icon(icon, color: color, size: 20),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColor.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
change,
style: AppStyle.xs.copyWith(
color: AppColor.success,
fontWeight: FontWeight.w700,
),
),
),
],
),
const SpaceHeight(16),
@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
class HomeTitle extends StatelessWidget {
final String title;
const HomeTitle({super.key, required this.title});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: AppStyle.h6.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
letterSpacing: -0.5,
),
),
],
);
}
}
@@ -0,0 +1,39 @@
import 'package:flutter/material.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) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: AppValue.padding,
).copyWith(bottom: 0),
child: Column(
children: [
HomeTitle(title: 'Product Terlaris Hari Ini'),
SpaceHeight(20),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: products.length,
itemBuilder: (context, index) {
return HomeTopProductTile(
product: products[index],
ranking: index + 1,
);
},
),
],
),
);
}
}
@@ -0,0 +1,287 @@
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(),
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(),
],
),
),
),
),
);
}
Widget _buildRankingBadge() {
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(
'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() {
return Row(
children: [
Expanded(
child: Column(
children: [
_buildMetricCard(
icon: Icons.shopping_cart_outlined,
label: 'Quantity Sold',
value: product.quantitySold.toString(),
color: AppColor.info,
),
const SizedBox(height: 8),
_buildMetricCard(
icon: Icons.attach_money,
label: 'Average Price',
value: product.averagePrice.round().currencyFormatRp,
color: AppColor.success,
),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
children: [
_buildMetricCard(
icon: Icons.receipt_outlined,
label: 'Total Orders',
value: product.orderCount.toString(),
color: AppColor.warning,
),
const SizedBox(height: 8),
_buildMetricCard(
icon: Icons.trending_up,
label: 'Performance',
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,
),
),
],
),
);
}
}