Alur Logika Stock It ke produk
This commit is contained in:
parent
95a703dd4e
commit
eae95fa4eb
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)}";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)}';
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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)}";
|
||||
}
|
||||
}
|
||||
|
|
@ -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)}';
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue