diff --git a/lib/data/models/response/profit_loss_response_model.dart b/lib/data/models/response/profit_loss_response_model.dart index 0f6f152..a69c7a1 100644 --- a/lib/data/models/response/profit_loss_response_model.dart +++ b/lib/data/models/response/profit_loss_response_model.dart @@ -72,10 +72,14 @@ class ProfitLossData { dateTo: map['date_to'], groupBy: map['group_by'], summary: ProfitLossSummary.fromMap(map['summary']), - data: List.from( - map['data'].map((x) => ProfitLossItem.fromMap(x))), - productData: List.from( - map['product_data'].map((x) => ProfitLossProduct.fromMap(x))), + data: map['data'] == null + ? [] + : List.from( + map['data'].map((x) => ProfitLossItem.fromMap(x))), + productData: map['product_data'] == null + ? [] + : List.from( + map['product_data'].map((x) => ProfitLossProduct.fromMap(x))), ); } diff --git a/lib/presentation/report/widgets/profit_loss_widget.dart b/lib/presentation/report/widgets/profit_loss_widget.dart index b773b38..ac318b9 100644 --- a/lib/presentation/report/widgets/profit_loss_widget.dart +++ b/lib/presentation/report/widgets/profit_loss_widget.dart @@ -172,12 +172,17 @@ class ProfitLossWidget extends StatelessWidget { break; } - if (previous == 0) return '+0.0%'; + // Handle division by zero and invalid values + if (previous == 0 || previous.isNaN || previous.isInfinite) return '+0.0%'; + if (current.isNaN || current.isInfinite) return '+0.0%'; final trendPercentage = ((current - previous) / previous) * 100; - final sign = trendPercentage >= 0 ? '+' : ''; - return '$sign${trendPercentage.toStringAsFixed(1)}%'; + // Check if trendPercentage is valid + if (trendPercentage.isNaN || trendPercentage.isInfinite) return '+0.0%'; + + final sign = trendPercentage >= 0 ? '+' : ''; + return '$sign${trendPercentage.round()}%'; } Widget _buildSummaryCards() { @@ -201,8 +206,7 @@ class ProfitLossWidget extends StatelessWidget { { 'title': 'Laba Kotor', 'value': _formatCurrency(data.summary.grossProfit), - 'subtitle': - '${data.summary.grossProfitMargin.toStringAsFixed(1)}% margin', + 'subtitle': '${_safeRound(data.summary.grossProfitMargin)}% margin', 'icon': Icons.trending_up, 'color': AppColorProfitLoss.primary, 'trend': _calculateTrend('grossProfit'), @@ -210,8 +214,7 @@ class ProfitLossWidget extends StatelessWidget { { 'title': 'Laba Bersih', 'value': _formatCurrency(data.summary.netProfit), - 'subtitle': - '${data.summary.netProfitMargin.toStringAsFixed(1)}% margin', + 'subtitle': '${_safeRound(data.summary.netProfitMargin)}% margin', 'icon': Icons.account_balance, 'color': AppColorProfitLoss.info, 'trend': _calculateTrend('netProfit'), @@ -360,13 +363,17 @@ class ProfitLossWidget extends StatelessWidget { showTitles: true, reservedSize: 50, getTitlesWidget: (value, meta) { - return Text( - '${(value / 1000).toInt()}K', - style: TextStyle( - color: Colors.grey[600], - fontSize: 10, - ), - ); + final kValue = (value / 1000); + if (kValue.isFinite) { + return Text( + '${kValue.toInt()}K', + style: TextStyle( + color: Colors.grey[600], + fontSize: 10, + ), + ); + } + return const SizedBox(); }, ), ), @@ -402,8 +409,9 @@ class ProfitLossWidget extends StatelessWidget { // Garis Pendapatan LineChartBarData( spots: data.data.asMap().entries.map((entry) { + final revenue = entry.value.revenue.toDouble(); return FlSpot( - entry.key.toDouble(), entry.value.revenue.toDouble()); + entry.key.toDouble(), revenue.isFinite ? revenue : 0); }).toList(), isCurved: true, color: AppColorProfitLoss.info, @@ -412,8 +420,9 @@ class ProfitLossWidget extends StatelessWidget { // Garis Biaya LineChartBarData( spots: data.data.asMap().entries.map((entry) { + final cost = entry.value.cost.toDouble(); return FlSpot( - entry.key.toDouble(), entry.value.cost.toDouble()); + entry.key.toDouble(), cost.isFinite ? cost : 0); }).toList(), isCurved: true, color: AppColorProfitLoss.danger, @@ -422,8 +431,9 @@ class ProfitLossWidget extends StatelessWidget { // Garis Laba Bersih LineChartBarData( spots: data.data.asMap().entries.map((entry) { + final netProfit = entry.value.netProfit.toDouble(); return FlSpot(entry.key.toDouble(), - entry.value.netProfit.toDouble()); + netProfit.isFinite ? netProfit : 0); }).toList(), isCurved: true, color: AppColorProfitLoss.success, @@ -506,9 +516,10 @@ class ProfitLossWidget extends StatelessWidget { } Widget _buildProductItem(ProfitLossProduct product) { - final profitColor = product.grossProfitMargin >= 35 + final profitMargin = _safeDouble(product.grossProfitMargin); + final profitColor = profitMargin >= 35 ? AppColorProfitLoss.success - : product.grossProfitMargin >= 25 + : profitMargin >= 25 ? AppColorProfitLoss.warning : AppColorProfitLoss.danger; @@ -572,7 +583,7 @@ class ProfitLossWidget extends StatelessWidget { borderRadius: BorderRadius.circular(6), ), child: Text( - '${product.grossProfitMargin.toStringAsFixed(1)}%', + '${_safeRound(profitMargin)}%', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, @@ -710,13 +721,17 @@ class ProfitLossWidget extends StatelessWidget { startDegreeOffset: -90, sections: breakdownData.asMap().entries.map((entry) { final item = entry.value; + final grossProfit = data.summary.grossProfit; + final value = item['value'] as int; + + // Handle division by zero final percentage = - (item['value'] as int) / data.summary.grossProfit * 100; + grossProfit > 0 ? (value / grossProfit * 100) : 0.0; return PieChartSectionData( color: item['color'] as Color, - value: (item['value'] as int).toDouble(), - title: '${percentage.toStringAsFixed(1)}%', + value: value.toDouble(), + title: '${_safeRound(percentage)}%', radius: 40, titleStyle: const TextStyle( fontSize: 10, @@ -799,7 +814,7 @@ class ProfitLossWidget extends StatelessWidget { ), const SizedBox(height: 8), Text( - '${(data.summary.profitabilityRatio * 100).toStringAsFixed(1)}%', + '${_safeRound(data.summary.profitabilityRatio)}%', style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, @@ -848,9 +863,7 @@ class ProfitLossWidget extends StatelessWidget { Expanded( child: _buildMetricCard( 'Nilai Rata-rata Pesanan', - _formatCurrency( - (data.summary.totalRevenue / data.summary.totalOrders) - .round()), + _formatCurrency(_safeCalculateAverageOrder()), 'Per transaksi', Icons.shopping_cart_outlined, AppColorProfitLoss.info, @@ -870,7 +883,7 @@ class ProfitLossWidget extends StatelessWidget { Expanded( child: _buildMetricCard( 'Rasio Biaya', - '${((data.summary.totalCost / data.summary.totalRevenue) * 100).toStringAsFixed(1)}%', + '${_safeCalculateCostRatio()}%', 'Dari total pendapatan', Icons.pie_chart, AppColorProfitLoss.danger, @@ -1026,7 +1039,7 @@ class ProfitLossWidget extends StatelessWidget { borderRadius: BorderRadius.circular(4), ), child: Text( - '${item.netProfitMargin.toStringAsFixed(1)}%', + '${_safeRound(item.netProfitMargin)}%', textAlign: TextAlign.center, style: TextStyle( fontSize: 10, @@ -1093,6 +1106,31 @@ class ProfitLossWidget extends StatelessWidget { ); } + // Helper methods for safe calculations + int _safeRound(double value) { + if (value.isNaN || value.isInfinite) return 0; + return value.round(); + } + + double _safeDouble(double value) { + if (value.isNaN || value.isInfinite) return 0.0; + return value; + } + + int _safeCalculateAverageOrder() { + if (data.summary.totalOrders == 0) return 0; + final average = data.summary.totalRevenue / data.summary.totalOrders; + if (average.isNaN || average.isInfinite) return 0; + return average.round(); + } + + int _safeCalculateCostRatio() { + if (data.summary.totalRevenue == 0) return 0; + final ratio = (data.summary.totalCost / data.summary.totalRevenue) * 100; + if (ratio.isNaN || ratio.isInfinite) return 0; + return ratio.round(); + } + IconData _getProductIcon(String category) { switch (category.toLowerCase()) { case 'coffee': @@ -1110,8 +1148,9 @@ class ProfitLossWidget extends StatelessWidget { } Color _getMarginColor(double margin) { - if (margin >= 25) return AppColorProfitLoss.success; - if (margin >= 15) return AppColorProfitLoss.warning; + final safeMargin = _safeDouble(margin); + if (safeMargin >= 25) return AppColorProfitLoss.success; + if (safeMargin >= 15) return AppColorProfitLoss.warning; return AppColorProfitLoss.danger; } @@ -1126,7 +1165,7 @@ class ProfitLossWidget extends StatelessWidget { String _formatCurrencyShort(int amount) { if (amount >= 1000000) { - return 'Rp ${(amount / 1000000).toStringAsFixed(1)}M'; + return 'Rp ${(amount / 1000000).round()}M'; } else if (amount >= 1000) { return 'Rp ${(amount / 1000).toStringAsFixed(0)}K'; }