import 'package:enaklo_pos/data/models/response/profit_loss_response_model.dart'; import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; // Warna Aplikasi class AppColorProfitLoss { static const primary = Color(0xff36175e); 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 ProfitLossWidget extends StatelessWidget { final ProfitLossData data; final String title; final String searchDateFormatted; const ProfitLossWidget({ super.key, required this.data, required this.title, required this.searchDateFormatted, }); @override Widget build(BuildContext context) { return Container( color: const Color(0xFFF1F5F9), padding: const EdgeInsets.all(20.0), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(), const SizedBox(height: 20), _buildSummaryCards(), const SizedBox(height: 20), _buildProfitTrendChart(), const SizedBox(height: 20), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 3, child: _buildProductProfitability(), ), const SizedBox(width: 16), Expanded( flex: 2, child: _buildProfitBreakdown(), ), ], ), const SizedBox(height: 20), _buildDetailedMetrics(), ], ), ), ); } Widget _buildHeader() { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey[300]!), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF0F172A), ), ), const SizedBox(height: 6), Text( 'Analisis profitabilitas dan margin keuntungan', style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), ], ), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColorProfitLoss.primary, AppColorProfitLoss.secondary ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.date_range, color: Colors.white, size: 18), const SizedBox(width: 8), Text( searchDateFormatted, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), ), ], ), ); } // Fungsi untuk menghitung tren berdasarkan data historis String _calculateTrend(String metric) { if (data.data.length < 2) return '+0.0%'; double current = 0; double previous = 0; // Ambil data dari beberapa hari terakhir untuk perhitungan tren final dataLength = data.data.length; final splitPoint = (dataLength * 0.6).round(); // 60% data lama, 40% data baru final recentData = data.data.skip(splitPoint).toList(); final olderData = data.data.take(splitPoint).toList(); if (recentData.isEmpty || olderData.isEmpty) return '+0.0%'; switch (metric) { case 'revenue': current = recentData.fold(0.0, (sum, item) => sum + item.revenue) / recentData.length; previous = olderData.fold(0.0, (sum, item) => sum + item.revenue) / olderData.length; break; case 'cost': current = recentData.fold(0.0, (sum, item) => sum + item.cost) / recentData.length; previous = olderData.fold(0.0, (sum, item) => sum + item.cost) / olderData.length; break; case 'grossProfit': current = recentData.fold(0.0, (sum, item) => sum + item.grossProfit) / recentData.length; previous = olderData.fold(0.0, (sum, item) => sum + item.grossProfit) / olderData.length; break; case 'netProfit': current = recentData.fold(0.0, (sum, item) => sum + item.netProfit) / recentData.length; previous = olderData.fold(0.0, (sum, item) => sum + item.netProfit) / olderData.length; break; } if (previous == 0) return '+0.0%'; final trendPercentage = ((current - previous) / previous) * 100; final sign = trendPercentage >= 0 ? '+' : ''; return '$sign${trendPercentage.toStringAsFixed(1)}%'; } Widget _buildSummaryCards() { final summaryItems = [ { 'title': 'Jumlah Pendapatan', 'value': _formatCurrency(data.summary.totalRevenue), 'subtitle': '${data.summary.totalOrders} pesanan', 'icon': Icons.attach_money, 'color': AppColorProfitLoss.success, 'trend': _calculateTrend('revenue'), }, { 'title': 'Jumlah Biaya', 'value': _formatCurrency(data.summary.totalCost), 'subtitle': 'HPP & Biaya', 'icon': Icons.receipt, 'color': AppColorProfitLoss.danger, 'trend': _calculateTrend('cost'), }, { 'title': 'Laba Kotor', 'value': _formatCurrency(data.summary.grossProfit), 'subtitle': '${data.summary.grossProfitMargin.toStringAsFixed(1)}% margin', 'icon': Icons.trending_up, 'color': AppColorProfitLoss.primary, 'trend': _calculateTrend('grossProfit'), }, { 'title': 'Laba Bersih', 'value': _formatCurrency(data.summary.netProfit), 'subtitle': '${data.summary.netProfitMargin.toStringAsFixed(1)}% margin', 'icon': Icons.account_balance, 'color': AppColorProfitLoss.info, 'trend': _calculateTrend('netProfit'), }, ]; return Row( children: summaryItems.map((item) { final trendValue = item['trend'] as String; final isPositive = !trendValue.startsWith('-'); final trendColor = isPositive ? AppColorProfitLoss.success : AppColorProfitLoss.danger; return Expanded( child: Container( margin: const EdgeInsets.only(right: 12), padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: Colors.grey[200]!), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: (item['color'] as Color).withOpacity(0.12), borderRadius: BorderRadius.circular(10), ), child: Icon( item['icon'] as IconData, color: item['color'] as Color, size: 22, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: trendColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( trendValue, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: trendColor, ), ), ), ], ), const SizedBox(height: 14), Text( item['value'] as String, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF0F172A), ), ), const SizedBox(height: 4), Text( item['title'] as String, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF475569), ), ), const SizedBox(height: 2), Text( item['subtitle'] as String, style: TextStyle( fontSize: 11, color: Colors.grey[500], ), ), ], ), ), ); }).toList(), ); } Widget _buildProfitTrendChart() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey[200]!), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Analisis Tren Keuntungan', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF0F172A), ), ), Row( children: [ _buildLegendItem('Pendapatan', AppColorProfitLoss.info), const SizedBox(width: 16), _buildLegendItem('Biaya', AppColorProfitLoss.danger), const SizedBox(width: 16), _buildLegendItem('Laba Bersih', AppColorProfitLoss.success), ], ), ], ), const SizedBox(height: 20), SizedBox( height: 220, child: LineChart( LineChartData( gridData: FlGridData( show: true, drawHorizontalLine: true, drawVerticalLine: false, horizontalInterval: 100000, getDrawingHorizontalLine: (value) { return FlLine( color: Colors.grey[100]!, strokeWidth: 1, ); }, ), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 50, 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.data.length) { final date = DateTime.parse(data.data[index].date); return Padding( padding: const EdgeInsets.only(top: 8), child: Text( '${date.day}/${date.month}', 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: [ // Garis Pendapatan LineChartBarData( spots: data.data.asMap().entries.map((entry) { return FlSpot( entry.key.toDouble(), entry.value.revenue.toDouble()); }).toList(), isCurved: true, color: AppColorProfitLoss.info, dotData: const FlDotData(show: false), ), // Garis Biaya LineChartBarData( spots: data.data.asMap().entries.map((entry) { return FlSpot( entry.key.toDouble(), entry.value.cost.toDouble()); }).toList(), isCurved: true, color: AppColorProfitLoss.danger, dotData: const FlDotData(show: false), ), // Garis Laba Bersih LineChartBarData( spots: data.data.asMap().entries.map((entry) { return FlSpot(entry.key.toDouble(), entry.value.netProfit.toDouble()); }).toList(), isCurved: true, color: AppColorProfitLoss.success, dotData: const FlDotData(show: true), belowBarData: BarAreaData( show: true, color: AppColorProfitLoss.success.withOpacity(0.1), ), ), ], ), ), ), ], ), ); } Widget _buildLegendItem(String label, Color color) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 6), Text( label, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF64748B), ), ), ], ); } Widget _buildProductProfitability() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey[200]!), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Profitabilitas Produk', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF0F172A), ), ), const SizedBox(height: 4), Text( 'Analisis margin keuntungan per produk', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(height: 16), Column( children: data.productData.take(4).map((product) { return _buildProductItem(product); }).toList(), ), ], ), ); } Widget _buildProductItem(ProfitLossProduct product) { final profitColor = product.grossProfitMargin >= 35 ? AppColorProfitLoss.success : product.grossProfitMargin >= 25 ? AppColorProfitLoss.warning : AppColorProfitLoss.danger; return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey[100]!), ), child: Column( children: [ Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: AppColorProfitLoss.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(10), ), child: Icon( _getProductIcon(product.categoryName), color: AppColorProfitLoss.primary, size: 22, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.productName, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 15, color: Color(0xFF0F172A), ), ), const SizedBox(height: 2), Text( '${product.quantitySold} unit • ${product.categoryName}', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: profitColor.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Text( '${product.grossProfitMargin.toStringAsFixed(1)}%', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: profitColor, ), ), ), const SizedBox(height: 4), Text( _formatCurrency(product.grossProfit), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF0F172A), ), ), ], ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildProductMetric( 'Pendapatan', product.revenue, AppColorProfitLoss.info), ), const SizedBox(width: 8), Expanded( child: _buildProductMetric( 'Biaya', product.cost, AppColorProfitLoss.danger), ), const SizedBox(width: 8), Expanded( child: _buildProductMetric('Laba/Unit', product.profitPerUnit, AppColorProfitLoss.success), ), ], ), ], ), ); } Widget _buildProductMetric(String label, int value, Color color) { return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(8), ), child: Column( children: [ Text( label, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: Colors.grey[600], ), ), const SizedBox(height: 2), Text( _formatCurrency(value), style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: color, ), ), ], ), ); } Widget _buildProfitBreakdown() { final breakdownData = [ { 'label': 'Laba Kotor', 'value': data.summary.grossProfit, 'color': AppColorProfitLoss.success }, { 'label': 'Pajak', 'value': data.summary.totalTax, 'color': AppColorProfitLoss.warning }, { 'label': 'Diskon', 'value': data.summary.totalDiscount, 'color': AppColorProfitLoss.info }, { 'label': 'Laba Bersih', 'value': data.summary.netProfit, 'color': AppColorProfitLoss.primary }, ]; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey[200]!), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Rincian Keuntungan', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF0F172A), ), ), const SizedBox(height: 4), Text( 'Komponen pembentuk profit', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(height: 20), // Grafik Donut SizedBox( height: 160, child: PieChart( PieChartData( sectionsSpace: 3, centerSpaceRadius: 50, startDegreeOffset: -90, sections: breakdownData.asMap().entries.map((entry) { final item = entry.value; final percentage = (item['value'] as int) / data.summary.grossProfit * 100; return PieChartSectionData( color: item['color'] as Color, value: (item['value'] as int).toDouble(), title: '${percentage.toStringAsFixed(1)}%', radius: 40, titleStyle: const TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white, ), ); }).toList(), ), ), ), const SizedBox(height: 16), // Legenda Column( children: breakdownData.map((item) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: item['color'] as Color, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Expanded( child: Text( item['label'] as String, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, ), ), ), Text( _formatCurrency(item['value'] as int), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF0F172A), ), ), ], ), ); }).toList(), ), const SizedBox(height: 16), // Rasio Profitabilitas Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColorProfitLoss.primary.withOpacity(0.1), AppColorProfitLoss.secondary.withOpacity(0.1), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), ), child: Column( children: [ const Text( 'Rasio Profitabilitas', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF475569), ), ), const SizedBox(height: 8), Text( '${(data.summary.profitabilityRatio * 100).toStringAsFixed(1)}%', style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: AppColorProfitLoss.primary, ), ), const SizedBox(height: 4), Text( 'Tingkat Pengembalian Pendapatan', style: TextStyle( fontSize: 10, color: Colors.grey[600], ), ), ], ), ), ], ), ); } Widget _buildDetailedMetrics() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey[200]!), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Metrik Kinerja Terperinci', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF0F172A), ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: _buildMetricCard( 'Nilai Rata-rata Pesanan', _formatCurrency( (data.summary.totalRevenue / data.summary.totalOrders) .round()), 'Per transaksi', Icons.shopping_cart_outlined, AppColorProfitLoss.info, ), ), const SizedBox(width: 12), Expanded( child: _buildMetricCard( 'Keuntungan Rata-rata', _formatCurrency(data.summary.averageProfit), 'Per pesanan', Icons.trending_up, AppColorProfitLoss.success, ), ), const SizedBox(width: 12), Expanded( child: _buildMetricCard( 'Rasio Biaya', '${((data.summary.totalCost / data.summary.totalRevenue) * 100).toStringAsFixed(1)}%', 'Dari total pendapatan', Icons.pie_chart, AppColorProfitLoss.danger, ), ), ], ), const SizedBox(height: 16), // Tabel rincian harian Container( decoration: BoxDecoration( color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey[100]!), ), child: Column( children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColorProfitLoss.primary.withOpacity(0.05), borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), ), child: const Row( children: [ Expanded( flex: 2, child: Text( 'Tanggal', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF475569), ), ), ), Expanded( child: Text( 'Pendapatan', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF475569), ), ), ), Expanded( child: Text( 'Biaya', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF475569), ), ), ), Expanded( child: Text( 'Laba Bersih', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF475569), ), ), ), Expanded( child: Text( 'Margin', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF475569), ), ), ), ], ), ), ...data.data.map((item) { final date = DateTime.parse(item.date); final dateStr = DateFormat('dd MMM').format(date); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: Colors.grey[100]!), ), ), child: Row( children: [ Expanded( flex: 2, child: Text( dateStr, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF0F172A), ), ), ), Expanded( child: Text( _formatCurrencyShort(item.revenue), textAlign: TextAlign.center, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: AppColorProfitLoss.info, ), ), ), Expanded( child: Text( _formatCurrencyShort(item.cost), textAlign: TextAlign.center, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: AppColorProfitLoss.danger, ), ), ), Expanded( child: Text( _formatCurrencyShort(item.netProfit), textAlign: TextAlign.center, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColorProfitLoss.success, ), ), ), Expanded( child: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 3), decoration: BoxDecoration( color: _getMarginColor(item.netProfitMargin) .withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: Text( '${item.netProfitMargin.toStringAsFixed(1)}%', textAlign: TextAlign.center, style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: _getMarginColor(item.netProfitMargin), ), ), ), ), ], ), ); }).toList(), ], ), ), ], ), ); } Widget _buildMetricCard( String title, String value, String subtitle, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: color.withOpacity(0.06), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.2)), ), child: Column( children: [ Icon(icon, color: color, size: 24), const SizedBox(height: 8), Text( value, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: color, ), ), const SizedBox(height: 4), Text( title, textAlign: TextAlign.center, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF475569), ), ), const SizedBox(height: 2), Text( subtitle, textAlign: TextAlign.center, style: TextStyle( fontSize: 10, color: Colors.grey[600], ), ), ], ), ); } IconData _getProductIcon(String category) { switch (category.toLowerCase()) { case 'coffee': case 'kopi': return Icons.local_cafe; case 'pastry': case 'kue': return Icons.cake; case 'food': case 'makanan': return Icons.restaurant; default: return Icons.inventory; } } Color _getMarginColor(double margin) { if (margin >= 25) return AppColorProfitLoss.success; if (margin >= 15) return AppColorProfitLoss.warning; return AppColorProfitLoss.danger; } String _formatCurrency(int amount) { final formatter = NumberFormat.currency( locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0, ); return formatter.format(amount); } String _formatCurrencyShort(int amount) { if (amount >= 1000000) { return 'Rp ${(amount / 1000000).toStringAsFixed(1)}M'; } else if (amount >= 1000) { return 'Rp ${(amount / 1000).toStringAsFixed(0)}K'; } return 'Rp $amount'; } }