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:flutter/material.dart';
|
||||||
import 'package:finalproject/theme/app_theme.dart';
|
import 'package:finalproject/theme/app_theme.dart';
|
||||||
import 'package:finalproject/screens/splash_screen.dart';
|
import 'package:finalproject/screens/splash_screen.dart';
|
||||||
import 'package:finalproject/screens/login_screen.dart';
|
import 'package:finalproject/screens/login/login_page.dart';
|
||||||
import 'package:finalproject/screens/dashboard_screen.dart';
|
import 'package:finalproject/screens/dashboard/dashboard_page.dart';
|
||||||
import 'package:finalproject/screens/prediction_screen.dart';
|
import 'package:finalproject/screens/prediction/prediction_page.dart';
|
||||||
import 'package:finalproject/screens/transaction_screen.dart';
|
import 'package:finalproject/screens/transaction/transaction_page.dart';
|
||||||
import 'package:finalproject/screens/product_list_screen.dart';
|
import 'package:finalproject/screens/products/product_list_page.dart';
|
||||||
import 'package:finalproject/screens/report_screen.dart';
|
import 'package:finalproject/screens/reports/report_page.dart';
|
||||||
import 'package:finalproject/screens/settings_screen.dart';
|
import 'package:finalproject/screens/settings/settings_page.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ class Product {
|
||||||
final String category;
|
final String category;
|
||||||
final int price;
|
final int price;
|
||||||
final int stock;
|
final int stock;
|
||||||
|
final String unit;
|
||||||
final String status; // 'tersedia', 'rendah', 'kritis'
|
final String status; // 'tersedia', 'rendah', 'kritis'
|
||||||
|
|
||||||
Product({
|
Product({
|
||||||
|
|
@ -12,6 +13,7 @@ class Product {
|
||||||
required this.category,
|
required this.category,
|
||||||
required this.price,
|
required this.price,
|
||||||
required this.stock,
|
required this.stock,
|
||||||
|
this.unit = 'kg',
|
||||||
this.status = 'tersedia',
|
this.status = 'tersedia',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -22,6 +24,7 @@ class Product {
|
||||||
category: json['category'] as String,
|
category: json['category'] as String,
|
||||||
price: json['price'] as int,
|
price: json['price'] as int,
|
||||||
stock: json['stock'] as int,
|
stock: json['stock'] as int,
|
||||||
|
unit: (json['unit'] as String?) ?? 'kg',
|
||||||
status: json['status'] as String? ?? 'tersedia',
|
status: json['status'] as String? ?? 'tersedia',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -33,6 +36,7 @@ class Product {
|
||||||
'category': category,
|
'category': category,
|
||||||
'price': price,
|
'price': price,
|
||||||
'stock': stock,
|
'stock': stock,
|
||||||
|
'unit': unit,
|
||||||
'status': status,
|
'status': status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +47,7 @@ class Product {
|
||||||
String? category,
|
String? category,
|
||||||
int? price,
|
int? price,
|
||||||
int? stock,
|
int? stock,
|
||||||
|
String? unit,
|
||||||
String? status,
|
String? status,
|
||||||
}) {
|
}) {
|
||||||
return Product(
|
return Product(
|
||||||
|
|
@ -51,6 +56,7 @@ class Product {
|
||||||
category: category ?? this.category,
|
category: category ?? this.category,
|
||||||
price: price ?? this.price,
|
price: price ?? this.price,
|
||||||
stock: stock ?? this.stock,
|
stock: stock ?? this.stock,
|
||||||
|
unit: unit ?? this.unit,
|
||||||
status: status ?? this.status,
|
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
|
// API URL - Change based on environment
|
||||||
// Untuk emulator Android: 10.0.2.2
|
// Untuk emulator Android: 10.0.2.2
|
||||||
// Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local
|
// 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;
|
static const int timeoutSeconds = 30;
|
||||||
|
|
||||||
|
|
@ -215,6 +215,8 @@ class MLService {
|
||||||
required String category,
|
required String category,
|
||||||
required int price,
|
required int price,
|
||||||
required int currentStock,
|
required int currentStock,
|
||||||
|
String? unit,
|
||||||
|
String? productType,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final data = {
|
final data = {
|
||||||
|
|
@ -222,6 +224,8 @@ class MLService {
|
||||||
'category': category,
|
'category': category,
|
||||||
'price': price,
|
'price': price,
|
||||||
'current_stock': currentStock,
|
'current_stock': currentStock,
|
||||||
|
if (unit != null) 'unit': unit,
|
||||||
|
if (productType != null) 'product_type': productType,
|
||||||
};
|
};
|
||||||
|
|
||||||
final response = await http
|
final response = await http
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,9 @@ def create_product():
|
||||||
"name": "Tepung Terigu 1kg",
|
"name": "Tepung Terigu 1kg",
|
||||||
"category": "Tepung",
|
"category": "Tepung",
|
||||||
"price": 15000,
|
"price": 15000,
|
||||||
"current_stock": 50
|
"current_stock": 50,
|
||||||
|
"unit": "kg",
|
||||||
|
"product_type": "Bahan"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -331,6 +333,15 @@ def create_product():
|
||||||
|
|
||||||
cursor = connection.cursor()
|
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
|
# Check for duplicate product name
|
||||||
cursor.execute("SELECT id FROM products WHERE name = %s", (data['name'],))
|
cursor.execute("SELECT id FROM products WHERE name = %s", (data['name'],))
|
||||||
if cursor.fetchone():
|
if cursor.fetchone():
|
||||||
|
|
@ -342,6 +353,44 @@ def create_product():
|
||||||
}), 409
|
}), 409
|
||||||
|
|
||||||
# Insert new product
|
# Insert new product
|
||||||
|
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("""
|
cursor.execute("""
|
||||||
INSERT INTO products
|
INSERT INTO products
|
||||||
(name, category, price, current_stock)
|
(name, category, price, current_stock)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ CREATE TABLE IF NOT EXISTS products (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL UNIQUE,
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
category VARCHAR(100) NOT NULL,
|
category VARCHAR(100) NOT NULL,
|
||||||
|
product_type VARCHAR(20) DEFAULT 'Bahan',
|
||||||
|
unit VARCHAR(20) DEFAULT 'kg',
|
||||||
price INT NOT NULL,
|
price INT NOT NULL,
|
||||||
current_stock INT NOT NULL DEFAULT 0,
|
current_stock INT NOT NULL DEFAULT 0,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
@ -62,15 +64,15 @@ CREATE TABLE IF NOT EXISTS recipe_ingredients (
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- Insert Initial Products (8 items)
|
-- Insert Initial Products (8 items)
|
||||||
INSERT INTO products (name, category, price, current_stock) VALUES
|
INSERT INTO products (name, category, product_type, unit, price, current_stock) VALUES
|
||||||
('Tepung Terigu 1kg', 'Tepung', 15000, 50),
|
('Tepung Terigu 1kg', 'Tepung', 'Bahan', 'kg', 15000, 50),
|
||||||
('Telur 1kg', 'Telur', 25000, 30),
|
('Telur 1kg', 'Telur', 'Bahan', 'kg', 25000, 30),
|
||||||
('Gula Pasir 1kg', 'Gula', 12000, 40),
|
('Gula Pasir 1kg', 'Gula', 'Bahan', 'kg', 12000, 40),
|
||||||
('Susu Bubuk', 'Susu', 20000, 20),
|
('Susu Bubuk', 'Susu', 'Bahan', 'kg', 20000, 20),
|
||||||
('Cokelat Bubuk 250gr', 'Cokelat', 18000, 15),
|
('Cokelat Bubuk 250gr', 'Cokelat', 'Bahan', 'kg', 18000, 15),
|
||||||
('Mentega 500gr', 'Mentega', 22000, 25),
|
('Mentega 500gr', 'Mentega', 'Bahan', 'kg', 22000, 25),
|
||||||
('Keju Parut 250gr', 'Keju', 28000, 10),
|
('Keju Parut 250gr', 'Keju', 'Bahan', 'kg', 28000, 10),
|
||||||
('Baking Powder', 'Bahan Tambahan', 8000, 35);
|
('Baking Powder', 'Bahan Tambahan', 'Bahan', 'kg', 8000, 35);
|
||||||
|
|
||||||
-- Insert Sample Recipes
|
-- Insert Sample Recipes
|
||||||
INSERT INTO recipes (recipe_name, description) VALUES
|
INSERT INTO recipes (recipe_name, description) VALUES
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue