feat: category analytic
This commit is contained in:
@@ -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/category_analytic_loader/category_analytic_loader_bloc.dart';
|
||||
import '../../../application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart';
|
||||
import '../../../common/extension/extension.dart';
|
||||
import '../../../common/theme/theme.dart';
|
||||
@@ -23,9 +24,18 @@ class FinancePage extends StatefulWidget implements AutoRouteWrapper {
|
||||
State<FinancePage> createState() => _FinancePageState();
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) => BlocProvider(
|
||||
create: (_) =>
|
||||
getIt<ProfitLossLoaderBloc>()..add(ProfitLossLoaderEvent.fetched()),
|
||||
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) =>
|
||||
getIt<ProfitLossLoaderBloc>()..add(ProfitLossLoaderEvent.fetched()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
getIt<CategoryAnalyticLoaderBloc>()
|
||||
..add(CategoryAnalyticLoaderEvent.fetched()),
|
||||
),
|
||||
],
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
@@ -149,11 +159,20 @@ class _FinancePageState extends State<FinancePage>
|
||||
),
|
||||
),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: FinanceCategory(),
|
||||
),
|
||||
BlocBuilder<
|
||||
CategoryAnalyticLoaderBloc,
|
||||
CategoryAnalyticLoaderState
|
||||
>(
|
||||
builder: (context, stateCategory) {
|
||||
return SliverToBoxAdapter(
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: FinanceCategory(
|
||||
categories: stateCategory.categoryAnalytic.data,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Product Analysis Section
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../../common/extension/extension.dart';
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../domain/analytic/analytic.dart';
|
||||
import '../../../components/widgets/empty_widget.dart';
|
||||
|
||||
class FinanceCategory extends StatelessWidget {
|
||||
const FinanceCategory({super.key});
|
||||
final List<CategoryAnalyticItem> categories;
|
||||
|
||||
const FinanceCategory({super.key, required this.categories});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categories = [
|
||||
{
|
||||
'name': 'Makanan & Minuman',
|
||||
'amount': 'Rp 18.5M',
|
||||
'percentage': 72,
|
||||
'color': AppColor.primary,
|
||||
},
|
||||
{
|
||||
'name': 'Produk Retail',
|
||||
'amount': 'Rp 4.2M',
|
||||
'percentage': 16,
|
||||
'color': AppColor.secondary,
|
||||
},
|
||||
{
|
||||
'name': 'Jasa & Lainnya',
|
||||
'amount': 'Rp 3.1M',
|
||||
'percentage': 12,
|
||||
'color': AppColor.info,
|
||||
},
|
||||
];
|
||||
final totalRevenue = _calculateTotalRevenue();
|
||||
final sortedCategories = _sortCategoriesByRevenue();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
@@ -70,25 +58,25 @@ class FinanceCategory extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
...categories
|
||||
.map(
|
||||
(category) => _buildCategoryItem(
|
||||
category['name'] as String,
|
||||
category['amount'] as String,
|
||||
category['percentage'] as int,
|
||||
category['color'] as Color,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
// Show empty state if no categories
|
||||
if (categories.isEmpty)
|
||||
_buildEmptyState()
|
||||
else
|
||||
...sortedCategories.asMap().entries.map(
|
||||
(entry) => _buildCategoryItem(
|
||||
entry.value,
|
||||
_calculatePercentage(entry.value.totalRevenue, totalRevenue),
|
||||
_getCategoryColor(entry.key),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryItem(
|
||||
String name,
|
||||
String amount,
|
||||
int percentage,
|
||||
CategoryAnalyticItem category,
|
||||
double percentage,
|
||||
Color color,
|
||||
) {
|
||||
return Container(
|
||||
@@ -98,30 +86,59 @@ class FinanceCategory extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
category.categoryName,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${category.productCount} produk • ${category.orderCount} pesanan',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
Text(
|
||||
category.totalRevenue.currencyFormatRp,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
name,
|
||||
style: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
|
||||
'${NumberFormat('#,###', 'id_ID').format(category.totalQuantity)} unit',
|
||||
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
amount,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -135,7 +152,7 @@ class FinanceCategory extends StatelessWidget {
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
'$percentage%',
|
||||
'${percentage.toStringAsFixed(1)}%',
|
||||
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
),
|
||||
@@ -143,4 +160,48 @@ class FinanceCategory extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return EmptyWidget(
|
||||
title: 'Belum ada data kategori',
|
||||
message: 'Data kategori penjualan akan muncul di sini',
|
||||
);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
int _calculateTotalRevenue() {
|
||||
return categories.fold(0, (sum, category) => sum + category.totalRevenue);
|
||||
}
|
||||
|
||||
List<CategoryAnalyticItem> _sortCategoriesByRevenue() {
|
||||
final sorted = List<CategoryAnalyticItem>.from(categories);
|
||||
sorted.sort((a, b) => b.totalRevenue.compareTo(a.totalRevenue));
|
||||
return sorted;
|
||||
}
|
||||
|
||||
double _calculatePercentage(int categoryRevenue, int totalRevenue) {
|
||||
if (totalRevenue == 0) return 0;
|
||||
return (categoryRevenue / totalRevenue) * 100;
|
||||
}
|
||||
|
||||
Color _getCategoryColor(int index) {
|
||||
// Predefined color palette for categories
|
||||
const colors = [
|
||||
AppColor.primary,
|
||||
AppColor.secondary,
|
||||
AppColor.success,
|
||||
AppColor.warning,
|
||||
AppColor.error,
|
||||
AppColor.info,
|
||||
];
|
||||
|
||||
// Generate additional colors if needed
|
||||
if (index < colors.length) {
|
||||
return colors[index];
|
||||
} else {
|
||||
// Generate colors based on index for unlimited categories
|
||||
final hue = (index * 137.5) % 360; // Golden angle approximation
|
||||
return HSLColor.fromAHSL(1.0, hue, 0.7, 0.5).toColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user