2025-08-06 13:05:58 +07:00
|
|
|
import 'package:enaklo_pos/core/constants/colors.dart';
|
|
|
|
|
import 'package:enaklo_pos/data/models/response/dashboard_analytic_response_model.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
|
|
|
import 'package:intl/intl.dart';
|
|
|
|
|
|
|
|
|
|
// App Colors
|
|
|
|
|
class AppColorDashboard {
|
|
|
|
|
static const secondary = Color(0xff7c3aed);
|
|
|
|
|
static const success = Color(0xff10b981);
|
|
|
|
|
static const warning = Color(0xfff59e0b);
|
|
|
|
|
static const danger = Color(0xffef4444);
|
|
|
|
|
static const info = Color(0xff3b82f6);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class DashboardAnalyticWidget extends StatelessWidget {
|
|
|
|
|
final String title;
|
|
|
|
|
final String searchDateFormatted;
|
|
|
|
|
final DashboardAnalyticData data;
|
|
|
|
|
|
|
|
|
|
const DashboardAnalyticWidget({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.data,
|
|
|
|
|
required this.title,
|
|
|
|
|
required this.searchDateFormatted,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Container(
|
|
|
|
|
color: const Color(0xFFF8FAFC),
|
|
|
|
|
padding: const EdgeInsets.all(24.0),
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
_buildHeader(),
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
_buildKPICards(),
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(flex: 2, child: _buildSalesChart()),
|
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
|
Expanded(flex: 1, child: _buildProductChart()),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(flex: 2, child: _buildTopProductsList()),
|
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
|
Expanded(flex: 1, child: _buildOrderSummary()),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildHeader() {
|
|
|
|
|
return Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
title,
|
|
|
|
|
style: TextStyle(
|
2025-08-06 13:10:40 +07:00
|
|
|
fontSize: 18,
|
2025-08-06 13:05:58 +07:00
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Color(0xFF1F2937),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
Text(
|
|
|
|
|
'Analisis performa penjualan outlet',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
color: Colors.grey[600],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: AppColors.primary,
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
const Icon(Icons.calendar_today, color: Colors.white, size: 16),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
Text(
|
|
|
|
|
searchDateFormatted,
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildKPICards() {
|
|
|
|
|
final successfulOrders = data.overview.totalOrders -
|
|
|
|
|
data.overview.voidedOrders -
|
|
|
|
|
data.overview.refundedOrders;
|
|
|
|
|
|
|
|
|
|
final kpiData = [
|
|
|
|
|
{
|
|
|
|
|
'title': 'Total Penjualan',
|
|
|
|
|
'value': _formatCurrency(data.overview.totalSales),
|
|
|
|
|
'icon': Icons.trending_up,
|
|
|
|
|
'color': AppColorDashboard.success,
|
|
|
|
|
'bgColor': AppColorDashboard.success.withOpacity(0.1),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
'title': 'Total Pesanan',
|
|
|
|
|
'value': '${data.overview.totalOrders}',
|
|
|
|
|
'icon': Icons.shopping_cart,
|
|
|
|
|
'color': AppColorDashboard.info,
|
|
|
|
|
'bgColor': AppColorDashboard.info.withOpacity(0.1),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
'title': 'Rata-rata Pesanan',
|
|
|
|
|
'value': _formatCurrency(data.overview.averageOrderValue.toInt()),
|
|
|
|
|
'icon': Icons.attach_money,
|
|
|
|
|
'color': AppColorDashboard.warning,
|
|
|
|
|
'bgColor': AppColorDashboard.warning.withOpacity(0.1),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
'title': 'Pesanan Sukses',
|
|
|
|
|
'value': '$successfulOrders',
|
|
|
|
|
'icon': Icons.check_circle,
|
|
|
|
|
'color': AppColors.primary,
|
|
|
|
|
'bgColor': AppColors.primary.withOpacity(0.1),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return Row(
|
|
|
|
|
children: kpiData.map((kpi) {
|
|
|
|
|
return Expanded(
|
|
|
|
|
child: Container(
|
|
|
|
|
margin: const EdgeInsets.only(right: 16),
|
|
|
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
border: Border.all(color: Colors.grey[200]!),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: kpi['bgColor'] as Color,
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
|
|
|
|
child: Icon(
|
|
|
|
|
kpi['icon'] as IconData,
|
|
|
|
|
color: kpi['color'] as Color,
|
|
|
|
|
size: 20,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.trending_up,
|
|
|
|
|
color: Colors.grey[400],
|
|
|
|
|
size: 16,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
Text(
|
|
|
|
|
kpi['value'] as String,
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 24,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Color(0xFF1F2937),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
Text(
|
|
|
|
|
kpi['title'] as String,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
color: Colors.grey[600],
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildSalesChart() {
|
|
|
|
|
return Container(
|
|
|
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
border: Border.all(color: Colors.grey[200]!),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'Tren Penjualan Harian',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 18,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Color(0xFF1F2937),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: 200,
|
|
|
|
|
child: LineChart(
|
|
|
|
|
LineChartData(
|
|
|
|
|
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: TextStyle(
|
|
|
|
|
color: Colors.grey[600],
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
bottomTitles: AxisTitles(
|
|
|
|
|
sideTitles: SideTitles(
|
|
|
|
|
showTitles: true,
|
|
|
|
|
getTitlesWidget: (value, meta) {
|
|
|
|
|
final index = value.toInt();
|
|
|
|
|
if (index >= 0 && index < data.recentSales.length) {
|
|
|
|
|
final date =
|
|
|
|
|
DateTime.parse(data.recentSales[index].date);
|
|
|
|
|
final formatter = DateFormat('dd MMM');
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.only(top: 8),
|
|
|
|
|
child: Text(
|
|
|
|
|
formatter.format(date),
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: Colors.grey[600],
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return const SizedBox();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
rightTitles: const AxisTitles(
|
|
|
|
|
sideTitles: SideTitles(showTitles: false)),
|
|
|
|
|
topTitles: const AxisTitles(
|
|
|
|
|
sideTitles: SideTitles(showTitles: false)),
|
|
|
|
|
),
|
|
|
|
|
borderData: FlBorderData(show: false),
|
|
|
|
|
lineBarsData: [
|
|
|
|
|
LineChartBarData(
|
|
|
|
|
spots: data.recentSales.asMap().entries.map((entry) {
|
|
|
|
|
return FlSpot(
|
|
|
|
|
entry.key.toDouble(), entry.value.sales.toDouble());
|
|
|
|
|
}).toList(),
|
|
|
|
|
isCurved: true,
|
|
|
|
|
color: AppColors.primary,
|
|
|
|
|
// strokeWidth: 3,
|
|
|
|
|
dotData: const FlDotData(show: true),
|
|
|
|
|
belowBarData: BarAreaData(
|
|
|
|
|
show: true,
|
|
|
|
|
color: AppColors.primary.withOpacity(0.1),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildProductChart() {
|
|
|
|
|
final colors = [
|
|
|
|
|
AppColors.primary,
|
|
|
|
|
AppColorDashboard.secondary,
|
|
|
|
|
AppColorDashboard.info,
|
|
|
|
|
AppColorDashboard.warning,
|
|
|
|
|
AppColorDashboard.success,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
border: Border.all(color: Colors.grey[200]!),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'Distribusi Produk',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 18,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Color(0xFF1F2937),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: 160,
|
|
|
|
|
child: PieChart(
|
|
|
|
|
PieChartData(
|
|
|
|
|
sectionsSpace: 2,
|
|
|
|
|
centerSpaceRadius: 40,
|
|
|
|
|
sections: data.topProducts.asMap().entries.map((entry) {
|
|
|
|
|
return PieChartSectionData(
|
|
|
|
|
color: colors[entry.key % colors.length],
|
|
|
|
|
value: entry.value.quantitySold.toDouble(),
|
|
|
|
|
title: '${entry.value.quantitySold}',
|
|
|
|
|
radius: 60,
|
|
|
|
|
titleStyle: const TextStyle(
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
Column(
|
|
|
|
|
children: data.topProducts.take(3).map((product) {
|
|
|
|
|
final index = data.topProducts.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: const TextStyle(fontSize: 12),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
'${product.quantitySold}',
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildTopProductsList() {
|
|
|
|
|
return Container(
|
|
|
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
border: Border.all(color: Colors.grey[200]!),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'Produk Terlaris',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 18,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Color(0xFF1F2937),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
Column(
|
|
|
|
|
children: data.topProducts.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: Colors.grey[200]!),
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
width: 40,
|
|
|
|
|
height: 40,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: AppColors.primary.withOpacity(0.1),
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
|
|
|
|
child: const Icon(
|
|
|
|
|
Icons.local_cafe,
|
|
|
|
|
color: AppColors.primary,
|
|
|
|
|
size: 20,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
product.productName,
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
|
Text(
|
|
|
|
|
product.categoryName,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
color: Colors.grey[600],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'${product.quantitySold} unit',
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
|
Text(
|
|
|
|
|
_formatCurrency(product.revenue),
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
color: Colors.grey[600],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildOrderSummary() {
|
|
|
|
|
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),
|
|
|
|
|
border: Border.all(color: Colors.grey[200]!),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'Ringkasan Pesanan',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 18,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Color(0xFF1F2937),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
_buildSummaryItem('Total Pesanan', '${data.overview.totalOrders}',
|
|
|
|
|
Icons.shopping_cart, AppColorDashboard.info),
|
|
|
|
|
_buildSummaryItem('Pesanan Sukses', '$successfulOrders',
|
|
|
|
|
Icons.check_circle, AppColorDashboard.success),
|
|
|
|
|
_buildSummaryItem(
|
|
|
|
|
'Pesanan Dibatalkan',
|
|
|
|
|
'${data.overview.voidedOrders}',
|
|
|
|
|
Icons.cancel,
|
|
|
|
|
AppColorDashboard.danger),
|
|
|
|
|
_buildSummaryItem('Pesanan Refund', '${data.overview.refundedOrders}',
|
|
|
|
|
Icons.refresh, AppColorDashboard.warning),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
// Payment Methods
|
|
|
|
|
if (data.paymentMethods.isNotEmpty)
|
|
|
|
|
Container(
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: AppColors.primary.withOpacity(0.1),
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'Metode Pembayaran',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
color: Color(0xFF374151),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 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: AppColors.primary,
|
|
|
|
|
size: 16,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
Text(
|
|
|
|
|
method.paymentMethodName,
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: AppColors.primary,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
))
|
|
|
|
|
.toList(),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: const TextStyle(
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String _formatCurrency(int amount) {
|
|
|
|
|
final formatter = NumberFormat.currency(
|
|
|
|
|
locale: 'id_ID',
|
|
|
|
|
symbol: 'Rp ',
|
|
|
|
|
decimalDigits: 0,
|
|
|
|
|
);
|
|
|
|
|
return formatter.format(amount);
|
|
|
|
|
}
|
|
|
|
|
}
|