Alur Logika Stock It ke produk

This commit is contained in:
rhanarmt 2026-04-23 14:18:00 +07:00
parent 95a703dd4e
commit eae95fa4eb
27 changed files with 4990 additions and 4659 deletions

View File

@ -1,13 +1,13 @@
import 'package:flutter/material.dart';
import 'package:finalproject/theme/app_theme.dart';
import 'package:finalproject/screens/splash_screen.dart';
import 'package:finalproject/screens/login_screen.dart';
import 'package:finalproject/screens/dashboard_screen.dart';
import 'package:finalproject/screens/prediction_screen.dart';
import 'package:finalproject/screens/transaction_screen.dart';
import 'package:finalproject/screens/product_list_screen.dart';
import 'package:finalproject/screens/report_screen.dart';
import 'package:finalproject/screens/settings_screen.dart';
import 'package:finalproject/screens/login/login_page.dart';
import 'package:finalproject/screens/dashboard/dashboard_page.dart';
import 'package:finalproject/screens/prediction/prediction_page.dart';
import 'package:finalproject/screens/transaction/transaction_page.dart';
import 'package:finalproject/screens/products/product_list_page.dart';
import 'package:finalproject/screens/reports/report_page.dart';
import 'package:finalproject/screens/settings/settings_page.dart';
void main() {
runApp(const MyApp());

View File

@ -4,6 +4,7 @@ class Product {
final String category;
final int price;
final int stock;
final String unit;
final String status; // 'tersedia', 'rendah', 'kritis'
Product({
@ -12,6 +13,7 @@ class Product {
required this.category,
required this.price,
required this.stock,
this.unit = 'kg',
this.status = 'tersedia',
});
@ -22,6 +24,7 @@ class Product {
category: json['category'] as String,
price: json['price'] as int,
stock: json['stock'] as int,
unit: (json['unit'] as String?) ?? 'kg',
status: json['status'] as String? ?? 'tersedia',
);
}
@ -33,6 +36,7 @@ class Product {
'category': category,
'price': price,
'stock': stock,
'unit': unit,
'status': status,
};
}
@ -43,6 +47,7 @@ class Product {
String? category,
int? price,
int? stock,
String? unit,
String? status,
}) {
return Product(
@ -51,6 +56,7 @@ class Product {
category: category ?? this.category,
price: price ?? this.price,
stock: stock ?? this.stock,
unit: unit ?? this.unit,
status: status ?? this.status,
);
}

View File

@ -1,135 +0,0 @@
import 'package:flutter/material.dart';
import '../services/ml_service.dart';
class PredictionPage extends StatefulWidget {
const PredictionPage({Key? key}) : super(key: key);
@override
State<PredictionPage> createState() => _PredictionPageState();
}
class _PredictionPageState extends State<PredictionPage> {
bool _isLoading = false;
String? _errorMessage;
Map<String, dynamic>? _predictionResult;
final _tahunController = TextEditingController(text: '2024');
final _bulanController = TextEditingController(text: '4');
final _hariController = TextEditingController(text: '4');
final _hariDalamMingguController = TextEditingController(text: '3');
@override
void initState() {
super.initState();
_checkAPIHealth();
}
Future<void> _checkAPIHealth() async {
final isHealthy = await MLService.healthCheck();
if (!isHealthy) {
setState(() {
_errorMessage = 'API tidak tersedia. Pastikan server Python sudah berjalan.';
});
}
}
Future<void> _predict() async {
setState(() {
_isLoading = true;
_errorMessage = null;
_predictionResult = null;
});
try {
final result = await MLService.prediksiStok(
tahun: int.parse(_tahunController.text),
bulan: int.parse(_bulanController.text),
hari: int.parse(_hariController.text),
hariDalamMinggu: int.parse(_hariDalamMingguController.text),
hariMinggu: int.parse(_hariDalamMingguController.text),
hargaSatuanUpdate: 50000,
totalHargaUpdate: 250000,
produkEncoded: 2,
namaProdukEncoded: 2,
kategoriProdukEncoded: 1,
);
setState(() {
if (result['status'] == 'success') {
_predictionResult = result;
_errorMessage = null;
} else {
_errorMessage = result['message'] ?? 'Prediksi gagal';
_predictionResult = null;
}
});
} catch (e) {
setState(() {
_errorMessage = 'Error: $e';
_predictionResult = null;
});
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Prediksi Permintaan Stok')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
if (_errorMessage != null)
Container(
padding: const EdgeInsets.all(12.0),
margin: const EdgeInsets.only(bottom: 16.0),
decoration: BoxDecoration(
color: Colors.red[100],
border: Border.all(color: Colors.red),
borderRadius: BorderRadius.circular(8.0),
),
child: Text(_errorMessage!, style: const TextStyle(color: Colors.red)),
),
TextField(controller: _tahunController, decoration: const InputDecoration(labelText: 'Tahun')),
TextField(controller: _bulanController, decoration: const InputDecoration(labelText: 'Bulan')),
TextField(controller: _hariController, decoration: const InputDecoration(labelText: 'Hari')),
TextField(controller: _hariDalamMingguController, decoration: const InputDecoration(labelText: 'Hari Minggu')),
const SizedBox(height: 20.0),
ElevatedButton(onPressed: _isLoading ? null : _predict, style: ElevatedButton.styleFrom(minimumSize: const Size.fromHeight(50)), child: _isLoading ? const CircularProgressIndicator() : const Text('PREDIKSI')),
const SizedBox(height: 20.0),
if (_predictionResult != null)
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('HASIL PREDIKSI', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12.0),
Text('Jumlah Unit: ${_predictionResult!['prediksi']['jumlah_unit']}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.green)),
Text('Nilai Raw: ${_predictionResult!['prediksi']['nilai_raw']}'),
const Divider(),
Text('R² Score: ${_predictionResult!['model_accuracy']['r2_score']}'),
Text('MAE: ${_predictionResult!['model_accuracy']['mae']}'),
Text('RMSE: ${_predictionResult!['model_accuracy']['rmse']}'),
],
),
),
),
],
),
),
);
}
@override
void dispose() {
_tahunController.dispose();
_bulanController.dispose();
_hariController.dispose();
_hariDalamMingguController.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,32 @@
import 'package:finalproject/theme/colors.dart';
import 'package:flutter/material.dart';
class DashboardController extends ChangeNotifier {
int selectedIndex = 0;
final List<Map<String, dynamic>> lowStockItems = [
{
'name': 'Tepung Terigu',
'stock': '5 kg',
'status': 'Kritis',
'statusColor': AppColors.statusError,
},
{
'name': 'Gula Pasir',
'stock': '8 kg',
'status': 'Rendah',
'statusColor': AppColors.statusWarning,
},
{
'name': 'Mentega',
'stock': '3 kg',
'status': 'Kritis',
'statusColor': AppColors.statusError,
},
];
void setSelectedIndex(int index) {
selectedIndex = index;
notifyListeners();
}
}

View File

@ -0,0 +1,539 @@
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
import 'package:flutter/material.dart';
import 'dashboard_controller.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
late final DashboardController _controller;
@override
void initState() {
super.initState();
_controller = DashboardController();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Scaffold(
backgroundColor: AppColors.bgLight,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(240),
child: Container(
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Stack(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: IconButton(
icon: const Icon(
Icons.notifications_none,
color: Colors.white,
size: 20,
),
onPressed: () {},
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,
),
),
child: const Center(
child: Text(
'3',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
],
),
const SizedBox(height: 2),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selamat Datang',
style: TextStyle(
color: Colors.white70,
fontSize: 13,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 2),
Text(
'Admin Sulastri',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
],
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
children: [
Expanded(
child: _buildStatCard(
title: 'Total Penjualan',
value: 'Rp 67 Jt',
change: '+12.5%',
icon: Icons.trending_up,
iconBgColor: AppColors.statusSuccess,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
title: 'Produk',
value: '24',
change: 'Aktif',
icon: Icons.shopping_bag,
iconBgColor: AppColors.secondaryBlue,
),
),
],
),
),
],
),
),
),
),
body: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Grafik Penjualan',
style: AppTextStyles.headlineSmall
.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
Text(
'6 Bulan Terakhir',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
),
],
),
Icon(
Icons.trending_up,
color: AppColors.statusSuccess,
size: 20,
),
],
),
const SizedBox(height: 20),
SizedBox(
height: 200,
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...[
'80000000',
'60000000',
'40000000',
'20000000',
'0',
].map((label) {
return Text(
label,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.grey300,
),
);
}).toList(),
],
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
[
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
].map((month) {
return Text(
month,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textTertiary,
),
);
}).toList(),
),
],
),
),
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.warning_amber_rounded,
color: AppColors.statusWarning,
size: 22,
),
const SizedBox(width: 10),
Text(
'Stok Menipis',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
],
),
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,
),
),
),
],
),
);
}).toList(),
],
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pushNamed('/prediction');
},
icon: const Icon(Icons.trending_up, size: 20),
label: const Text(
'Lihat Prediksi',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.secondaryBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 2,
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.check_circle, size: 20),
label: const Text(
'Rekomendasi Stok',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.statusSuccess,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 2,
),
),
),
],
),
const SizedBox(height: 24),
],
),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _controller.selectedIndex,
onTap: (index) {
_controller.setSelectedIndex(index);
switch (index) {
case 0:
break;
case 1:
Navigator.of(context).pushNamed('/products');
break;
case 2:
Navigator.of(context).pushNamed('/transaction');
break;
case 3:
Navigator.of(context).pushNamed('/prediction');
break;
case 4:
Navigator.of(context).pushNamed('/reports');
break;
case 5:
Navigator.of(context).pushNamed('/settings');
break;
}
},
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Stock In',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
label: 'Pengaturan',
),
],
),
);
},
);
}
Widget _buildStatCard({
required String title,
required String value,
required String change,
required IconData icon,
required Color iconBgColor,
}) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(14),
boxShadow: [AppColors.shadowLight],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Text(
value,
style: AppTextStyles.titleLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
Text(
change,
style: AppTextStyles.bodySmall.copyWith(
color:
change.contains('+')
? AppColors.statusSuccess
: AppColors.textTertiary,
fontWeight: FontWeight.w600,
),
),
],
),
),
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: iconBgColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
),
child: Center(child: Icon(icon, color: iconBgColor, size: 22)),
),
],
),
],
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

View File

@ -1,540 +0,0 @@
import 'package:flutter/material.dart';
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({Key? key}) : super(key: key);
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
int _selectedIndex = 0;
// Sample data for low stock items
final List<Map<String, dynamic>> lowStockItems = [
{
'name': 'Tepung Terigu',
'stock': '5 kg',
'status': 'Kritis',
'statusColor': AppColors.statusError,
},
{
'name': 'Gula Pasir',
'stock': '8 kg',
'status': 'Rendah',
'statusColor': AppColors.statusWarning,
},
{
'name': 'Mentega',
'stock': '3 kg',
'status': 'Kritis',
'statusColor': AppColors.statusError,
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgLight,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(240),
child: Container(
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: SafeArea(
child: Column(
children: [
// Top section: Menu + Greeting + Notification
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top row: Menu + Notification
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: IconButton(
icon: const Icon(Icons.menu,
color: Colors.white, size: 20),
onPressed: () {},
padding: EdgeInsets.zero,
),
),
Stack(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: IconButton(
icon: const Icon(Icons.notifications_none,
color: Colors.white, size: 20),
onPressed: () {},
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,
),
),
child: Center(
child: Text(
'3',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
],
),
// Greeting + Title
const SizedBox(height: 6),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Selamat Datang',
style: TextStyle(
color: Colors.white70,
fontSize: 13,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 2),
Text(
'Admin Sulastri',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
],
),
],
),
),
// Stats Cards section (overlap ke bawah)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: _buildStatCard(
title: 'Total Penjualan',
value: 'Rp 67 Jt',
change: '+12.5%',
icon: Icons.trending_up,
iconBgColor: AppColors.statusSuccess,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
title: 'Produk',
value: '24',
change: 'Aktif',
icon: Icons.shopping_bag,
iconBgColor: AppColors.secondaryBlue,
),
),
],
),
),
],
),
),
),
),
body: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
// Chart Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Grafik Penjualan',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
Text(
'6 Bulan Terakhir',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
),
],
),
Icon(Icons.trending_up,
color: AppColors.statusSuccess, size: 20),
],
),
const SizedBox(height: 20),
// Area chart placeholder dengan axes
SizedBox(
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Y-axis labels
...['80000000', '60000000', '40000000', '20000000',
'0']
.map((label) {
return Text(
label,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.grey300,
),
);
}).toList(),
],
),
),
const SizedBox(height: 12),
// X-axis labels
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun']
.map((month) {
return Text(
month,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textTertiary,
),
);
}).toList(),
),
],
),
),
const SizedBox(height: 24),
// Low Stock Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.warning_amber_rounded,
color: AppColors.statusWarning, size: 22),
const SizedBox(width: 10),
Text(
'Stok Menipis',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
],
),
const SizedBox(height: 16),
...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,
),
),
),
],
),
);
}).toList(),
],
),
),
const SizedBox(height: 24),
// Action Buttons - BIGGER & BOLDER
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pushNamed('/prediction');
},
icon: const Icon(Icons.trending_up, size: 20),
label: const Text(
'Lihat Prediksi',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.secondaryBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 2,
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.check_circle, size: 20),
label: const Text(
'Rekomendasi Stok',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.statusSuccess,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 2,
),
),
),
],
),
const SizedBox(height: 24),
],
),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() => _selectedIndex = index);
switch (index) {
case 0:
break;
case 1:
Navigator.of(context).pushNamed('/products');
break;
case 2:
Navigator.of(context).pushNamed('/transaction');
break;
case 3:
Navigator.of(context).pushNamed('/prediction');
break;
case 4:
Navigator.of(context).pushNamed('/reports');
break;
case 5:
Navigator.of(context).pushNamed('/settings');
break;
}
},
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Transaksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
label: 'Pengaturan',
),
],
),
);
}
Widget _buildStatCard({
required String title,
required String value,
required String change,
required IconData icon,
required Color iconBgColor,
}) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(14),
boxShadow: [AppColors.shadowLight],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Text(
value,
style: AppTextStyles.titleLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
Text(
change,
style: AppTextStyles.bodySmall.copyWith(
color:
change.contains('+')
? AppColors.statusSuccess
: AppColors.textTertiary,
fontWeight: FontWeight.w600,
),
),
],
),
),
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: iconBgColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Icon(
icon,
color: iconBgColor,
size: 22,
),
),
),
],
),
],
),
);
}
}

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
class LoginController extends ChangeNotifier {
final usernameController = TextEditingController(text: 'admin@sulastri.com');
final passwordController = TextEditingController(text: 'password');
bool isPasswordVisible = false;
bool isLoading = false;
void togglePasswordVisibility() {
isPasswordVisible = !isPasswordVisible;
notifyListeners();
}
String? validateLoginForm() {
if (usernameController.text.isEmpty || passwordController.text.isEmpty) {
return 'Email dan password tidak boleh kosong';
}
return null;
}
Future<bool> handleLogin() async {
final error = validateLoginForm();
if (error != null) return false;
isLoading = true;
notifyListeners();
await Future.delayed(const Duration(seconds: 1));
isLoading = false;
notifyListeners();
return true;
}
@override
void dispose() {
usernameController.dispose();
passwordController.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,300 @@
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
import 'package:flutter/material.dart';
import 'login_controller.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
late final LoginController _controller;
@override
void initState() {
super.initState();
_controller = LoginController();
}
Future<void> _onLoginPressed() async {
final validationError = _controller.validateLoginForm();
if (validationError != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(validationError),
backgroundColor: AppColors.statusError,
),
);
return;
}
final success = await _controller.handleLogin();
if (!mounted) return;
if (success) {
Navigator.of(context).pushReplacementNamed('/dashboard');
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Scaffold(
backgroundColor: AppColors.bgLight,
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Center(
child: Column(
children: [
const SizedBox(height: 40),
Container(
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 32.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.home_rounded,
color: Colors.white,
size: 40,
),
),
const SizedBox(height: 24),
Text(
'Selamat Datang',
style: AppTextStyles.displaySmall.copyWith(
color: AppColors.primaryBrown,
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Masuk ke akun Anda',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
Align(
alignment: Alignment.centerLeft,
child: Text(
'Email atau Username',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
),
const SizedBox(height: 8),
TextField(
controller: _controller.usernameController,
enabled: !_controller.isLoading,
decoration: InputDecoration(
hintText: 'Masukkan email atau username',
hintStyle: AppTextStyles.bodyMedium.copyWith(
color: AppColors.grey300,
),
prefixIcon: Icon(
Icons.email_outlined,
color: AppColors.textSecondary,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: AppColors.grey300,
),
),
filled: true,
fillColor: AppColors.bgLight,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
const SizedBox(height: 24),
Align(
alignment: Alignment.centerLeft,
child: Text(
'Password',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
),
const SizedBox(height: 8),
TextField(
controller: _controller.passwordController,
enabled: !_controller.isLoading,
obscureText: !_controller.isPasswordVisible,
decoration: InputDecoration(
hintText: 'Masukkan password',
hintStyle: AppTextStyles.bodyMedium.copyWith(
color: AppColors.grey300,
),
prefixIcon: Icon(
Icons.lock_outline,
color: AppColors.textSecondary,
),
suffixIcon: IconButton(
icon: Icon(
_controller.isPasswordVisible
? Icons.visibility
: Icons.visibility_off,
color: AppColors.textSecondary,
),
onPressed:
_controller.isLoading
? null
: _controller
.togglePasswordVisibility,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: AppColors.grey300,
),
),
filled: true,
fillColor: AppColors.bgLight,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed:
_controller.isLoading ? null : () {},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
child: Text(
'Lupa Password?',
style: AppTextStyles.labelMedium.copyWith(
color: AppColors.primaryBrown,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed:
_controller.isLoading
? null
: _onLoginPressed,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBrown,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child:
_controller.isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Text(
'Masuk',
style: AppTextStyles.labelLarge
.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(height: 24),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.bgLight,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.grey300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Demo: Klik "Masuk" untuk melanjutkan',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.statusError,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
const SizedBox(height: 48),
Text(
'© 2025 Toko Bahan Kue Sulastri',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
),
const SizedBox(height: 16),
],
),
),
),
),
),
);
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

View File

@ -1,325 +0,0 @@
import 'package:flutter/material.dart';
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
late TextEditingController _usernameController;
late TextEditingController _passwordController;
bool _isPasswordVisible = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
_usernameController = TextEditingController(text: 'admin@sulastri.com');
_passwordController = TextEditingController(text: 'password');
}
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
// Validation
if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Email dan password tidak boleh kosong'),
backgroundColor: AppColors.statusError,
),
);
return;
}
setState(() => _isLoading = true);
// Simulate login process
await Future.delayed(const Duration(seconds: 1));
setState(() => _isLoading = false);
if (mounted) {
Navigator.of(context).pushReplacementNamed('/dashboard');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgLight,
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Center(
child: Column(
children: [
const SizedBox(height: 40),
// Card Container
Container(
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 32.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Logo Icon
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.home_rounded,
color: Colors.white,
size: 40,
),
),
const SizedBox(height: 24),
// Greeting Title
Text(
'Selamat Datang',
style: AppTextStyles.displaySmall.copyWith(
color: AppColors.primaryBrown,
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
// Subtitle
Text(
'Masuk ke akun Anda',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Email/Username Label
Align(
alignment: Alignment.centerLeft,
child: Text(
'Email atau Username',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
),
const SizedBox(height: 8),
// Email/Username Field
TextField(
controller: _usernameController,
enabled: !_isLoading,
decoration: InputDecoration(
hintText: 'Masukkan email atau username',
hintStyle:
AppTextStyles.bodyMedium.copyWith(
color: AppColors.grey300,
),
prefixIcon: Icon(
Icons.email_outlined,
color: AppColors.textSecondary,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
BorderSide(color: AppColors.grey300),
),
filled: true,
fillColor: AppColors.bgLight,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
const SizedBox(height: 24),
// Password Label
Align(
alignment: Alignment.centerLeft,
child: Text(
'Password',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
),
const SizedBox(height: 8),
// Password Field
TextField(
controller: _passwordController,
enabled: !_isLoading,
obscureText: !_isPasswordVisible,
decoration: InputDecoration(
hintText: 'Masukkan password',
hintStyle:
AppTextStyles.bodyMedium.copyWith(
color: AppColors.grey300,
),
prefixIcon: Icon(
Icons.lock_outline,
color: AppColors.textSecondary,
),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible
? Icons.visibility
: Icons.visibility_off,
color: AppColors.textSecondary,
),
onPressed: _isLoading
? null
: () {
setState(() {
_isPasswordVisible =
!_isPasswordVisible;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
BorderSide(color: AppColors.grey300),
),
filled: true,
fillColor: AppColors.bgLight,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
const SizedBox(height: 16),
// Forgot Password Link
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _isLoading ? null : () {},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
child: Text(
'Lupa Password?',
style: AppTextStyles.labelMedium.copyWith(
color: AppColors.primaryBrown,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(height: 32),
// Login Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBrown,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Text(
'Masuk',
style:
AppTextStyles.labelLarge.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(height: 24),
// Demo Info Box
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.bgLight,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.grey300,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Demo: Klik "Masuk" untuk melanjutkan',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.statusError,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
const SizedBox(height: 48),
// Footer Copyright
Text(
'© 2025 Toko Bahan Kue Sulastri',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
),
const SizedBox(height: 16),
],
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,136 @@
import 'package:finalproject/services/ml_service.dart';
import 'package:flutter/material.dart';
class PredictionController extends ChangeNotifier {
String? selectedRecipe;
int productionQuantity = 0;
bool isCalculated = false;
bool isLoading = true;
List<Map<String, dynamic>> recipes = [];
Map<String, Map<String, dynamic>> recipeIngredients = {};
final Map<String, int> currentStock = {
'Tepung Terigu 1kg': 45000,
'Telur 1kg': 12,
'Gula Pasir 1kg': 28000,
'Susu Bubuk': 8000,
'Cokelat Bubuk 250gr': 22000,
'Mentega 500gr': 15000,
'Keju Parut 250gr': 3000,
'Baking Powder': 60000,
};
Future<String?> loadRecipes() async {
isLoading = true;
notifyListeners();
try {
final fetchedRecipes = await MLService.getRecipes();
recipes = fetchedRecipes;
recipeIngredients = {};
for (final recipe in recipes) {
final recipeName = recipe['recipe_name'] as String?;
if (recipeName == null) continue;
recipeIngredients[recipeName] = {};
if (recipe['ingredients'] != null) {
for (final ingredient in recipe['ingredients']) {
recipeIngredients[recipeName]![ingredient['product_name']] = {
'quantity': ingredient['quantity_needed'],
'unit': ingredient['unit'],
};
}
}
}
return null;
} catch (e) {
return 'Gagal memuat resep: $e';
} finally {
isLoading = false;
notifyListeners();
}
}
void setSelectedRecipe(String? value) {
selectedRecipe = value;
isCalculated = false;
notifyListeners();
}
void setProductionQuantity(String value) {
productionQuantity = int.tryParse(value) ?? 0;
isCalculated = false;
notifyListeners();
}
bool get canCalculate => selectedRecipe != null && productionQuantity > 0;
void calculate() {
isCalculated = true;
notifyListeners();
}
void reset() {
selectedRecipe = null;
productionQuantity = 0;
isCalculated = false;
notifyListeners();
}
Map<String, int> get requiredIngredients {
if (selectedRecipe == null || productionQuantity == 0) {
return {};
}
final ingredients = recipeIngredients[selectedRecipe] ?? {};
final required = <String, int>{};
ingredients.forEach((productName, details) {
final quantity = (details['quantity'] as num).toInt();
required[productName] = quantity * productionQuantity;
});
return required;
}
Map<String, int> get insufficientStock {
final required = requiredIngredients;
final insufficient = <String, int>{};
required.forEach((ingredient, neededAmount) {
final available = currentStock[ingredient] ?? 0;
if (available < neededAmount) {
insufficient[ingredient] = neededAmount - available;
}
});
return insufficient;
}
bool get isStockSufficient => insufficientStock.isEmpty;
String getIngredientUnit(String ingredient) {
if (selectedRecipe == null) return 'gr';
final recipeIngs = recipeIngredients[selectedRecipe];
if (recipeIngs == null) return 'gr';
final ingData = recipeIngs[ingredient];
if (ingData == null) return 'gr';
return ingData['unit'] as String? ?? 'gr';
}
Color getStatusColor(String ingredient) {
final required = requiredIngredients[ingredient] ?? 0;
final available = currentStock[ingredient] ?? 0;
return available >= required
? const Color(0xFF10B981)
: const Color(0xFFDC2626);
}
String cleanIngredientName(String ingredient) {
return ingredient.replaceAll(RegExp(r' \d+(kg|gr)'), '').trim();
}
}

View File

@ -0,0 +1,956 @@
import 'package:flutter/material.dart';
import 'prediction_controller.dart';
class PredictionScreen extends StatefulWidget {
const PredictionScreen({super.key});
@override
State<PredictionScreen> createState() => _PredictionScreenState();
}
class _PredictionScreenState extends State<PredictionScreen> {
late final PredictionController _controller;
@override
void initState() {
super.initState();
_controller = PredictionController();
_loadRecipes();
}
Future<void> _loadRecipes() async {
final error = await _controller.loadRecipes();
if (!mounted || error == null) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error)));
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(200),
child: Container(
decoration: const BoxDecoration(
color: Color(0xFFA89080),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.arrow_back,
color: Colors.white,
size: 20,
),
),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Prediksi Kebutuhan Bahan',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
SizedBox(height: 2),
Text(
'Kalkulasi bahan berdasarkan rencana produksi',
style: TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
Icons.calculate,
color: Colors.white,
size: 18,
),
),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sistem Kalkulasi Otomatis',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 4),
Text(
'Pilih produk dan jumlah untuk lihat kebutuhan bahan',
style: TextStyle(
color: Colors.white70,
fontSize: 11,
fontWeight: FontWeight.w400,
height: 1.3,
),
),
],
),
),
],
),
),
],
),
),
),
),
),
body:
_controller.isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Memuat resep...'),
],
),
)
: _controller.recipes.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 48,
color: Color(0xFFDC2626),
),
const SizedBox(height: 16),
const Text('Gagal memuat resep'),
const SizedBox(height: 8),
ElevatedButton(
onPressed: _loadRecipes,
child: const Text('Coba Lagi'),
),
],
),
)
: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Rencana Produksi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 16),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'Pilih Produk',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
border: Border.all(
color: const Color(0xFFE5E7EB),
width: 1,
),
borderRadius: BorderRadius.circular(
10,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _controller.selectedRecipe,
hint: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 12,
),
child: Text(
'-- Pilih Produk --',
style: TextStyle(
color: Color(0xFF9CA3AF),
fontSize: 14,
),
),
),
isExpanded: true,
icon: const Padding(
padding: EdgeInsets.only(
right: 12,
),
child: Icon(
Icons.expand_more,
color: Color(0xFF9CA3AF),
),
),
items:
_controller.recipes
.map(
(
recipe,
) => DropdownMenuItem<
String
>(
value:
recipe['recipe_name']
as String,
child: Padding(
padding:
const EdgeInsets.symmetric(
horizontal:
12,
),
child: Text(
recipe['recipe_name']
as String,
),
),
),
)
.toList(),
onChanged:
_controller.setSelectedRecipe,
),
),
),
],
),
const SizedBox(height: 16),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'Jumlah Produksi',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 8),
TextField(
keyboardType: TextInputType.number,
onChanged:
_controller.setProductionQuantity,
decoration: InputDecoration(
hintText: '0',
hintStyle: const TextStyle(
color: Color(0xFF9CA3AF),
),
suffixText: 'pcs',
suffixStyle: const TextStyle(
color: Color(0xFF9CA3AF),
fontSize: 12,
),
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(10),
borderSide: const BorderSide(
color: Color(0xFFE5E7EB),
),
),
enabledBorder: OutlineInputBorder(
borderRadius:
BorderRadius.circular(10),
borderSide: const BorderSide(
color: Color(0xFFE5E7EB),
),
),
contentPadding:
const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
),
),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed:
_controller.canCalculate
? _controller.calculate
: null,
icon: const Icon(
Icons.calculate,
size: 18,
),
label: const Text(
'Hitung Kebutuhan',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(
0xFFA89080,
),
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10),
),
),
),
),
const SizedBox(width: 10),
OutlinedButton(
onPressed: _controller.reset,
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Color(0xFFE5E7EB),
width: 1,
),
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10),
),
),
child: const Text(
'Reset',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
),
],
),
],
),
),
const SizedBox(height: 24),
if (_controller.isCalculated) ...[
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color:
_controller.isStockSufficient
? const Color(
0xFF10B981,
).withOpacity(0.1)
: const Color(
0xFFDC2626,
).withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color:
_controller.isStockSufficient
? const Color(
0xFF10B981,
).withOpacity(0.3)
: const Color(
0xFFDC2626,
).withOpacity(0.3),
width: 1,
),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color:
_controller.isStockSufficient
? const Color(
0xFF10B981,
).withOpacity(0.2)
: const Color(
0xFFDC2626,
).withOpacity(0.2),
borderRadius: BorderRadius.circular(
12,
),
),
child: Icon(
_controller.isStockSufficient
? Icons.check_circle
: Icons.warning_amber,
color:
_controller.isStockSufficient
? const Color(0xFF10B981)
: const Color(0xFFDC2626),
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
_controller.isStockSufficient
? 'Stok Cukup'
: 'Stok Belum Cukup',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color:
_controller
.isStockSufficient
? const Color(
0xFF10B981,
)
: const Color(
0xFFDC2626,
),
),
),
const SizedBox(height: 2),
Text(
_controller.isStockSufficient
? 'Semua bahan tersedia untuk produksi'
: 'Ada bahan yang perlu ditambah',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
const Text(
'Kebutuhan Bahan',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children:
_controller.requiredIngredients.entries.toList().asMap().entries.map((
entry,
) {
final isLast =
entry.key ==
_controller
.requiredIngredients
.length -
1;
final ingredient = entry.value.key;
final neededAmount =
entry.value.value;
final availableAmount =
_controller
.currentStock[ingredient] ??
0;
final unit = _controller
.getIngredientUnit(ingredient);
final isSufficient =
availableAmount >= neededAmount;
return Column(
children: [
Padding(
padding: const EdgeInsets.all(
14,
),
child: Column(
children: [
Row(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
_controller
.cleanIngredientName(
ingredient,
),
style: const TextStyle(
fontSize: 13,
fontWeight:
FontWeight
.w600,
color: Color(
0xFF1F2937,
),
),
),
const SizedBox(
height: 6,
),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
const Text(
'Kebutuhan',
style: TextStyle(
fontSize:
11,
color: Color(
0xFF9CA3AF,
),
),
),
Text(
'$neededAmount $unit',
style: const TextStyle(
fontSize:
12,
fontWeight:
FontWeight.w700,
color: Color(
0xFF1F2937,
),
),
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
const Text(
'Stok Saat Ini',
style: TextStyle(
fontSize:
11,
color: Color(
0xFF9CA3AF,
),
),
),
Text(
'$availableAmount $unit',
style: const TextStyle(
fontSize:
12,
fontWeight:
FontWeight.w700,
color: Color(
0xFF1F2937,
),
),
),
],
),
),
],
),
],
),
),
const SizedBox(
width: 10,
),
Container(
padding:
const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: _controller
.getStatusColor(
ingredient,
)
.withOpacity(
0.15,
),
borderRadius:
BorderRadius.circular(
8,
),
border: Border.all(
color: _controller
.getStatusColor(
ingredient,
)
.withOpacity(
0.3,
),
width: 1,
),
),
child: Text(
isSufficient
? 'Aman'
: 'Kurang',
style: TextStyle(
fontSize: 11,
fontWeight:
FontWeight
.w700,
color: _controller
.getStatusColor(
ingredient,
),
),
),
),
],
),
],
),
),
if (!isLast)
Container(
height: 1,
color: const Color(
0xFFF3F4F6,
),
),
],
);
}).toList(),
),
),
const SizedBox(height: 16),
if (!_controller.isStockSufficient) ...[
const Text(
'Rekomendasi Penambahan Stok',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(
0xFFDC2626,
).withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(
0xFFDC2626,
).withOpacity(0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children:
_controller.insufficientStock.entries.map((
entry,
) {
final ingredient = entry.key;
final deficitAmount = entry.value;
final unit = _controller
.getIngredientUnit(ingredient);
return Padding(
padding:
const EdgeInsets.symmetric(
vertical: 8,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Expanded(
child: Text(
_controller
.cleanIngredientName(
ingredient,
),
style: const TextStyle(
fontSize: 13,
fontWeight:
FontWeight.w600,
color: Color(
0xFF1F2937,
),
),
),
),
Container(
padding:
const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: const Color(
0xFFDC2626,
).withOpacity(0.1),
borderRadius:
BorderRadius.circular(
8,
),
),
child: Text(
'+$deficitAmount $unit',
style: const TextStyle(
fontSize: 12,
fontWeight:
FontWeight.w700,
color: Color(
0xFFDC2626,
),
),
),
),
],
),
);
}).toList(),
),
),
],
] else
Container(
width: double.infinity,
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const Column(
children: [
_EmptyCalcIcon(),
SizedBox(height: 16),
Text(
'Belum Ada Kalkulasi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
SizedBox(height: 8),
Text(
'Pilih produk dan masukkan jumlah produksi untuk melihat kebutuhan bahan',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: Color(0xFF9CA3AF),
height: 1.5,
),
),
],
),
),
],
),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 3,
type: BottomNavigationBarType.fixed,
onTap: (index) {
switch (index) {
case 0:
Navigator.popUntil(context, (route) => route.isFirst);
break;
case 1:
Navigator.pushNamed(context, '/products');
break;
case 2:
Navigator.pushNamed(context, '/transaction');
break;
case 4:
Navigator.pushNamed(context, '/reports');
break;
case 5:
Navigator.pushNamed(context, '/settings');
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Stock In',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
label: 'Pengaturan',
),
],
),
);
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
class _EmptyCalcIcon extends StatelessWidget {
const _EmptyCalcIcon();
@override
Widget build(BuildContext context) {
return SizedBox(
width: 60,
height: 60,
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xFF9CA3AF).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.calculate, color: Color(0xFF9CA3AF), size: 32),
),
);
}
}

View File

@ -1,909 +0,0 @@
import 'package:flutter/material.dart';
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
import 'package:finalproject/services/ml_service.dart';
class PredictionScreen extends StatefulWidget {
const PredictionScreen({Key? key}) : super(key: key);
@override
State<PredictionScreen> createState() => _PredictionScreenState();
}
class _PredictionScreenState extends State<PredictionScreen> {
String? _selectedRecipe;
int _productionQuantity = 0;
bool _isCalculated = false;
bool _isLoading = true;
// Data dari API
List<Map<String, dynamic>> recipes = [];
Map<String, Map<String, dynamic>> recipeIngredients = {};
// Stok saat ini (sama dengan di product list)
final Map<String, int> currentStock = {
'Tepung Terigu 1kg': 45000, // gram
'Telur 1kg': 12, // butir
'Gula Pasir 1kg': 28000, // gram
'Susu Bubuk': 8000, // gram
'Cokelat Bubuk 250gr': 22000, // gram
'Mentega 500gr': 15000, // gram
'Keju Parut 250gr': 3000, // gram
'Baking Powder': 60000, // gram
};
@override
void initState() {
super.initState();
_loadRecipes();
}
Future<void> _loadRecipes() async {
try {
final fetchedRecipes = await MLService.getRecipes();
setState(() {
recipes = fetchedRecipes;
// Build recipeIngredients map
for (var recipe in recipes) {
recipeIngredients[recipe['recipe_name']] = {};
if (recipe['ingredients'] != null) {
for (var ingredient in recipe['ingredients']) {
recipeIngredients[recipe['recipe_name']]![ingredient['product_name']] = {
'quantity': ingredient['quantity_needed'],
'unit': ingredient['unit'],
};
}
}
}
_isLoading = false;
});
} catch (e) {
print('Error loading recipes: $e');
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal memuat resep: $e')),
);
}
}
Map<String, int> get requiredIngredients {
if (_selectedRecipe == null || _productionQuantity == 0) {
return {};
}
final ingredients = recipeIngredients[_selectedRecipe] ?? {};
final required = <String, int>{};
ingredients.forEach((productName, details) {
final quantity = (details['quantity'] as num).toInt();
required[productName] = quantity * _productionQuantity;
});
return required;
}
Map<String, int> get insufficientStock {
final required = requiredIngredients;
final insufficient = <String, int>{};
required.forEach((ingredient, neededAmount) {
final available = currentStock[ingredient] ?? 0;
if (available < neededAmount) {
insufficient[ingredient] = neededAmount - available;
}
});
return insufficient;
}
bool get isStockSufficient => insufficientStock.isEmpty;
String _getIngredientUnit(String ingredient) {
if (_selectedRecipe == null) return 'gr';
final recipeIngs = recipeIngredients[_selectedRecipe];
if (recipeIngs == null) return 'gr';
final ingData = recipeIngs[ingredient];
if (ingData == null) return 'gr';
return ingData['unit'] as String? ?? 'gr';
}
Color _getStatusColor(String ingredient) {
final required = requiredIngredients[ingredient] ?? 0;
final available = currentStock[ingredient] ?? 0;
return available >= required ? Color(0xFF10B981) : Color(0xFFDC2626);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFF5F5F5),
// Brown Header
appBar: PreferredSize(
preferredSize: const Size.fromHeight(200),
child: Container(
decoration: BoxDecoration(
color: Color(0xFFA89080),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Back button + Title
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.arrow_back,
color: Colors.white, size: 20),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Prediksi Kebutuhan Bahan',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
SizedBox(height: 2),
Text(
'Kalkulasi bahan berdasarkan rencana produksi',
style: TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
const SizedBox(height: 12),
// Info Box
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(Icons.calculate,
color: Colors.white, size: 18),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Sistem Kalkulasi Otomatis',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 4),
Text(
'Pilih produk dan jumlah untuk lihat kebutuhan bahan',
style: TextStyle(
color: Colors.white70,
fontSize: 11,
fontWeight: FontWeight.w400,
height: 1.3,
),
),
],
),
),
],
),
),
],
),
),
),
),
),
body: _isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Memuat resep...'),
],
),
)
: recipes.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Color(0xFFDC2626)),
const SizedBox(height: 16),
const Text('Gagal memuat resep'),
const SizedBox(height: 8),
ElevatedButton(
onPressed: _loadRecipes,
child: const Text('Coba Lagi'),
),
],
),
)
: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Input Form Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Rencana Produksi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 16),
// Product Selection
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pilih Produk',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
border: Border.all(
color: Color(0xFFE5E7EB),
width: 1,
),
borderRadius: BorderRadius.circular(10),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedRecipe,
hint: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text(
'-- Pilih Produk --',
style: TextStyle(
color: Color(0xFF9CA3AF),
fontSize: 14,
),
),
),
isExpanded: true,
icon: const Padding(
padding: EdgeInsets.only(right: 12),
child: Icon(Icons.expand_more,
color: Color(0xFF9CA3AF)),
),
items: recipes
.map((recipe) =>
DropdownMenuItem<String>(
value: recipe['recipe_name'] as String,
child: Padding(
padding:
const EdgeInsets.symmetric(
horizontal: 12),
child: Text(recipe['recipe_name'] as String),
),
))
.toList(),
onChanged: (value) {
setState(() {
_selectedRecipe = value;
_isCalculated = false;
});
},
),
),
),
],
),
const SizedBox(height: 16),
// Quantity Input
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Jumlah Produksi',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 8),
TextField(
keyboardType: TextInputType.number,
onChanged: (value) {
setState(() {
_productionQuantity =
int.tryParse(value) ?? 0;
_isCalculated = false;
});
},
decoration: InputDecoration(
hintText: '0',
hintStyle:
const TextStyle(color: Color(0xFF9CA3AF)),
suffixText: 'pcs',
suffixStyle: const TextStyle(
color: Color(0xFF9CA3AF),
fontSize: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: Color(0xFFE5E7EB),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: Color(0xFFE5E7EB),
),
),
contentPadding:
const EdgeInsets.symmetric(
horizontal: 12, vertical: 12),
),
),
],
),
const SizedBox(height: 20),
// Calculate Button
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: (_selectedRecipe != null &&
_productionQuantity > 0)
? () {
setState(() => _isCalculated = true);
}
: null,
icon: const Icon(Icons.calculate, size: 18),
label: const Text(
'Hitung Kebutuhan',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFA89080),
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
const SizedBox(width: 10),
OutlinedButton(
onPressed: () {
setState(() {
_selectedRecipe = null;
_productionQuantity = 0;
_isCalculated = false;
});
},
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Color(0xFFE5E7EB),
width: 1,
),
padding:
const EdgeInsets.symmetric(horizontal: 20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
'Reset',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
),
],
),
],
),
),
const SizedBox(height: 24),
// Results Section
if (_isCalculated) ...[
// Status Card
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isStockSufficient
? Color(0xFF10B981).withOpacity(0.1)
: Color(0xFFDC2626).withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isStockSufficient
? Color(0xFF10B981).withOpacity(0.3)
: Color(0xFFDC2626).withOpacity(0.3),
width: 1,
),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isStockSufficient
? Color(0xFF10B981).withOpacity(0.2)
: Color(0xFFDC2626).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
isStockSufficient
? Icons.check_circle
: Icons.warning_amber,
color: isStockSufficient
? Color(0xFF10B981)
: Color(0xFFDC2626),
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isStockSufficient
? 'Stok Cukup'
: 'Stok Belum Cukup',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: isStockSufficient
? Color(0xFF10B981)
: Color(0xFFDC2626),
),
),
const SizedBox(height: 2),
Text(
isStockSufficient
? 'Semua bahan tersedia untuk produksi'
: 'Ada bahan yang perlu ditambah',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
// Ingredients Table
Text(
'Kebutuhan Bahan',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: requiredIngredients.entries
.toList()
.asMap()
.entries
.map((entry) {
final isLast =
entry.key == requiredIngredients.length - 1;
final ingredient = entry.value.key;
final neededAmount = entry.value.value;
final availableAmount =
currentStock[ingredient] ?? 0;
final unit = _getIngredientUnit(ingredient);
final isSufficient =
availableAmount >= neededAmount;
return Column(
children: [
Padding(
padding: const EdgeInsets.all(14),
child: Column(
children: [
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
ingredient
.replaceAll(
RegExp(r' \d+(kg|gr)'),
'')
.trim(),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
'Kebutuhan',
style: TextStyle(
fontSize: 11,
color: Color(
0xFF9CA3AF),
),
),
Text(
'$neededAmount $unit',
style:
const TextStyle(
fontSize: 12,
fontWeight:
FontWeight.w700,
color: Color(
0xFF1F2937),
),
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
'Stok Saat Ini',
style: TextStyle(
fontSize: 11,
color: Color(
0xFF9CA3AF),
),
),
Text(
'$availableAmount $unit',
style:
const TextStyle(
fontSize: 12,
fontWeight:
FontWeight.w700,
color: Color(
0xFF1F2937),
),
),
],
),
),
],
),
],
),
),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: _getStatusColor(ingredient)
.withOpacity(0.15),
borderRadius:
BorderRadius.circular(8),
border: Border.all(
color: _getStatusColor(ingredient)
.withOpacity(0.3),
width: 1,
),
),
child: Text(
isSufficient ? 'Aman' : 'Kurang',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: _getStatusColor(
ingredient),
),
),
),
],
),
],
),
),
if (!isLast)
Container(
height: 1,
color: Color(0xFFF3F4F6),
),
],
);
}).toList(),
),
),
const SizedBox(height: 16),
// Recommendation Card
if (!isStockSufficient) ...[
Text(
'Rekomendasi Penambahan Stok',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Color(0xFFDC2626).withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Color(0xFFDC2626).withOpacity(0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: insufficientStock.entries
.map((entry) {
final ingredient = entry.key;
final deficitAmount = entry.value;
final unit = _getIngredientUnit(ingredient);
return Padding(
padding:
const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
ingredient
.replaceAll(
RegExp(r' \d+(kg|gr)'), '')
.trim(),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Color(0xFFDC2626)
.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'+$deficitAmount $unit',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Color(0xFFDC2626),
),
),
),
],
),
);
}).toList(),
),
),
],
] else
Container(
width: double.infinity,
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Color(0xFF9CA3AF).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.calculate,
color: Color(0xFF9CA3AF), size: 32),
),
const SizedBox(height: 16),
const Text(
'Belum Ada Kalkulasi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 8),
const Text(
'Pilih produk dan masukkan jumlah produksi untuk melihat kebutuhan bahan',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: Color(0xFF9CA3AF),
height: 1.5,
),
),
],
),
),
],
),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 3,
type: BottomNavigationBarType.fixed,
onTap: (index) {
switch (index) {
case 0:
Navigator.popUntil(context, (route) => route.isFirst);
break;
case 1:
Navigator.pushNamed(context, '/products');
break;
case 2:
Navigator.pushNamed(context, '/transaction');
break;
case 4:
Navigator.pushNamed(context, '/reports');
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Transaksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
],
),
);
}
}

View File

@ -1,637 +0,0 @@
import 'package:flutter/material.dart';
import 'package:finalproject/models/product_model.dart';
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
import 'package:finalproject/services/ml_service.dart';
class ProductListScreen extends StatefulWidget {
const ProductListScreen({Key? key}) : super(key: key);
@override
State<ProductListScreen> createState() => _ProductListScreenState();
}
class _ProductListScreenState extends State<ProductListScreen> {
String _selectedFilter = 'semua';
String _searchQuery = '';
bool _isLoading = true;
late List<Product> products = [];
@override
void initState() {
super.initState();
_loadProducts();
}
Future<void> _loadProducts() async {
try {
final fetchedProducts = await MLService.getProducts();
// Determine stock status based on quantity
String _getStatus(int stock) {
if (stock == 0) return 'kritis';
if (stock <= 5) return 'rendah';
return 'tersedia';
}
final productList = fetchedProducts.map((p) {
int stock = p['current_stock'] ?? 0;
return Product(
id: p['id'] ?? 0,
name: p['name'] ?? '',
category: p['category'] ?? '',
price: p['price'] ?? 0,
stock: stock,
status: _getStatus(stock),
);
}).toList();
setState(() {
products = productList;
_isLoading = false;
});
} catch (e) {
print('Error loading products: $e');
setState(() => _isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Gagal memuat data produk'),
backgroundColor: AppColors.statusError,
),
);
}
}
}
List<Product> get filteredProducts {
List<Product> result = products;
// Filter by status
if (_selectedFilter != 'semua') {
result = result.where((p) => p.status == _selectedFilter).toList();
}
// Filter by search
if (_searchQuery.isNotEmpty) {
result = result
.where((p) =>
p.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
p.category.toLowerCase().contains(_searchQuery.toLowerCase()))
.toList();
}
return result;
}
Color _getStatusColor(String status) {
switch (status) {
case 'tersedia':
return const Color(0xFF10B981);
case 'rendah':
return const Color(0xFFFB923C);
case 'kritis':
return const Color(0xFFDC2626);
default:
return const Color(0xFF9CA3AF);
}
}
IconData _getCategoryIcon(String category) {
switch (category) {
case 'Tepung':
return Icons.grain;
case 'Telur':
return Icons.circle;
case 'Gula':
return Icons.blur_circular;
case 'Susu':
return Icons.local_drink;
case 'Cokelat':
return Icons.square_rounded;
case 'Mentega':
return Icons.spa;
case 'Keju':
return Icons.lunch_dining;
case 'Bahan Tambahan':
return Icons.miscellaneous_services;
default:
return Icons.shopping_bag;
}
}
String _formatPrice(int price) {
return 'Rp ${(price ~/ 1000).toString()}K';
}
int _getMaxStock() {
return products.fold<int>(0, (max, p) => p.stock > max ? p.stock : max);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgLight,
// Brown Header with Search
appBar: PreferredSize(
preferredSize: const Size.fromHeight(160),
child: Container(
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: SafeArea(
child: Column(
children: [
// Top: Back button & Title
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.arrow_back,
color: Colors.white,
size: 20,
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Data Produk',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
'${products.length} Produk',
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
],
),
),
// Bottom: Search Bar (dalam brown area)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: AppColors.bgWhite,
width: 1,
),
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: TextField(
onChanged: (value) {
setState(() => _searchQuery = value);
},
decoration: InputDecoration(
hintText: 'Cari produk...',
hintStyle: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
prefixIcon: Icon(
Icons.search,
color: AppColors.textSecondary,
size: 20,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
),
),
),
),
],
),
),
),
),
body: RefreshIndicator(
onRefresh: _loadProducts,
color: AppColors.primaryBrown,
child: _isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFF8B6E58),
),
),
SizedBox(height: 16),
Text('Memuat data produk...'),
],
),
)
: SingleChildScrollView(
child: Column(
children: [
// Filter Chips Section (moved to body)
Container(
color: AppColors.bgWhite,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
'semua',
'tersedia',
'rendah',
'kritis'
]
.map((filter) => Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(
_capitalize(filter),
style: AppTextStyles.labelSmall.copyWith(
color: _selectedFilter == filter
? Colors.white
: AppColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
backgroundColor: _selectedFilter == filter
? AppColors.primaryBrown
: AppColors.bgLight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: _selectedFilter == filter
? AppColors.primaryBrown
: AppColors.grey200,
width: 1,
),
),
onSelected: (selected) {
setState(() => _selectedFilter = filter);
},
),
))
.toList(),
),
),
),
// Products List
Padding(
padding: const EdgeInsets.all(16),
child: filteredProducts.isNotEmpty
? Column(
children: filteredProducts
.map((product) => _buildProductCard(product))
.toList()
.expand((card) =>
[card, const SizedBox(height: 12)])
.toList(),
)
: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 60),
child: Column(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.grey200,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.shopping_bag_outlined,
size: 40,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 16),
Text(
'Produk Tidak Ditemukan',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'Coba ubah filter atau cari dengan kata kunci lain',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
],
),
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 1,
type: BottomNavigationBarType.fixed,
onTap: (index) {
switch (index) {
case 0:
Navigator.popUntil(context, (route) => route.isFirst);
break;
case 2:
Navigator.pushNamed(context, '/transaction');
break;
case 3:
Navigator.pushNamed(context, '/prediction');
break;
case 4:
Navigator.pushNamed(context, '/reports');
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Transaksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
],
),
);
}
Widget _buildProductCard(Product product) {
final maxStock = _getMaxStock();
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],
),
child: Column(
children: [
// Top Row: Icon & Info
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Product Icon (Category-based)
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: _getStatusColor(product.status).withOpacity(0.12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
_getCategoryIcon(product.category),
color: _getStatusColor(product.status),
size: 28,
),
),
const SizedBox(width: 14),
// Product Info (Middle)
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(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: 8),
Text(
_formatPrice(product.price),
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
),
// Status Badge (Right)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: _getStatusColor(product.status).withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _getStatusColor(product.status).withOpacity(0.3),
width: 1,
),
),
child: Text(
_getStatusLabel(product.status),
style: AppTextStyles.labelSmall.copyWith(
color: _getStatusColor(product.status),
fontWeight: FontWeight.w700,
),
),
),
],
),
const SizedBox(height: 12),
// Divider
Container(
height: 1,
color: AppColors.grey200,
),
const SizedBox(height: 12),
// Bottom Row: Stock Info
Row(
children: [
// Stock Display
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} kg',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(width: 16),
// Stock Progress Bar
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>(
_getStatusColor(product.status),
),
),
),
],
),
),
const SizedBox(width: 12),
// Action Button
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,
),
),
),
],
),
],
),
);
}
String _getStatusLabel(String status) {
switch (status) {
case 'tersedia':
return '✅ Tersedia';
case 'rendah':
return '⚠️ Rendah';
case 'kritis':
return '🔴 Kritis';
default:
return 'Unknown';
}
}
String _capitalize(String text) {
return "${text[0].toUpperCase()}${text.substring(1)}";
}
}

View File

@ -0,0 +1,139 @@
import 'package:finalproject/models/product_model.dart';
import 'package:finalproject/services/ml_service.dart';
import 'package:flutter/material.dart';
class ProductListController extends ChangeNotifier {
String selectedFilter = 'semua';
String searchQuery = '';
bool isLoading = true;
List<Product> products = [];
Future<void> loadProducts() async {
isLoading = true;
notifyListeners();
try {
final fetchedProducts = await MLService.getProducts();
String getStatus(int stock) {
if (stock == 0) return 'kritis';
if (stock <= 5) return 'rendah';
return 'tersedia';
}
products =
fetchedProducts.map((p) {
final stock = p['current_stock'] ?? 0;
final category = p['category'] ?? '';
final unit =
p['unit'] ??
(category.toString().toLowerCase() == 'barang' ? 'pcs' : 'kg');
return Product(
id: p['id'] ?? 0,
name: p['name'] ?? '',
category: category,
price: p['price'] ?? 0,
stock: stock,
unit: unit,
status: getStatus(stock),
);
}).toList();
} finally {
isLoading = false;
notifyListeners();
}
}
List<Product> get filteredProducts {
var result = products;
if (selectedFilter != 'semua') {
result = result.where((p) => p.status == selectedFilter).toList();
}
if (searchQuery.isNotEmpty) {
result =
result
.where(
(p) =>
p.name.toLowerCase().contains(searchQuery.toLowerCase()) ||
p.category.toLowerCase().contains(
searchQuery.toLowerCase(),
),
)
.toList();
}
return result;
}
void setSearchQuery(String value) {
searchQuery = value;
notifyListeners();
}
void setFilter(String value) {
selectedFilter = value;
notifyListeners();
}
int get maxStock {
if (products.isEmpty) return 1;
return products.fold<int>(0, (max, p) => p.stock > max ? p.stock : max);
}
Color getStatusColor(String status) {
switch (status) {
case 'tersedia':
return const Color(0xFF10B981);
case 'rendah':
return const Color(0xFFFB923C);
case 'kritis':
return const Color(0xFFDC2626);
default:
return const Color(0xFF9CA3AF);
}
}
IconData getCategoryIcon(String category) {
switch (category) {
case 'Tepung':
return Icons.grain;
case 'Telur':
return Icons.circle;
case 'Gula':
return Icons.blur_circular;
case 'Susu':
return Icons.local_drink;
case 'Cokelat':
return Icons.square_rounded;
case 'Mentega':
return Icons.spa;
case 'Keju':
return Icons.lunch_dining;
case 'Bahan Tambahan':
return Icons.miscellaneous_services;
default:
return Icons.shopping_bag;
}
}
String formatPrice(int price) => 'Rp ${(price ~/ 1000)}K';
String getStatusLabel(String status) {
switch (status) {
case 'tersedia':
return '✅ Tersedia';
case 'rendah':
return '⚠️ Rendah';
case 'kritis':
return '🔴 Kritis';
default:
return 'Unknown';
}
}
String capitalize(String text) =>
'${text[0].toUpperCase()}${text.substring(1)}';
}

View File

@ -0,0 +1,552 @@
import 'package:finalproject/models/product_model.dart';
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
import 'package:flutter/material.dart';
import 'product_list_controller.dart';
class ProductListScreen extends StatefulWidget {
const ProductListScreen({super.key});
@override
State<ProductListScreen> createState() => _ProductListScreenState();
}
class _ProductListScreenState extends State<ProductListScreen> {
late final ProductListController _controller;
@override
void initState() {
super.initState();
_controller = ProductListController();
_controller.loadProducts().catchError((_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Gagal memuat data produk'),
backgroundColor: AppColors.statusError,
),
);
});
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Scaffold(
backgroundColor: AppColors.bgLight,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(160),
child: Container(
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.arrow_back,
color: Colors.white,
size: 20,
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Data Produk',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
'${_controller.products.length} Produk',
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: AppColors.bgWhite,
width: 1,
),
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: TextField(
onChanged: _controller.setSearchQuery,
decoration: InputDecoration(
hintText: 'Cari produk...',
hintStyle: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
prefixIcon: Icon(
Icons.search,
color: AppColors.textSecondary,
size: 20,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
),
),
),
),
],
),
),
),
),
body: RefreshIndicator(
onRefresh: _controller.loadProducts,
color: AppColors.primaryBrown,
child:
_controller.isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFF8B6E58),
),
),
SizedBox(height: 16),
Text('Memuat data produk...'),
],
),
)
: SingleChildScrollView(
child: Column(
children: [
Container(
color: AppColors.bgWhite,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children:
['semua', 'tersedia', 'rendah', 'kritis']
.map(
(filter) => Padding(
padding: const EdgeInsets.only(
right: 8,
),
child: FilterChip(
label: Text(
_controller.capitalize(filter),
style: AppTextStyles.labelSmall
.copyWith(
color:
_controller.selectedFilter ==
filter
? Colors.white
: AppColors
.textSecondary,
fontWeight:
FontWeight.w600,
),
),
backgroundColor:
_controller.selectedFilter ==
filter
? AppColors.primaryBrown
: AppColors.bgLight,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(8),
side: BorderSide(
color:
_controller.selectedFilter ==
filter
? AppColors
.primaryBrown
: AppColors.grey200,
width: 1,
),
),
onSelected:
(_) => _controller.setFilter(
filter,
),
),
),
)
.toList(),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child:
_controller.filteredProducts.isNotEmpty
? Column(
children:
_controller.filteredProducts
.map(
(product) =>
_buildProductCard(product),
)
.toList()
.expand(
(card) => [
card,
const SizedBox(height: 12),
],
)
.toList(),
)
: Center(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 60,
),
child: Column(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.grey200,
borderRadius:
BorderRadius.circular(20),
),
child: Icon(
Icons.shopping_bag_outlined,
size: 40,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 16),
Text(
'Produk Tidak Ditemukan',
style: AppTextStyles.headlineSmall
.copyWith(
color:
AppColors.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'Coba ubah filter atau cari dengan kata kunci lain',
style: AppTextStyles.bodySmall
.copyWith(
color:
AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
],
),
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 1,
type: BottomNavigationBarType.fixed,
onTap: (index) {
switch (index) {
case 0:
Navigator.popUntil(context, (route) => route.isFirst);
break;
case 2:
Navigator.pushNamed(context, '/transaction');
break;
case 3:
Navigator.pushNamed(context, '/prediction');
break;
case 4:
Navigator.pushNamed(context, '/reports');
break;
case 5:
Navigator.pushNamed(context, '/settings');
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Stock In',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
label: 'Pengaturan',
),
],
),
);
},
);
}
Widget _buildProductCard(Product product) {
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],
),
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
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: 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)
.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,
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),
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,
),
),
),
],
),
],
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

View File

@ -1,468 +0,0 @@
import 'package:flutter/material.dart';
class ReportScreen extends StatefulWidget {
const ReportScreen({Key? key}) : super(key: key);
@override
State<ReportScreen> createState() => _ReportScreenState();
}
class _ReportScreenState extends State<ReportScreen> {
String _selectedPeriod = 'bulanan';
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: Color(0xFFA89080),
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'Laporan & Analitik',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
centerTitle: false,
),
body: SingleChildScrollView(
child: Column(
children: [
// Period Filter
Container(
color: Colors.white,
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text(
'Period:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(width: 8),
Container(
decoration: BoxDecoration(
border: Border.all(
color: Color(0xFFE5E7EB),
width: 1,
),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedPeriod,
items: [
'harian',
'mingguan',
'bulanan',
'tahunan'
]
.map((period) => DropdownMenuItem(
value: period,
child: Text(period.capitalize()),
))
.toList(),
onChanged: (value) {
if (value != null) {
setState(() => _selectedPeriod = value);
}
},
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Chart Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Grafik Penjualan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 2),
Text(
'6 ${_selectedPeriod}',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF9CA3AF),
),
),
],
),
Icon(
Icons.trending_up,
color: Color(0xFF10B981),
size: 20,
),
],
),
const SizedBox(height: 20),
// Chart Placeholder
Container(
height: 200,
decoration: BoxDecoration(
color: Color(0xFFF9FAFB),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'[Area Chart: Sales Data by ${_selectedPeriod.capitalize()}]',
style: TextStyle(
color: Color(0xFFD1D5DB),
fontSize: 12,
),
),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun']
.map((month) => Text(
month,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF9CA3AF),
),
))
.toList(),
),
],
),
),
const SizedBox(height: 16),
// Statistics Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Statistik Penjualan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 16),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: [
_buildStatItem(
icon: Icons.shopping_bag,
label: 'Total Transaksi',
value: '150',
color: Color(0xFF2563EB),
),
_buildStatItem(
icon: Icons.attach_money,
label: 'Total Penjualan',
value: 'Rp 2.5M',
color: Color(0xFF10B981),
),
_buildStatItem(
icon: Icons.trending_up,
label: 'Rata-rata',
value: 'Rp 16K',
color: Color(0xFFFB923C),
),
_buildStatItem(
icon: Icons.inventory_2,
label: 'Produk Terjual',
value: '8',
color: Color(0xFFA89080),
),
],
),
],
),
),
const SizedBox(height: 16),
// Top Products Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Top 5 Produk Terlaris',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 16),
...[
('Tepung Terigu', 25),
('Gula Pasir', 18),
('Telur', 15),
('Mentega', 12),
('Cokelat Bubuk', 10),
]
.asMap()
.entries
.map((entry) =>
_buildTopProductItem(
rank: entry.key + 1,
name: entry.value.$1,
quantity: entry.value.$2,
))
.toList()
.expand((item) => [item, SizedBox(height: 12)])
.toList(),
],
),
),
const SizedBox(height: 24),
],
),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 4,
type: BottomNavigationBarType.fixed,
onTap: (index) {
switch (index) {
case 0:
Navigator.popUntil(context, (route) => route.isFirst);
break;
case 1:
Navigator.pushNamed(context, '/products');
break;
case 2:
Navigator.pushNamed(context, '/transaction');
break;
case 3:
Navigator.pushNamed(context, '/prediction');
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Transaksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
],
),
);
}
Widget _buildStatItem({
required IconData icon,
required String label,
required String value,
required Color color,
}) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 18),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: color,
),
),
SizedBox(height: 2),
Text(
label,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF9CA3AF),
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
Widget _buildTopProductItem({
required int rank,
required String name,
required int quantity,
}) {
return Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Color(0xFFA89080),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
rank.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
Text(
'$quantity unit',
style: const TextStyle(
fontSize: 11,
color: Color(0xFF9CA3AF),
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Color(0xFFA89080).withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${((quantity / 25) * 100).toStringAsFixed(0)}%',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFFA89080),
),
),
),
],
);
}
}
extension StringExtension on String {
String capitalize() {
return "${this[0].toUpperCase()}${substring(1)}";
}
}

View File

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
class ReportController extends ChangeNotifier {
String selectedPeriod = 'bulanan';
final List<String> periods = ['harian', 'mingguan', 'bulanan', 'tahunan'];
final List<(String, int)> topProducts = const [
('Tepung Terigu', 25),
('Gula Pasir', 18),
('Telur', 15),
('Mentega', 12),
('Cokelat Bubuk', 10),
];
void setSelectedPeriod(String value) {
selectedPeriod = value;
notifyListeners();
}
String capitalize(String value) =>
'${value[0].toUpperCase()}${value.substring(1)}';
}

View File

@ -0,0 +1,473 @@
import 'package:flutter/material.dart';
import 'report_controller.dart';
class ReportScreen extends StatefulWidget {
const ReportScreen({super.key});
@override
State<ReportScreen> createState() => _ReportScreenState();
}
class _ReportScreenState extends State<ReportScreen> {
late final ReportController _controller;
@override
void initState() {
super.initState();
_controller = ReportController();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: const Color(0xFFA89080),
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'Laporan & Analitik',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
centerTitle: false,
),
body: SingleChildScrollView(
child: Column(
children: [
Container(
color: Colors.white,
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Text(
'Period:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(width: 8),
Container(
decoration: BoxDecoration(
border: Border.all(
color: const Color(0xFFE5E7EB),
width: 1,
),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _controller.selectedPeriod,
items:
_controller.periods
.map(
(period) => DropdownMenuItem(
value: period,
child: Text(
_controller.capitalize(period),
),
),
)
.toList(),
onChanged: (value) {
if (value != null) {
_controller.setSelectedPeriod(value);
}
},
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Grafik Penjualan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 2),
Text(
'6 ${_controller.selectedPeriod}',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF9CA3AF),
),
),
],
),
const Icon(
Icons.trending_up,
color: Color(0xFF10B981),
size: 20,
),
],
),
const SizedBox(height: 20),
Container(
height: 200,
decoration: BoxDecoration(
color: const Color(0xFFF9FAFB),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'[Area Chart: Sales Data by ${_controller.capitalize(_controller.selectedPeriod)}]',
style: const TextStyle(
color: Color(0xFFD1D5DB),
fontSize: 12,
),
),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun']
.map(
(month) => Text(
month,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF9CA3AF),
),
),
)
.toList(),
),
],
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Statistik Penjualan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 16),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: [
_buildStatItem(
icon: Icons.shopping_bag,
label: 'Total Stock In',
value: '150',
color: const Color(0xFF2563EB),
),
_buildStatItem(
icon: Icons.attach_money,
label: 'Total Penjualan',
value: 'Rp 2.5M',
color: const Color(0xFF10B981),
),
_buildStatItem(
icon: Icons.trending_up,
label: 'Rata-rata',
value: 'Rp 16K',
color: const Color(0xFFFB923C),
),
_buildStatItem(
icon: Icons.inventory_2,
label: 'Produk Terjual',
value: '8',
color: const Color(0xFFA89080),
),
],
),
],
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Top 5 Produk Terlaris',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 16),
..._controller.topProducts
.asMap()
.entries
.map(
(entry) => _buildTopProductItem(
rank: entry.key + 1,
name: entry.value.$1,
quantity: entry.value.$2,
),
)
.toList()
.expand(
(item) => [item, const SizedBox(height: 12)],
)
.toList(),
],
),
),
const SizedBox(height: 24),
],
),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 4,
type: BottomNavigationBarType.fixed,
onTap: (index) {
switch (index) {
case 0:
Navigator.popUntil(context, (route) => route.isFirst);
break;
case 1:
Navigator.pushNamed(context, '/products');
break;
case 2:
Navigator.pushNamed(context, '/transaction');
break;
case 3:
Navigator.pushNamed(context, '/prediction');
break;
case 5:
Navigator.pushNamed(context, '/settings');
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Stock In',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
label: 'Pengaturan',
),
],
),
);
},
);
}
Widget _buildStatItem({
required IconData icon,
required String label,
required String value,
required Color color,
}) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 18),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: color,
),
),
const SizedBox(height: 2),
Text(
label,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF9CA3AF),
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
Widget _buildTopProductItem({
required int rank,
required String name,
required int quantity,
}) {
return Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: const Color(0xFFA89080),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
rank.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
Text(
'$quantity unit',
style: const TextStyle(fontSize: 11, color: Color(0xFF9CA3AF)),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFA89080).withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${((quantity / 25) * 100).toStringAsFixed(0)}%',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFFA89080),
),
),
),
],
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
class SettingsController extends ChangeNotifier {
int selectedIndex = 5;
bool notificationEnabled = true;
void setSelectedIndex(int index) {
selectedIndex = index;
notifyListeners();
}
void setNotificationEnabled(bool value) {
notificationEnabled = value;
notifyListeners();
}
}

View File

@ -0,0 +1,420 @@
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
import 'package:flutter/material.dart';
import 'settings_controller.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
late final SettingsController _controller;
@override
void initState() {
super.initState();
_controller = SettingsController();
}
void _showLogoutDialog() {
showDialog(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Text(
'Logout',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
content: Text(
'Apakah Anda yakin ingin keluar dari aplikasi?',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.textSecondary,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Batal',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textSecondary,
),
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
Navigator.of(
context,
).pushNamedAndRemoveUntil('/login', (route) => false);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.statusError,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Logout',
style: AppTextStyles.labelLarge.copyWith(color: Colors.white),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Scaffold(
backgroundColor: AppColors.bgLight,
appBar: AppBar(
backgroundColor: AppColors.primaryBrown,
elevation: 0,
title: Text(
'Pengaturan',
style: AppTextStyles.headlineLarge.copyWith(color: Colors.white),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Akun',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 32,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Admin Sulastri',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
'admin@sulastri.com',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
),
],
),
),
const SizedBox(height: 32),
Text(
'Umum',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(12),
boxShadow: [AppColors.shadowLight],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.language,
color: AppColors.primaryBrown,
size: 24,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bahasa',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
Text(
'Bahasa Indonesia',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
],
),
Icon(
Icons.chevron_right,
color: AppColors.textSecondary,
),
],
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(12),
boxShadow: [AppColors.shadowLight],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.notifications_outlined,
color: AppColors.primaryBrown,
size: 24,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Notifikasi',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
Text(
'Aktifkan notifikasi penting',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
],
),
Switch(
value: _controller.notificationEnabled,
onChanged: _controller.setNotificationEnabled,
activeColor: AppColors.primaryBrown,
),
],
),
),
const SizedBox(height: 32),
Text(
'Keamanan',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(12),
boxShadow: [AppColors.shadowLight],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.lock_outlined,
color: AppColors.primaryBrown,
size: 24,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ubah Password',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
Text(
'Perbarui password akun Anda',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
],
),
Icon(
Icons.chevron_right,
color: AppColors.textSecondary,
),
],
),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _showLogoutDialog,
icon: const Icon(Icons.logout),
label: const Text('Logout'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.statusError,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
const SizedBox(height: 32),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.grey300),
),
child: Column(
children: [
Text(
'Prediksi Stok Bahan Kue',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
'Version 1.0.0',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
const SizedBox(height: 12),
Text(
'© 2025 Toko Bahan Kue Sulastri',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
textAlign: TextAlign.center,
),
],
),
),
],
),
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _controller.selectedIndex,
onTap: (index) {
_controller.setSelectedIndex(index);
switch (index) {
case 0:
Navigator.of(context).pushNamed('/dashboard');
break;
case 1:
Navigator.of(context).pushNamed('/products');
break;
case 2:
Navigator.of(context).pushNamed('/transaction');
break;
case 3:
Navigator.of(context).pushNamed('/prediction');
break;
case 4:
Navigator.of(context).pushNamed('/reports');
break;
case 5:
break;
}
},
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Stock In',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
label: 'Pengaturan',
),
],
),
);
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

View File

@ -1,412 +0,0 @@
import 'package:flutter/material.dart';
import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({Key? key}) : super(key: key);
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
int _selectedIndex = 5; // Settings tab
void _showLogoutDialog() {
showDialog(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Text(
'Logout',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
content: Text(
'Apakah Anda yakin ingin keluar dari aplikasi?',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.textSecondary,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Batal',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textSecondary,
),
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
Navigator.of(
context,
).pushNamedAndRemoveUntil('/login', (route) => false);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.statusError,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Logout',
style: AppTextStyles.labelLarge.copyWith(color: Colors.white),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgLight,
appBar: AppBar(
backgroundColor: AppColors.primaryBrown,
elevation: 0,
title: Text(
'Pengaturan',
style: AppTextStyles.headlineLarge.copyWith(color: Colors.white),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Account Section
Text(
'Akun',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 16),
// User Profile Card
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(16),
boxShadow: [AppColors.shadowLight],
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppColors.primaryBrown,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 32,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Admin Sulastri',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
'admin@sulastri.com',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
),
],
),
),
const SizedBox(height: 32),
// General Settings Section
Text(
'Umum',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 16),
// Settings Item: Language
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(12),
boxShadow: [AppColors.shadowLight],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.language,
color: AppColors.primaryBrown,
size: 24,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bahasa',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
Text(
'Bahasa Indonesia',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
],
),
Icon(Icons.chevron_right, color: AppColors.textSecondary),
],
),
),
const SizedBox(height: 12),
// Settings Item: Notifikasi
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(12),
boxShadow: [AppColors.shadowLight],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.notifications_outlined,
color: AppColors.primaryBrown,
size: 24,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Notifikasi',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
Text(
'Aktifkan notifikasi penting',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
],
),
Switch(
value: true,
onChanged: (value) {},
activeColor: AppColors.primaryBrown,
),
],
),
),
const SizedBox(height: 32),
// Security Section
Text(
'Keamanan',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 16),
// Settings Item: Change Password
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgWhite,
borderRadius: BorderRadius.circular(12),
boxShadow: [AppColors.shadowLight],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.lock_outlined,
color: AppColors.primaryBrown,
size: 24,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ubah Password',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
),
),
Text(
'Perbarui password akun Anda',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
],
),
Icon(Icons.chevron_right, color: AppColors.textSecondary),
],
),
),
const SizedBox(height: 32),
// Logout Button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _showLogoutDialog,
icon: const Icon(Icons.logout),
label: const Text('Logout'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.statusError,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
const SizedBox(height: 32),
// App Info
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.grey300),
),
child: Column(
children: [
Text(
'Prediksi Stok Bahan Kue',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
'Version 1.0.0',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
const SizedBox(height: 12),
Text(
'© 2025 Toko Bahan Kue Sulastri',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
textAlign: TextAlign.center,
),
],
),
),
],
),
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() => _selectedIndex = index);
switch (index) {
case 0:
Navigator.of(context).pushNamed('/dashboard');
break;
case 1:
Navigator.of(context).pushNamed('/products');
break;
case 2:
Navigator.of(context).pushNamed('/transaction');
break;
case 3:
Navigator.of(context).pushNamed('/prediction');
break;
case 4:
Navigator.of(context).pushNamed('/reports');
break;
case 5:
break;
}
},
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Produk',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Transaksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up_outlined),
label: 'Prediksi',
),
BottomNavigationBarItem(
icon: Icon(Icons.description_outlined),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
label: 'Pengaturan',
),
],
),
);
}
}

View File

@ -0,0 +1,258 @@
import 'package:finalproject/models/transaction_model.dart';
import 'package:finalproject/services/ml_service.dart';
import 'package:flutter/material.dart';
class CartItem {
final int productId;
final String productName;
final String category;
final int unitPrice;
int quantity;
CartItem({
required this.productId,
required this.productName,
required this.category,
required this.unitPrice,
required this.quantity,
});
int get totalPrice => unitPrice * quantity;
}
class TransactionController extends ChangeNotifier {
final quantityController = TextEditingController();
DateTime? selectedDate = DateTime.now();
bool isLoading = false;
int selectedIndex = 2;
final List<String> products = [];
final Map<String, int> productIds = {};
final Map<String, String> productCategories = {};
final Map<String, String> productUnits = {};
final Map<String, int> productPrices = {};
final List<String> categories = [];
String? selectedProduct;
final List<CartItem> cartItems = [];
final List<Transaction> transactions = [];
Future<void> loadProducts() async {
final fetchedProducts = await MLService.getProducts();
products.clear();
productIds.clear();
productCategories.clear();
productUnits.clear();
productPrices.clear();
categories.clear();
for (final product in fetchedProducts) {
final id = product['id'] ?? 0;
final name = product['name'] ?? '';
final category = product['category'] ?? '';
final unit = product['unit'] ?? _defaultUnitFromCategory(category);
final price = product['price'] ?? 0;
if (name.isNotEmpty && id > 0) {
products.add(name);
productIds[name] = id;
productCategories[name] = category;
productUnits[name] = unit;
productPrices[name] = price;
if (!categories.contains(category)) {
categories.add(category);
}
}
}
notifyListeners();
}
void setSelectedProduct(String? value) {
selectedProduct = value;
notifyListeners();
}
void setSelectedDate(DateTime value) {
selectedDate = value;
notifyListeners();
}
void setSelectedIndex(int index) {
selectedIndex = index;
notifyListeners();
}
String? addToCart() {
if (selectedProduct == null || quantityController.text.isEmpty) {
return 'Pilih produk dan masukkan jumlah';
}
final quantity = int.tryParse(quantityController.text) ?? 0;
if (quantity <= 0) {
return 'Jumlah harus lebih dari 0';
}
final existingIndex = cartItems.indexWhere(
(item) => item.productName == selectedProduct,
);
if (existingIndex >= 0) {
cartItems[existingIndex].quantity += quantity;
} else {
cartItems.add(
CartItem(
productId: productIds[selectedProduct!] ?? 0,
productName: selectedProduct!,
category: productCategories[selectedProduct!]!,
unitPrice: productPrices[selectedProduct!]!,
quantity: quantity,
),
);
}
quantityController.clear();
notifyListeners();
return null;
}
void removeFromCart(int index) {
cartItems.removeAt(index);
notifyListeners();
}
void updateQuantity(int index, int newQuantity) {
if (newQuantity <= 0) {
removeFromCart(index);
return;
}
cartItems[index].quantity = newQuantity;
notifyListeners();
}
void clearCart() {
cartItems.clear();
notifyListeners();
}
int get totalPrice =>
cartItems.fold(0, (total, item) => total + item.totalPrice);
Future<Map<String, dynamic>> submitAllTransactions() async {
if (cartItems.isEmpty) {
return {'status': 'error', 'message': 'Keranjang kosong'};
}
isLoading = true;
notifyListeners();
try {
final date = selectedDate ?? DateTime.now();
final dateStr =
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
int successCount = 0;
String? firstError;
final originalCount = cartItems.length;
for (final item in cartItems) {
final result = await MLService.addTransactionWithStockUpdate(
productId: item.productId,
quantity: item.quantity,
unitPrice: item.unitPrice,
totalPrice: item.totalPrice,
transactionDate: dateStr,
);
if (result['status'] == 'success') {
successCount++;
transactions.insert(
0,
Transaction(
id: transactions.length + 1,
productName: item.productName,
category: item.category,
quantity: item.quantity,
unitPrice: item.unitPrice,
totalPrice: item.totalPrice,
date: date,
),
);
} else {
firstError ??= result['message'] ?? 'Transaksi gagal';
}
}
cartItems.clear();
selectedDate = DateTime.now();
return {
'status': 'success',
'successCount': successCount,
'totalCount': originalCount,
'error': firstError,
};
} catch (e) {
return {'status': 'error', 'message': 'Error: ${e.toString()}'};
} finally {
isLoading = false;
notifyListeners();
}
}
Future<Map<String, dynamic>> createProduct({
required String productName,
required String category,
required int price,
required int initialStock,
required String unit,
required String productType,
}) async {
if (products.contains(productName)) {
return {'status': 'error', 'message': 'Produk "$productName" sudah ada'};
}
final result = await MLService.createProduct(
name: productName,
category: category,
price: price,
currentStock: initialStock,
unit: unit,
productType: productType,
);
if (result['status'] == 'success') {
final createdProductId = result['product_id'];
products.add(productName);
if (createdProductId is int && createdProductId > 0) {
productIds[productName] = createdProductId;
}
productCategories[productName] = category;
productUnits[productName] = unit;
productPrices[productName] = price;
if (!categories.contains(category)) {
categories.add(category);
}
notifyListeners();
}
return result;
}
@override
void dispose() {
quantityController.dispose();
super.dispose();
}
static String _defaultUnitFromCategory(String category) {
if (category.toLowerCase() == 'barang') {
return 'pcs';
}
return 'kg';
}
}

File diff suppressed because it is too large Load Diff

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.110.16:5000';
static const int timeoutSeconds = 30;
@ -215,6 +215,8 @@ class MLService {
required String category,
required int price,
required int currentStock,
String? unit,
String? productType,
}) async {
try {
final data = {
@ -222,6 +224,8 @@ class MLService {
'category': category,
'price': price,
'current_stock': currentStock,
if (unit != null) 'unit': unit,
if (productType != null) 'product_type': productType,
};
final response = await http

View File

@ -308,7 +308,9 @@ def create_product():
"name": "Tepung Terigu 1kg",
"category": "Tepung",
"price": 15000,
"current_stock": 50
"current_stock": 50,
"unit": "kg",
"product_type": "Bahan"
}
"""
try:
@ -331,6 +333,15 @@ def create_product():
cursor = connection.cursor()
# Optional columns compatibility (works for old/new schemas)
cursor.execute("SHOW COLUMNS FROM products LIKE 'unit'")
has_unit_column = cursor.fetchone() is not None
cursor.execute("SHOW COLUMNS FROM products LIKE 'product_type'")
has_product_type_column = cursor.fetchone() is not None
unit_value = data.get('unit')
product_type_value = data.get('product_type')
# Check for duplicate product name
cursor.execute("SELECT id FROM products WHERE name = %s", (data['name'],))
if cursor.fetchone():
@ -342,16 +353,54 @@ def create_product():
}), 409
# Insert new product
cursor.execute("""
INSERT INTO products
(name, category, price, current_stock)
VALUES (%s, %s, %s, %s)
""", (
data['name'],
data['category'],
data['price'],
data['current_stock']
))
if has_unit_column and has_product_type_column:
cursor.execute("""
INSERT INTO products
(name, category, price, current_stock, unit, product_type)
VALUES (%s, %s, %s, %s, %s, %s)
""", (
data['name'],
data['category'],
data['price'],
data['current_stock'],
unit_value,
product_type_value
))
elif has_unit_column:
cursor.execute("""
INSERT INTO products
(name, category, price, current_stock, unit)
VALUES (%s, %s, %s, %s, %s)
""", (
data['name'],
data['category'],
data['price'],
data['current_stock'],
unit_value
))
elif has_product_type_column:
cursor.execute("""
INSERT INTO products
(name, category, price, current_stock, product_type)
VALUES (%s, %s, %s, %s, %s)
""", (
data['name'],
data['category'],
data['price'],
data['current_stock'],
product_type_value
))
else:
cursor.execute("""
INSERT INTO products
(name, category, price, current_stock)
VALUES (%s, %s, %s, %s)
""", (
data['name'],
data['category'],
data['price'],
data['current_stock']
))
connection.commit()
product_id = cursor.lastrowid

View File

@ -10,6 +10,8 @@ CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
category VARCHAR(100) NOT NULL,
product_type VARCHAR(20) DEFAULT 'Bahan',
unit VARCHAR(20) DEFAULT 'kg',
price INT NOT NULL,
current_stock INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -62,15 +64,15 @@ CREATE TABLE IF NOT EXISTS recipe_ingredients (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Insert Initial Products (8 items)
INSERT INTO products (name, category, price, current_stock) VALUES
('Tepung Terigu 1kg', 'Tepung', 15000, 50),
('Telur 1kg', 'Telur', 25000, 30),
('Gula Pasir 1kg', 'Gula', 12000, 40),
('Susu Bubuk', 'Susu', 20000, 20),
('Cokelat Bubuk 250gr', 'Cokelat', 18000, 15),
('Mentega 500gr', 'Mentega', 22000, 25),
('Keju Parut 250gr', 'Keju', 28000, 10),
('Baking Powder', 'Bahan Tambahan', 8000, 35);
INSERT INTO products (name, category, product_type, unit, price, current_stock) VALUES
('Tepung Terigu 1kg', 'Tepung', 'Bahan', 'kg', 15000, 50),
('Telur 1kg', 'Telur', 'Bahan', 'kg', 25000, 30),
('Gula Pasir 1kg', 'Gula', 'Bahan', 'kg', 12000, 40),
('Susu Bubuk', 'Susu', 'Bahan', 'kg', 20000, 20),
('Cokelat Bubuk 250gr', 'Cokelat', 'Bahan', 'kg', 18000, 15),
('Mentega 500gr', 'Mentega', 'Bahan', 'kg', 22000, 25),
('Keju Parut 250gr', 'Keju', 'Bahan', 'kg', 28000, 10),
('Baking Powder', 'Bahan Tambahan', 'Bahan', 'kg', 8000, 35);
-- Insert Sample Recipes
INSERT INTO recipes (recipe_name, description) VALUES