report dashboard
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart';
|
||||
import '../../../../../application/report/report_bloc.dart';
|
||||
import '../../../../../common/data/report_menu.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
@@ -11,6 +12,7 @@ import '../../../../components/page/page_title.dart';
|
||||
import '../../../../components/picker/date_range_picker.dart';
|
||||
import '../../../../components/spaces/space.dart';
|
||||
import '../../../../router/app_router.gr.dart';
|
||||
import 'sections/report_dashboard_section.dart';
|
||||
import 'widgets/report_menu_card.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -27,6 +29,7 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
return Column(
|
||||
children: [
|
||||
PageTitle(
|
||||
isBack: false,
|
||||
title: 'Report',
|
||||
subtitle: state.rangeDateFormatted,
|
||||
actionWidget: [
|
||||
@@ -51,6 +54,11 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
),
|
||||
SpaceWidth(8),
|
||||
AppIconButton(icons: Icons.download_outlined, onTap: () {}),
|
||||
SpaceWidth(8),
|
||||
AppIconButton(
|
||||
icons: Icons.refresh_outlined,
|
||||
onTap: () => onFetched(context, state),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
@@ -81,6 +89,8 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
title: reportMenus[index].title,
|
||||
),
|
||||
);
|
||||
|
||||
onFetched(context, state);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -94,7 +104,20 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
child: Material(
|
||||
color: AppColor.background,
|
||||
child: switch (state.selectedMenu) {
|
||||
0 => Text(state.title),
|
||||
0 =>
|
||||
BlocBuilder<
|
||||
DashboardAnalyticLoaderBloc,
|
||||
DashboardAnalyticLoaderState
|
||||
>(
|
||||
builder: (context, dashboard) {
|
||||
return ReportDashboardSection(
|
||||
menu: reportMenus[state.selectedMenu],
|
||||
state: dashboard,
|
||||
startDate: state.startDate,
|
||||
endDate: state.endDate,
|
||||
);
|
||||
},
|
||||
),
|
||||
1 => Text(state.title),
|
||||
2 => Text(state.title),
|
||||
3 => Text(state.title),
|
||||
@@ -117,7 +140,41 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
);
|
||||
}
|
||||
|
||||
void onFetched(BuildContext context, ReportState state) {
|
||||
switch (state.selectedMenu) {
|
||||
case 0:
|
||||
return context.read<DashboardAnalyticLoaderBloc>().add(
|
||||
DashboardAnalyticLoaderEvent.fetched(
|
||||
startDate: state.startDate,
|
||||
endDate: state.endDate,
|
||||
),
|
||||
);
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) =>
|
||||
BlocProvider(create: (context) => getIt<ReportBloc>(), child: this);
|
||||
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (context) => getIt<ReportBloc>()),
|
||||
BlocProvider(
|
||||
create: (context) => getIt<DashboardAnalyticLoaderBloc>()
|
||||
..add(
|
||||
DashboardAnalyticLoaderEvent.fetched(
|
||||
startDate: DateTime.now().subtract(const Duration(days: 30)),
|
||||
endDate: DateTime.now(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../../application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart';
|
||||
import '../../../../../../common/data/report_menu.dart';
|
||||
import '../../../../../../common/extension/extension.dart';
|
||||
import '../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../components/error/analytic_error_state_widget.dart';
|
||||
import '../../../../../components/loader/loader_with_text.dart';
|
||||
import '../../../../../components/spaces/space.dart';
|
||||
import '../../../../../components/widgets/report/report_header.dart';
|
||||
import '../../../../../components/widgets/report/report_summary_card.dart';
|
||||
import '../widgets/dashboard/report_dashboard_order.dart';
|
||||
import '../widgets/dashboard/report_dashboard_product_chart.dart';
|
||||
import '../widgets/dashboard/report_dashboard_sales_chart.dart';
|
||||
import '../widgets/dashboard/report_dashboard_top_product.dart';
|
||||
|
||||
class ReportDashboardSection extends StatelessWidget {
|
||||
final ReportMenu menu;
|
||||
final DashboardAnalyticLoaderState state;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
|
||||
const ReportDashboardSection({
|
||||
super.key,
|
||||
required this.menu,
|
||||
required this.state,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (state.isFetching) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
|
||||
return state.failureOption.fold(
|
||||
() => RefreshIndicator(
|
||||
backgroundColor: AppColor.white,
|
||||
color: AppColor.primary,
|
||||
onRefresh: () {
|
||||
context.read<DashboardAnalyticLoaderBloc>().add(
|
||||
DashboardAnalyticLoaderEvent.fetched(
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
),
|
||||
);
|
||||
|
||||
return Future.value();
|
||||
},
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
ReportHeader(menu: menu, startDate: startDate, endDate: endDate),
|
||||
_buildSummary(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ReportDashboardSalesChart(
|
||||
sales: List.of(state.dashboardAnalytic.recentSales)
|
||||
..sort(
|
||||
(a, b) => DateTime.parse(
|
||||
a.date,
|
||||
).compareTo(DateTime.parse(b.date)),
|
||||
),
|
||||
),
|
||||
),
|
||||
SpaceWidth(12),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: ReportDashboardProductChart(
|
||||
data: state.dashboardAnalytic.topProducts,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ReportDashboardTopProduct(
|
||||
data: state.dashboardAnalytic.topProducts,
|
||||
),
|
||||
),
|
||||
SpaceWidth(12),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: ReportDashboardOrder(data: state.dashboardAnalytic),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
(f) => AnalyticErrorStateWidget(
|
||||
failure: f,
|
||||
menu: menu,
|
||||
onRefresh: () => context.read<DashboardAnalyticLoaderBloc>().add(
|
||||
DashboardAnalyticLoaderEvent.fetched(
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Padding _buildSummary() {
|
||||
final successfulOrders =
|
||||
state.dashboardAnalytic.overview.totalOrders -
|
||||
state.dashboardAnalytic.overview.voidedOrders -
|
||||
state.dashboardAnalytic.overview.refundedOrders;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ReportSummaryCard(
|
||||
color: AppColor.success,
|
||||
icon: Icons.trending_up,
|
||||
title: 'Total Penjualan',
|
||||
value: state
|
||||
.dashboardAnalytic
|
||||
.overview
|
||||
.totalSales
|
||||
.currencyFormatRpV2,
|
||||
),
|
||||
),
|
||||
SpaceWidth(12),
|
||||
Expanded(
|
||||
child: ReportSummaryCard(
|
||||
color: AppColor.info,
|
||||
icon: Icons.shopping_cart_outlined,
|
||||
title: 'Total Pesanan',
|
||||
value: state.dashboardAnalytic.overview.totalOrders
|
||||
.toString(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SpaceHeight(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ReportSummaryCard(
|
||||
color: AppColor.warning,
|
||||
icon: Icons.attach_money,
|
||||
title: 'Rata-rata Pesanan',
|
||||
value: state
|
||||
.dashboardAnalytic
|
||||
.overview
|
||||
.averageOrderValue
|
||||
.currencyFormatRpV2,
|
||||
),
|
||||
),
|
||||
SpaceWidth(12),
|
||||
Expanded(
|
||||
child: ReportSummaryCard(
|
||||
color: AppColor.primary,
|
||||
icon: Icons.check_circle_outline,
|
||||
title: 'Pesanan Sukses',
|
||||
value: successfulOrders.toString(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../../domain/analytic/analytic.dart';
|
||||
import '../../../../../../components/spaces/space.dart';
|
||||
|
||||
class ReportDashboardOrder extends StatelessWidget {
|
||||
final DashboardAnalytic data;
|
||||
const ReportDashboardOrder({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final successfulOrders =
|
||||
data.overview.totalOrders -
|
||||
data.overview.voidedOrders -
|
||||
data.overview.refundedOrders;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Ringkasan Pesanan',
|
||||
style: AppStyle.xl.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
SpaceHeight(20),
|
||||
_buildSummaryItem(
|
||||
'Total Pesanan',
|
||||
'${data.overview.totalOrders}',
|
||||
Icons.shopping_cart,
|
||||
AppColor.info,
|
||||
),
|
||||
_buildSummaryItem(
|
||||
'Pesanan Sukses',
|
||||
'$successfulOrders',
|
||||
Icons.check_circle,
|
||||
AppColor.success,
|
||||
),
|
||||
_buildSummaryItem(
|
||||
'Pesanan Dibatalkan',
|
||||
'${data.overview.voidedOrders}',
|
||||
Icons.cancel,
|
||||
AppColor.error,
|
||||
),
|
||||
_buildSummaryItem(
|
||||
'Pesanan Refund',
|
||||
'${data.overview.refundedOrders}',
|
||||
Icons.refresh,
|
||||
AppColor.warning,
|
||||
),
|
||||
const SpaceHeight(20),
|
||||
// Payment Methods
|
||||
if (data.paymentMethods.isNotEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Metode Pembayaran',
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
const SpaceHeight(8),
|
||||
...data.paymentMethods.map(
|
||||
(method) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
method.paymentMethodType == 'cash'
|
||||
? Icons.payments
|
||||
: Icons.credit_card,
|
||||
color: AppColor.primary,
|
||||
size: 16,
|
||||
),
|
||||
const SpaceWidth(8),
|
||||
Text(
|
||||
method.paymentMethodName,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryItem(
|
||||
String title,
|
||||
String value,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 16),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(title, style: const TextStyle(fontSize: 12))),
|
||||
Text(value, style: AppStyle.md.copyWith(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../../domain/analytic/analytic.dart';
|
||||
import '../../../../../../components/spaces/space.dart';
|
||||
|
||||
class ReportDashboardProductChart extends StatelessWidget {
|
||||
final List<DashboardTopProduct> data;
|
||||
const ReportDashboardProductChart({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = [
|
||||
AppColor.primary,
|
||||
AppColor.secondary,
|
||||
AppColor.info,
|
||||
AppColor.warning,
|
||||
AppColor.success,
|
||||
];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Distribusi Produk',
|
||||
style: AppStyle.xl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
SpaceHeight(12),
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: 40,
|
||||
sections: data.asMap().entries.map((entry) {
|
||||
return PieChartSectionData(
|
||||
color: colors[entry.key % colors.length],
|
||||
value: entry.value.quantitySold.toDouble(),
|
||||
title: '${entry.value.quantitySold}',
|
||||
radius: 40,
|
||||
titleStyle: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
SpaceHeight(16),
|
||||
Column(
|
||||
children: data.take(3).map((product) {
|
||||
final index = data.indexOf(product);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: colors[index % colors.length],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(product.productName, style: AppStyle.sm),
|
||||
),
|
||||
Text(
|
||||
'${product.quantitySold}',
|
||||
style: AppStyle.sm.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../../../../../common/extension/extension.dart';
|
||||
import '../../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../../domain/analytic/analytic.dart';
|
||||
import '../../../../../../components/spaces/space.dart';
|
||||
|
||||
class ReportDashboardSalesChart extends StatelessWidget {
|
||||
final List<DashboardRecentSale> sales;
|
||||
const ReportDashboardSalesChart({super.key, required this.sales});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (sales.isEmpty) {
|
||||
return const Center(child: Text('Tidak ada data penjualan'));
|
||||
}
|
||||
|
||||
// Sort agar tanggal berurutan
|
||||
final sortedSales = List.of(
|
||||
sales,
|
||||
)..sort((a, b) => DateTime.parse(a.date).compareTo(DateTime.parse(b.date)));
|
||||
|
||||
// Convert ke FlSpot berdasarkan tanggal
|
||||
final spots = sortedSales.map((sale) {
|
||||
final date = DateTime.parse(sale.date);
|
||||
return FlSpot(
|
||||
date.millisecondsSinceEpoch.toDouble(),
|
||||
sale.sales.toDouble(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
// Batas min & max untuk X axis
|
||||
final minX = spots.first.x;
|
||||
final maxX = spots.last.x;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Tren Penjualan Harian',
|
||||
style: AppStyle.xl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
SpaceHeight(12),
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minX: minX,
|
||||
maxX: maxX,
|
||||
lineTouchData: LineTouchData(
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
fitInsideHorizontally: true,
|
||||
fitInsideVertically: true,
|
||||
getTooltipColor: (touchedSpot) => AppColor.primary,
|
||||
getTooltipItems: (touchedSpots) {
|
||||
return touchedSpots.map((spot) {
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(
|
||||
spot.x.toInt(),
|
||||
);
|
||||
final sale = spot.y.toInt();
|
||||
|
||||
return LineTooltipItem(
|
||||
'${date.toFormattedDate()}\n ${sale.currencyFormatRpV2}',
|
||||
AppStyle.xs.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawHorizontalLine: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 200000,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(color: Colors.grey[200]!, strokeWidth: 1);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 60,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
'${(value / 1000).toInt()}K',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval:
|
||||
(maxX - minX) /
|
||||
(sortedSales.length > 6 ? 6 : sortedSales.length),
|
||||
getTitlesWidget: (value, meta) {
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(
|
||||
value.toInt(),
|
||||
);
|
||||
final formatter = DateFormat('dd MMM');
|
||||
|
||||
// tampilkan hanya jika tanggal cocok dengan titik data sebenarnya
|
||||
final match = sortedSales.any((s) {
|
||||
final d = DateTime.parse(s.date);
|
||||
return d.difference(date).inDays.abs() < 1;
|
||||
});
|
||||
|
||||
if (!match) return const SizedBox();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
formatter.format(date),
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: spots,
|
||||
isCurved: true,
|
||||
color: AppColor.primary,
|
||||
dotData: const FlDotData(show: true),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../../../common/extension/extension.dart';
|
||||
import '../../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../../domain/analytic/analytic.dart';
|
||||
import '../../../../../../components/spaces/space.dart';
|
||||
|
||||
class ReportDashboardTopProduct extends StatelessWidget {
|
||||
final List<DashboardTopProduct> data;
|
||||
const ReportDashboardTopProduct({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Produk Terlaris',
|
||||
style: AppStyle.xl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
SpaceHeight(16),
|
||||
Column(
|
||||
children: data.map((product) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColor.border),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product.productName,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SpaceHeight(2),
|
||||
Text(
|
||||
product.categoryName,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontSize: 12,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${product.quantitySold} unit',
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SpaceHeight(2),
|
||||
Text(
|
||||
product.revenue.currencyFormatRpV2,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontSize: 12,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user