memperbarui beberapa fitur

This commit is contained in:
rhanarmt 2026-05-14 21:59:17 +07:00
parent 16594b95e6
commit b0ed5950c2
8 changed files with 1550 additions and 520 deletions

View File

@ -38,13 +38,11 @@ class DashboardController extends ChangeNotifier {
MLService.getBahanDigunakanHariIni(),
MLService.getDashboardSummary(),
MLService.getProducts(),
MLService.getReportCritical(),
]);
final bahanDigunakan = results[0] as Map<String, dynamic>;
final summary = results[1] as Map<String, dynamic>;
final products = results[2] as List;
final critical = results[3] as Map<String, dynamic>;
_applyBahanDigunakanHariIni(bahanDigunakan);
@ -56,18 +54,13 @@ class DashboardController extends ChangeNotifier {
}
totalProduk = products.length;
if (critical['status'] == true) {
_applyLowStockItems(critical['data']);
} else {
_applyDummyLowStock();
}
_applyLowStockProducts(products);
} catch (e) {
errorMessage = 'Gagal memuat dashboard: $e';
bahanDigunakanError = 'Gagal memuat bahan digunakan hari ini';
_resetBahanDigunakanHariIni();
_applyDummyPenggunaan();
_applyDummyLowStock();
lowStockItems.clear();
} finally {
isLoading = false;
isBahanDigunakanLoading = false;
@ -109,25 +102,31 @@ class DashboardController extends ChangeNotifier {
}
}
void _applyLowStockItems(dynamic data) {
void _applyLowStockProducts(List<dynamic> products) {
lowStockItems.clear();
if (data is! List || data.isEmpty) {
_applyDummyLowStock();
return;
}
for (final item in data) {
final stockValue = StockStatusUtils.parseStock(item['stok']);
for (final item in products) {
if (item is! Map) continue;
final stockValue = StockStatusUtils.parseStock(item['current_stock']);
final statusKey = StockStatusUtils.statusFromStock(stockValue);
final unit = item['unit'] ?? 'kg';
final stockLabel = item['stok']?.toString() ?? '0';
if (statusKey != StockStatusUtils.statusKritis) continue;
final category = item['category']?.toString().toLowerCase() ?? '';
final unit =
item['unit']?.toString() ?? (category == 'barang' ? 'pcs' : 'kg');
lowStockItems.add({
'name': item['nama_bahan']?.toString() ?? '-',
'stock': '$stockLabel $unit',
'name': item['name']?.toString() ?? '-',
'stock': '${_formatStock(stockValue)} $unit',
'statusKey': statusKey,
'status': StockStatusUtils.label(statusKey),
'statusColor': StockStatusUtils.color(statusKey),
});
}
lowStockItems.sort(
(a, b) => a['name'].toString().compareTo(b['name'].toString()),
);
}
void _applyDummyPenggunaan() {
@ -142,34 +141,11 @@ class DashboardController extends ChangeNotifier {
]);
}
void _applyDummyLowStock() {
lowStockItems
..clear()
..addAll([
{
'name': 'Tepung Terigu',
'stock': '5 kg',
'status': StockStatusUtils.label(StockStatusUtils.statusFromStock(5)),
'statusColor': StockStatusUtils.color(
StockStatusUtils.statusFromStock(5),
),
},
{
'name': 'Gula Pasir',
'stock': '8 kg',
'status': StockStatusUtils.label(StockStatusUtils.statusFromStock(8)),
'statusColor': StockStatusUtils.color(
StockStatusUtils.statusFromStock(8),
),
},
{
'name': 'Mentega',
'stock': '3 kg',
'status': StockStatusUtils.label(StockStatusUtils.statusFromStock(3)),
'statusColor': StockStatusUtils.color(
StockStatusUtils.statusFromStock(3),
),
},
]);
String _formatStock(double value) {
if (value % 1 == 0) {
return value.toInt().toString();
}
return value.toStringAsFixed(1);
}
}

View File

@ -90,32 +90,36 @@ class _DashboardScreenState extends State<DashboardScreen> with RouteAware {
padding: EdgeInsets.zero,
),
),
Positioned(
right: 4,
top: 4,
child: Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: AppColors.statusError,
borderRadius: BorderRadius.circular(11),
border: Border.all(
color: AppColors.primaryBrown,
width: 2,
if (_controller.lowStockItems.isNotEmpty)
Positioned(
right: 4,
top: 4,
child: Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: AppColors.statusError,
borderRadius: BorderRadius.circular(
11,
),
border: Border.all(
color: AppColors.primaryBrown,
width: 2,
),
),
),
child: const Center(
child: Text(
'3',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
child: Center(
child: Text(
_controller.lowStockItems.length
.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
],
),
],
@ -278,58 +282,72 @@ class _DashboardScreenState extends State<DashboardScreen> with RouteAware {
],
),
const SizedBox(height: 16),
..._controller.lowStockItems.map((item) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
item['name'],
style: AppTextStyles.labelLarge
.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 4),
Text(
'Stok: ${item['stock']}',
style: AppTextStyles.bodySmall
.copyWith(
color: AppColors.textTertiary,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: item['statusColor'],
borderRadius: BorderRadius.circular(6),
),
child: Text(
item['status'],
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
if (_controller.isLoading)
const Center(child: CircularProgressIndicator())
else if (_controller.lowStockItems.isEmpty)
Text(
'Tidak ada stok kritis',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
)
else
..._controller.lowStockItems.map((item) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
item['name'],
style: AppTextStyles.labelLarge
.copyWith(
color:
AppColors.textPrimary,
),
),
const SizedBox(height: 4),
Text(
'Stok: ${item['stock']}',
style: AppTextStyles.bodySmall
.copyWith(
color:
AppColors.textTertiary,
),
),
],
),
),
),
],
),
);
}),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: item['statusColor'],
borderRadius: BorderRadius.circular(
6,
),
),
child: Text(
item['status'],
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
),
);
}),
],
),
),

View File

@ -78,7 +78,7 @@ class _ProductListScreenState extends State<ProductListScreen> with RouteAware {
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
@ -363,202 +363,481 @@ class _ProductListScreenState extends State<ProductListScreen> with RouteAware {
final maxStock = _controller.maxStock;
final stockPercentage = (product.stock / maxStock * 100).toInt();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
return InkWell(
onTap: () => _showProductDetail(product),
borderRadius: BorderRadius.circular(16),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: _controller
.getStatusColor(product.status)
.withOpacity(0.12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
_controller.getCategoryIcon(product.category),
color: _controller.getStatusColor(product.status),
size: 28,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: _controller
.getStatusColor(product.status)
.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
_controller.getCategoryIcon(product.category),
color: _controller.getStatusColor(product.status),
size: 28,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: AppColors.primaryBrown.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
product.category,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.primaryBrown,
fontWeight: FontWeight.w600,
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Text(
_controller.formatPrice(product.price),
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w600,
const SizedBox(height: 6),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: AppColors.primaryBrown.withValues(
alpha: 0.1,
),
borderRadius: BorderRadius.circular(6),
),
child: Text(
product.category,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.primaryBrown,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 8),
Text(
_controller.formatPrice(product.price),
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: _controller
.getStatusColor(product.status)
.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _controller
.getStatusColor(product.status)
.withValues(alpha: 0.3),
width: 1,
),
],
),
child: Text(
_controller.getStatusLabel(product.status),
style: AppTextStyles.labelSmall.copyWith(
color: _controller.getStatusColor(product.status),
fontWeight: FontWeight.w700,
),
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: _controller
.getStatusColor(product.status)
.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _controller
.getStatusColor(product.status)
.withOpacity(0.3),
width: 1,
),
),
child: Text(
_controller.getStatusLabel(product.status),
style: AppTextStyles.labelSmall.copyWith(
color: _controller.getStatusColor(product.status),
fontWeight: FontWeight.w700,
),
),
),
],
),
const SizedBox(height: 12),
Container(height: 1, color: AppColors.grey200),
const SizedBox(height: 12),
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Stok Tersedia',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'${product.stock} ${product.unit}',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 12),
Container(height: 1, color: AppColors.grey200),
const SizedBox(height: 12),
Row(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Kapasitas',
'Stok Tersedia',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'$stockPercentage%',
style: AppTextStyles.labelSmall.copyWith(
'${product.stock} ${product.unit}',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: product.stock / maxStock,
minHeight: 6,
backgroundColor: AppColors.grey200,
valueColor: AlwaysStoppedAnimation<Color>(
_controller.getStatusColor(product.status),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Kapasitas',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
Text(
'$stockPercentage%',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: product.stock / maxStock,
minHeight: 6,
backgroundColor: AppColors.grey200,
valueColor: AlwaysStoppedAnimation<Color>(
_controller.getStatusColor(product.status),
),
),
),
],
),
),
const SizedBox(width: 12),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.primaryBrown.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.info_outline_rounded,
color: AppColors.primaryBrown,
size: 20,
),
),
],
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${product.name} - Detail'),
duration: const Duration(seconds: 1),
),
);
},
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.primaryBrown.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.arrow_forward_ios,
color: AppColors.primaryBrown,
size: 16,
),
),
),
],
],
),
),
],
),
);
}
void _showProductDetail(Product product) {
final maxStock = _controller.maxStock;
final stockPercentage =
maxStock == 0 ? 0 : (product.stock / maxStock * 100).round();
final statusColor = _controller.getStatusColor(product.status);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return Container(
margin: const EdgeInsets.all(12),
padding: EdgeInsets.only(
left: 18,
right: 18,
top: 12,
bottom: 18 + MediaQuery.of(context).padding.bottom,
),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(18),
boxShadow: [AppColors.shadowMedium],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColors.grey200,
borderRadius: BorderRadius.circular(4),
),
),
),
const SizedBox(height: 18),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
_controller.getCategoryIcon(product.category),
color: statusColor,
size: 28,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
Text(
'ID Produk: ${product.id}',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_controller.getStatusLabel(product.status),
style: AppTextStyles.labelSmall.copyWith(
color: statusColor,
fontWeight: FontWeight.w700,
),
),
),
],
),
const SizedBox(height: 18),
Container(height: 1, color: AppColors.grey200),
const SizedBox(height: 16),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
_buildDetailTile(
icon: Icons.category_outlined,
label: 'Kategori',
value: product.category,
),
_buildDetailTile(
icon: Icons.sell_outlined,
label: 'Harga',
value: _formatRupiah(product.price),
),
_buildDetailTile(
icon: Icons.inventory_2_outlined,
label: 'Stok',
value: '${product.stock} ${product.unit}',
),
_buildDetailTile(
icon: Icons.straighten_outlined,
label: 'Satuan',
value: product.unit,
),
_buildDetailTile(
icon: Icons.speed_outlined,
label: 'Kapasitas',
value: '$stockPercentage%',
),
_buildDetailTile(
icon: Icons.verified_outlined,
label: 'Status',
value: _controller.getStatusLabel(product.status),
),
],
),
const SizedBox(height: 16),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LinearProgressIndicator(
value: maxStock == 0 ? 0 : product.stock / maxStock,
minHeight: 8,
backgroundColor: AppColors.grey200,
valueColor: AlwaysStoppedAnimation<Color>(statusColor),
),
),
const SizedBox(height: 14),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: statusColor.withValues(alpha: 0.18),
),
),
child: Text(
_stockRecommendation(product),
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close_rounded),
label: const Text('Tutup'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/transaction');
},
icon: const Icon(Icons.add_shopping_cart_rounded),
label: const Text('Tambah Stok'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.statusSuccess,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
],
),
);
},
);
}
Widget _buildDetailTile({
required IconData icon,
required String label,
required String value,
}) {
return SizedBox(
width: (MediaQuery.of(context).size.width - 60) / 2,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.bgLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.grey200),
),
child: Row(
children: [
Icon(icon, size: 18, color: AppColors.primaryBrown),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textTertiary,
),
),
const SizedBox(height: 2),
Text(
value,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
],
),
),
],
),
),
);
}
String _formatRupiah(int price) {
final text = price.toString();
final buffer = StringBuffer();
for (var i = 0; i < text.length; i++) {
final reverseIndex = text.length - i;
buffer.write(text[i]);
if (reverseIndex > 1 && reverseIndex % 3 == 1) {
buffer.write('.');
}
}
return 'Rp $buffer';
}
String _stockRecommendation(Product product) {
switch (product.status) {
case 'kritis':
return 'Stok kritis. Disarankan segera tambah stok agar produksi tidak terganggu.';
case 'sedang':
return 'Stok mulai menipis. Pantau pemakaian dan siapkan pembelian berikutnya.';
default:
return 'Stok aman. Produk masih cukup untuk kebutuhan operasional.';
}
}
@override
void dispose() {
routeObserver.unsubscribe(this);

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:finalproject/services/ml_service.dart';
import 'package:finalproject/utils/stock_status.dart';
import 'package:flutter/material.dart';
class ReportController extends ChangeNotifier {
@ -37,15 +38,15 @@ class ReportController extends ChangeNotifier {
MLService.getReportStock(),
MLService.getReportStockIn(),
MLService.getReportPredictions(),
MLService.getReportCritical(),
MLService.getProducts(),
MLService.getDashboardSummary(),
]);
final stockResponse = results[0] as Map<String, dynamic>;
final stockInResponse = results[1] as Map<String, dynamic>;
final predictionResponse = results[2] as Map<String, dynamic>;
final criticalResponse = results[3] as Map<String, dynamic>;
final productsResponse = results[4];
final productsResponse = results[3];
final dashboardSummary = results[4] as Map<String, dynamic>;
if (stockResponse['status'] != true) {
errorMessage = stockResponse['message']?.toString();
@ -65,18 +66,22 @@ class ReportController extends ChangeNotifier {
_applyPredictions(predictionResponse['data']);
}
if (criticalResponse['status'] != true) {
errorMessage ??= criticalResponse['message']?.toString();
} else {
_applyCriticalItems(criticalResponse['data']);
}
if (productsResponse is List) {
_productCount = productsResponse.length;
_applyCriticalProducts(productsResponse);
} else {
_productCount = 0;
criticalItems.clear();
}
if (dashboardSummary['status'] != true) {
errorMessage ??= dashboardSummary['message']?.toString();
usageSummary.clear();
} else {
_applyUsageSummary(dashboardSummary['penggunaan_bahan']);
}
_rebuildSummary();
_rebuildUsageSummary();
_rebuildDemandTrend();
} catch (e) {
errorMessage = 'Gagal memuat laporan: $e';
@ -150,17 +155,50 @@ class ReportController extends ChangeNotifier {
}
}
void _applyCriticalItems(dynamic data) {
void _applyCriticalProducts(List<dynamic> products) {
criticalItems.clear();
if (data is! List) return;
for (final item in data) {
for (final item in products) {
if (item is! Map) continue;
final stock = StockStatusUtils.parseStock(item['current_stock']);
final status = StockStatusUtils.statusFromStock(stock);
if (status != StockStatusUtils.statusKritis) continue;
final category = item['category']?.toString().toLowerCase() ?? '';
final unit =
item['unit']?.toString() ?? (category == 'barang' ? 'pcs' : 'kg');
criticalItems.add({
'name': item['nama_bahan']?.toString() ?? '-',
'stock': _toDouble(item['stok']),
'status': item['status']?.toString() ?? 'Kritis',
'unit': item['unit']?.toString() ?? 'kg',
'name': item['name']?.toString() ?? '-',
'stock': stock,
'status': StockStatusUtils.label(status),
'unit': unit,
});
}
criticalItems.sort(
(a, b) => a['name'].toString().compareTo(b['name'].toString()),
);
}
void _applyUsageSummary(dynamic data) {
usageSummary.clear();
if (data is! List) return;
for (final item in data) {
if (item is! Map) continue;
final label = item['nama_bahan']?.toString() ?? '-';
final total = _toDouble(item['total_digunakan']);
final unit = item['satuan']?.toString() ?? 'kg';
if (total <= 0) continue;
usageSummary.add({'label': label, 'value': total, 'unit': unit});
}
usageSummary.sort(
(a, b) => (b['value'] as double).compareTo(a['value'] as double),
);
}
void _rebuildSummary() {
@ -170,35 +208,17 @@ class ReportController extends ChangeNotifier {
totalKritis = criticalItems.length;
}
void _rebuildUsageSummary() {
final Map<String, double> totals = {};
for (final item in stockHistory) {
final name = item['name'] as String;
final amount = item['amount'] as double;
totals[name] = (totals[name] ?? 0) + amount;
}
final sorted =
totals.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
usageSummary
..clear()
..addAll(
sorted
.take(4)
.map((entry) => {'label': entry.key, 'value': entry.value}),
);
}
void _rebuildDemandTrend() {
final sorted =
predictionItems.toList()..sort(
(a, b) => (a['date'] as DateTime).compareTo(b['date'] as DateTime),
);
final latestItems =
sorted.length > 7 ? sorted.skip(sorted.length - 7) : sorted;
demandTrend
..clear()
..addAll(sorted.take(7).map((entry) => (entry['prediction'] as double)));
..addAll(latestItems.map((entry) => (entry['prediction'] as double)));
}
double _toDouble(dynamic value) {

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ class MLService {
// API URL - Change based on environment
// Untuk emulator Android: 10.0.2.2
// Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local
static const String baseUrl = 'http://192.168.18.30:5000';
static const String baseUrl = 'http://192.168.1.44:5000';
static const int timeoutSeconds = 30;

View File

@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
async:
dependency: transitive
description:
@ -9,6 +17,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.12.0"
barcode:
dependency: transitive
description:
name: barcode
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
url: "https://pub.dev"
source: hosted
version: "2.2.9"
bidi:
dependency: transitive
description:
name: bidi
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
url: "https://pub.dev"
source: hosted
version: "2.0.13"
boolean_selector:
dependency: transitive
description:
@ -152,6 +176,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
intl:
dependency: "direct main"
description:
@ -240,6 +272,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider:
dependency: "direct main"
description:
@ -288,6 +328,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
pdf:
dependency: "direct main"
description:
name: pdf
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
url: "https://pub.dev"
source: hosted
version: "3.11.3"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
platform:
dependency: transitive
description:
@ -304,6 +360,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
share_plus:
dependency: "direct main"
description:
@ -461,6 +533,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.29.0"

View File

@ -46,6 +46,7 @@ dependencies:
# File export
path_provider: ^2.1.4
pdf: ^3.11.1
# Open file + share
open_filex: ^4.5.0