MIF_E31232435/lib/screens/dashboard/dashboard_page.dart

621 lines
25 KiB
Dart

import 'package:finalproject/theme/colors.dart';
import 'package:finalproject/theme/text_styles.dart';
import 'package:finalproject/utils/route_observer.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> with RouteAware {
late final DashboardController _controller;
@override
void initState() {
super.initState();
_controller = DashboardController();
_controller.loadDashboard();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of(context);
if (route != null) {
routeObserver.subscribe(this, route);
}
}
@override
void didPopNext() {
_controller.loadDashboard(showLoading: false);
}
@override
Widget build(BuildContext context) {
final topPadding = MediaQuery.of(context).padding.top;
final headerHeight = 240 + topPadding;
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Scaffold(
backgroundColor: AppColors.bgLight,
appBar: PreferredSize(
preferredSize: Size.fromHeight(headerHeight),
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.withValues(
alpha: 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: 'Bahan Digunakan Hari Ini',
value:
'${_formatNumber(_controller.totalBahanDigunakanHariIni)} ${_controller.bahanDigunakanSatuan}',
change: _controller.bahanDigunakanKeterangan,
icon: Icons.inventory_2_outlined,
iconBgColor: AppColors.statusSuccess,
isLoading: _controller.isBahanDigunakanLoading,
hasError: _controller.bahanDigunakanError != null,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
title: 'Produk',
value: _controller.totalProduk.toString(),
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(
'Penggunaan Bahan',
style: AppTextStyles.headlineSmall
.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
Text(
'Top 5 bahan paling sering digunakan',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
),
),
],
),
Icon(
Icons.bar_chart_rounded,
color: AppColors.secondaryBlue,
size: 20,
),
],
),
const SizedBox(height: 16),
if (_controller.isLoading)
const Center(child: CircularProgressIndicator())
else
Column(
children:
_controller.penggunaanBahan.map((item) {
return Padding(
padding: const EdgeInsets.only(
bottom: 12,
),
child: _buildUsageRow(item),
);
}).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,
),
),
),
],
),
);
}),
],
),
),
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: () {
Navigator.of(context).pushNamed('/transaction');
},
icon: const Icon(Icons.check_circle, size: 20),
label: const Text(
'Tambah 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: 'Stok Masuk',
),
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,
bool isLoading = false,
bool hasError = false,
}) {
return Container(
constraints: const BoxConstraints(minHeight: 120),
padding: const EdgeInsets.all(12),
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,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textTertiary,
fontWeight: FontWeight.w500,
fontSize: 11,
height: 1.2,
),
),
const SizedBox(height: 4),
if (isLoading)
const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2.4),
)
else
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
value,
maxLines: 1,
style: AppTextStyles.titleLarge.copyWith(
color:
hasError
? AppColors.textSecondary
: AppColors.textPrimary,
fontWeight: FontWeight.w700,
fontSize: 18,
height: 1.1,
),
),
),
const SizedBox(height: 4),
Text(
change,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.bodySmall.copyWith(
color:
hasError
? AppColors.statusError
: AppColors.textTertiary,
fontWeight: FontWeight.w600,
fontSize: 11,
height: 1.2,
),
),
],
),
),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: iconBgColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: Center(child: Icon(icon, color: iconBgColor, size: 22)),
),
],
),
],
),
);
}
String _formatNumber(num value) {
if (value % 1 == 0) {
return value.toInt().toString();
}
return value.toStringAsFixed(1);
}
Widget _buildUsageRow(Map<String, dynamic> item) {
final total = (item['total'] as num?)?.toDouble() ?? 0.0;
final unit = item['unit']?.toString() ?? 'kg';
final max = _controller.penggunaanBahan.fold<double>(
0,
(value, element) =>
(((element['total'] as num?)?.toDouble() ?? 0.0) > value)
? (element['total'] as num?)!.toDouble()
: value,
);
final progress = max == 0 ? 0.0 : total / max;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item['name']?.toString() ?? '-',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w600,
),
),
),
Text(
'${total.toStringAsFixed(0)} $unit',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: progress,
minHeight: 8,
backgroundColor: AppColors.grey200,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.secondaryBlue),
),
),
],
);
}
@override
void dispose() {
routeObserver.unsubscribe(this);
_controller.dispose();
super.dispose();
}
}