diff --git a/lib/screens/dashboard/dashboard_page.dart b/lib/screens/dashboard/dashboard_page.dart index cc34a62..a215d02 100644 --- a/lib/screens/dashboard/dashboard_page.dart +++ b/lib/screens/dashboard/dashboard_page.dart @@ -372,10 +372,12 @@ class _DashboardScreenState extends State { const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( - onPressed: () {}, + onPressed: () { + Navigator.of(context).pushNamed('/transaction'); + }, icon: const Icon(Icons.check_circle, size: 20), label: const Text( - 'Rekomendasi Stok', + 'Tambah Stok', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -440,7 +442,7 @@ class _DashboardScreenState extends State { ), BottomNavigationBarItem( icon: Icon(Icons.shopping_cart_outlined), - label: 'Stock In', + label: 'Stok Masuk', ), BottomNavigationBarItem( icon: Icon(Icons.trending_up_outlined), diff --git a/lib/screens/prediction/prediction_page.dart b/lib/screens/prediction/prediction_page.dart index 030fd3f..f6993b1 100644 --- a/lib/screens/prediction/prediction_page.dart +++ b/lib/screens/prediction/prediction_page.dart @@ -1218,7 +1218,7 @@ class _PredictionScreenState extends State { ), BottomNavigationBarItem( icon: Icon(Icons.shopping_cart_outlined), - label: 'Stock In', + label: 'Stok Masuk', ), BottomNavigationBarItem( icon: Icon(Icons.trending_up_outlined), diff --git a/lib/screens/products/product_list_page.dart b/lib/screens/products/product_list_page.dart index 74394fb..8285366 100644 --- a/lib/screens/products/product_list_page.dart +++ b/lib/screens/products/product_list_page.dart @@ -338,7 +338,7 @@ class _ProductListScreenState extends State with RouteAware { ), BottomNavigationBarItem( icon: Icon(Icons.shopping_cart_outlined), - label: 'Stock In', + label: 'Stok Masuk', ), BottomNavigationBarItem( icon: Icon(Icons.trending_up_outlined), diff --git a/lib/screens/reports/report_page.dart b/lib/screens/reports/report_page.dart index daf85a9..f8e452b 100644 --- a/lib/screens/reports/report_page.dart +++ b/lib/screens/reports/report_page.dart @@ -1,6 +1,8 @@ +import 'package:finalproject/theme/colors.dart'; +import 'package:finalproject/theme/text_styles.dart'; import 'package:flutter/material.dart'; - -import 'report_controller.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:intl/intl.dart'; class ReportScreen extends StatefulWidget { const ReportScreen({super.key}); @@ -10,454 +12,564 @@ class ReportScreen extends StatefulWidget { } class _ReportScreenState extends State { - late final ReportController _controller; + final DateFormat _dateFormat = DateFormat('dd/MM/yyyy'); - @override - void initState() { - super.initState(); - _controller = ReportController(); + final List> _stockItems = [ + {'name': 'Tepung Terigu 1kg', 'stock': 18.0, 'status': 'Aman'}, + {'name': 'Gula Pasir 1kg', 'stock': 6.5, 'status': 'Rendah'}, + {'name': 'Telur 1kg', 'stock': 2.0, 'status': 'Kritis'}, + {'name': 'Mentega 500gr', 'stock': 4.0, 'status': 'Rendah'}, + {'name': 'Baking Powder', 'stock': 12.0, 'status': 'Aman'}, + ]; + + final List> _stockHistory = [ + { + 'date': DateTime.now().subtract(const Duration(days: 1)), + 'name': 'Tepung Terigu 1kg', + 'amount': 10, + }, + { + 'date': DateTime.now().subtract(const Duration(days: 2)), + 'name': 'Gula Pasir 1kg', + 'amount': 6, + }, + { + 'date': DateTime.now().subtract(const Duration(days: 4)), + 'name': 'Mentega 500gr', + 'amount': 3, + }, + { + 'date': DateTime.now().subtract(const Duration(days: 6)), + 'name': 'Telur 1kg', + 'amount': 5, + }, + ]..sort((a, b) => (b['date'] as DateTime).compareTo(a['date'] as DateTime)); + + final List> _predictionItems = [ + { + 'product': 'Donat Cokelat', + 'prediction': 120, + 'needs': 'Tepung 15kg, Gula 6kg, Telur 4kg', + 'date': DateTime.now(), + }, + { + 'product': 'Roti Manis', + 'prediction': 90, + 'needs': 'Tepung 12kg, Gula 5kg, Mentega 3kg', + 'date': DateTime.now().subtract(const Duration(days: 2)), + }, + ]; + + final List> _usageBars = [ + {'label': 'Tepung', 'value': 40.0, 'color': AppColors.primaryBrown}, + {'label': 'Gula', 'value': 28.0, 'color': AppColors.secondaryOrange}, + {'label': 'Telur', 'value': 18.0, 'color': AppColors.secondaryBlue}, + {'label': 'Mentega', 'value': 12.0, 'color': AppColors.secondaryGreen}, + ]; + + final List _demandTrend = [28, 32, 40, 36, 44, 50, 48]; + + final List> _usagePie = [ + {'label': 'Tepung', 'value': 40.0, 'color': AppColors.primaryBrown}, + {'label': 'Gula', 'value': 25.0, 'color': AppColors.secondaryOrange}, + {'label': 'Telur', 'value': 20.0, 'color': AppColors.secondaryBlue}, + {'label': 'Mentega', 'value': 15.0, 'color': AppColors.secondaryGreen}, + ]; + + Color _statusColor(String status) { + switch (status) { + case 'Kritis': + return AppColors.statusError; + case 'Rendah': + return AppColors.statusWarning; + default: + return AppColors.statusSuccess; + } } @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( + final width = MediaQuery.of(context).size.width; + final cardWidth = (width - 48) / 2; + + return Scaffold( + backgroundColor: AppColors.bgLight, + appBar: AppBar( + backgroundColor: AppColors.primaryBrown, + elevation: 0, + title: Text( + 'Laporan & Analitik', + style: AppTextStyles.headlineLarge.copyWith(color: Colors.white), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 16, + runSpacing: 16, 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( - 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); - } - }, - ), - ), - ), - ], - ), + _buildSummaryCard( + width: cardWidth, + title: 'Total Produk', + value: '24', + icon: Icons.shopping_bag, + color: AppColors.secondaryBlue, ), - 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), - ], - ), + _buildSummaryCard( + width: cardWidth, + title: 'Total Bahan', + value: '18', + icon: Icons.inventory_2, + color: AppColors.primaryBrown, + ), + _buildSummaryCard( + width: cardWidth, + title: 'Stok Kritis', + value: '3', + icon: Icons.warning_amber_rounded, + color: AppColors.statusError, + ), + _buildSummaryCard( + width: cardWidth, + title: 'Total Prediksi', + value: '210', + icon: Icons.trending_up, + color: AppColors.statusSuccess, ), ], ), - ), - 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), + const SizedBox(height: 24), + _buildSectionTitle('Laporan Stok Bahan'), + const SizedBox(height: 12), + _buildStockTable(), + const SizedBox(height: 24), + _buildSectionTitle('Riwayat Stok Masuk'), + const SizedBox(height: 12), + _buildStockHistory(), + const SizedBox(height: 24), + _buildSectionTitle('Laporan Prediksi Permintaan'), + const SizedBox(height: 12), + _buildPredictionList(), + const SizedBox(height: 24), + _buildSectionTitle('Grafik Penggunaan Bahan'), + const SizedBox(height: 12), + _buildCharts(), + const SizedBox(height: 24), + _buildSectionTitle('Bahan Kritis'), + const SizedBox(height: 12), + _buildCriticalItems(), + const SizedBox(height: 24), + _buildExportButton(), + const SizedBox(height: 24), + ], + ), ), - 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), + 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', ), - 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, - ), - ), - ], + 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 _buildTopProductItem({ - required int rank, - required String name, - required int quantity, + Widget _buildSummaryCard({ + required double width, + required String title, + required String value, + required IconData icon, + required Color color, }) { - return Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: const Color(0xFFA89080), - borderRadius: BorderRadius.circular(8), + return Container( + width: width, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.bgWhite, + borderRadius: BorderRadius.circular(14), + boxShadow: [AppColors.shadowLight], + ), + child: Row( + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: color, size: 22), ), - 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( + title, + style: AppTextStyles.bodySmall.copyWith( + color: AppColors.textTertiary, + ), + ), + const SizedBox(height: 6), + Text( + value, + style: AppTextStyles.headlineSmall.copyWith( + color: AppColors.textPrimary, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: AppTextStyles.headlineSmall.copyWith(color: AppColors.textPrimary), + ); + } + + Widget _buildStockTable() { + return Container( + decoration: BoxDecoration( + color: AppColors.bgWhite, + borderRadius: BorderRadius.circular(16), + boxShadow: [AppColors.shadowLight], + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + columns: const [ + DataColumn(label: Text('Nama Bahan')), + DataColumn(label: Text('Stok Tersedia')), + DataColumn(label: Text('Status')), + ], + rows: + _stockItems.map((item) { + final statusColor = _statusColor(item['status'] as String); + return DataRow( + cells: [ + DataCell(Text(item['name'] as String)), + DataCell(Text('${item['stock']} kg')), + DataCell( + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + item['status'] as String, + style: AppTextStyles.labelSmall.copyWith( + color: statusColor, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ); + }).toList(), + ), + ), + ); + } + + Widget _buildStockHistory() { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.bgWhite, + borderRadius: BorderRadius.circular(16), + boxShadow: [AppColors.shadowLight], + ), + child: Column( + children: + _stockHistory + .map( + (entry) => ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.primaryBrown.withOpacity(0.12), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.add_circle_outline, + color: AppColors.primaryBrown, + ), + ), + title: Text( + entry['name'] as String, + style: AppTextStyles.labelLarge, + ), + subtitle: Text( + _dateFormat.format(entry['date'] as DateTime), + style: AppTextStyles.bodySmall.copyWith( + color: AppColors.textTertiary, + ), + ), + trailing: Text( + '+${entry['amount']} kg', + style: AppTextStyles.labelLarge.copyWith( + color: AppColors.statusSuccess, + fontWeight: FontWeight.w700, + ), + ), + ), + ) + .toList(), + ), + ); + } + + Widget _buildPredictionList() { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.bgWhite, + borderRadius: BorderRadius.circular(16), + boxShadow: [AppColors.shadowLight], + ), + child: Column( + children: + _predictionItems + .map( + (item) => ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.secondaryBlue.withOpacity(0.12), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.trending_up, + color: AppColors.secondaryBlue, + ), + ), + title: Text( + item['product'] as String, + style: AppTextStyles.labelLarge, + ), + subtitle: Text( + '${item['needs']} • ${_dateFormat.format(item['date'] as DateTime)}', + style: AppTextStyles.bodySmall.copyWith( + color: AppColors.textTertiary, + ), + ), + trailing: Text( + '${item['prediction']} unit', + style: AppTextStyles.labelLarge.copyWith( + color: AppColors.primaryBrown, + fontWeight: FontWeight.w700, + ), + ), + ), + ) + .toList(), + ), + ); + } + + Widget _buildCharts() { + return Column( + children: [ + _buildChartCard( + title: 'Bar Chart Penggunaan Bahan', + child: SizedBox( + height: 180, + child: BarChart( + BarChartData( + borderData: FlBorderData(show: false), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + if (value < 0 || value >= _usageBars.length) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + _usageBars[value.toInt()]['label'] as String, + style: AppTextStyles.labelSmall.copyWith( + color: AppColors.textSecondary, + ), + ), + ); + }, + ), + ), + ), + barGroups: + _usageBars.asMap().entries.map((entry) { + return BarChartGroupData( + x: entry.key, + barRods: [ + BarChartRodData( + toY: entry.value['value'] as double, + color: entry.value['color'] as Color, + width: 18, + borderRadius: BorderRadius.circular(6), + ), + ], + ); + }).toList(), ), ), ), ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF1F2937), + const SizedBox(height: 16), + _buildChartCard( + title: 'Line Chart Permintaan Produk', + child: SizedBox( + height: 180, + child: LineChart( + LineChartData( + borderData: FlBorderData(show: false), + gridData: FlGridData(show: false), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + return Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + 'M${value.toInt() + 1}', + style: AppTextStyles.labelSmall.copyWith( + color: AppColors.textSecondary, + ), + ), + ); + }, + ), + ), ), + lineBarsData: [ + LineChartBarData( + spots: + _demandTrend + .asMap() + .entries + .map( + (entry) => + FlSpot(entry.key.toDouble(), entry.value), + ) + .toList(), + isCurved: true, + color: AppColors.secondaryBlue, + barWidth: 3, + dotData: FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + color: AppColors.secondaryBlue.withOpacity(0.15), + ), + ), + ], ), - 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), + const SizedBox(height: 16), + _buildChartCard( + title: 'Pie Chart Bahan Paling Sering Digunakan', + child: SizedBox( + height: 200, + child: PieChart( + PieChartData( + sectionsSpace: 2, + centerSpaceRadius: 40, + sections: + _usagePie.map((entry) { + return PieChartSectionData( + value: entry['value'] as double, + color: entry['color'] as Color, + radius: 50, + showTitle: true, + title: '${entry['value']}%', + titleStyle: AppTextStyles.labelSmall.copyWith( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ); + }).toList(), + ), ), ), ), @@ -465,9 +577,135 @@ class _ReportScreenState extends State { ); } - @override - void dispose() { - _controller.dispose(); - super.dispose(); + Widget _buildChartCard({required String title, required Widget child}) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.bgWhite, + borderRadius: BorderRadius.circular(16), + boxShadow: [AppColors.shadowLight], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppTextStyles.headlineSmall.copyWith( + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 12), + child, + ], + ), + ); + } + + Widget _buildCriticalItems() { + final criticalItems = + _stockItems.where((item) => item['status'] == 'Kritis').toList(); + + if (criticalItems.isEmpty) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.bgWhite, + borderRadius: BorderRadius.circular(16), + boxShadow: [AppColors.shadowLight], + ), + child: Text( + 'Tidak ada bahan kritis saat ini.', + style: AppTextStyles.bodyMedium.copyWith( + color: AppColors.textSecondary, + ), + ), + ); + } + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.bgWhite, + borderRadius: BorderRadius.circular(16), + boxShadow: [AppColors.shadowLight], + ), + child: Column( + children: + criticalItems + .map( + (item) => ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.statusError.withOpacity(0.12), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.warning_amber_rounded, + color: AppColors.statusError, + ), + ), + title: Text( + item['name'] as String, + style: AppTextStyles.labelLarge, + ), + subtitle: Text( + 'Sisa stok: ${item['stock']} kg', + style: AppTextStyles.bodySmall.copyWith( + color: AppColors.textTertiary, + ), + ), + trailing: ElevatedButton( + onPressed: () { + Navigator.of(context).pushNamed('/transaction'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.statusSuccess, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text( + 'Tambah Stok', + style: TextStyle(fontSize: 12), + ), + ), + ), + ) + .toList(), + ), + ); + } + + Widget _buildExportButton() { + return SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Export laporan masih menggunakan data dummy.'), + ), + ); + }, + icon: const Icon(Icons.download_rounded), + label: const Text('Export Laporan'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBrown, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ); } } diff --git a/lib/screens/settings/settings_page.dart b/lib/screens/settings/settings_page.dart index cc0ddc1..1113667 100644 --- a/lib/screens/settings/settings_page.dart +++ b/lib/screens/settings/settings_page.dart @@ -391,7 +391,7 @@ class _SettingsScreenState extends State { ), BottomNavigationBarItem( icon: Icon(Icons.shopping_cart_outlined), - label: 'Stock In', + label: 'Stok Masuk', ), BottomNavigationBarItem( icon: Icon(Icons.trending_up_outlined), diff --git a/lib/screens/transaction/transaction_page.dart b/lib/screens/transaction/transaction_page.dart index c6f1b94..91f2191 100644 --- a/lib/screens/transaction/transaction_page.dart +++ b/lib/screens/transaction/transaction_page.dart @@ -71,7 +71,7 @@ class _TransactionScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - '⚠️ $successCount/$totalCount Stock In berhasil. Error: $firstError', + '⚠️ $successCount/$totalCount Stok Masuk berhasil. Error: $firstError', ), backgroundColor: AppColors.statusError, ), @@ -80,7 +80,7 @@ class _TransactionScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - '✅ $successCount Stock In berhasil disimpan dan stok terupdate!', + '✅ $successCount Stok Masuk berhasil disimpan dan stok terupdate!', ), backgroundColor: AppColors.statusSuccess, ), @@ -446,7 +446,7 @@ class _TransactionScreenState extends State { backgroundColor: AppColors.primaryBrown, elevation: 0, title: Text( - ' Stock In', + ' Stok Masuk', style: AppTextStyles.headlineLarge.copyWith(color: Colors.white), ), leading: IconButton( @@ -765,7 +765,7 @@ class _TransactionScreenState extends State { ), const SizedBox(height: 16), Text( - 'Tanggal Stock In', + 'Tanggal Stok Masuk', style: AppTextStyles.labelLarge.copyWith( color: AppColors.textPrimary, ), @@ -846,7 +846,7 @@ class _TransactionScreenState extends State { label: Text( _controller.isLoading ? 'Menyimpan...' - : 'Simpan Stock In', + : 'Simpan Stok Masuk', style: AppTextStyles.labelLarge, ), ), @@ -857,7 +857,7 @@ class _TransactionScreenState extends State { const SizedBox(height: 24), ], Text( - 'Riwayat Stock In (${_controller.transactions.length})', + 'Riwayat Stok Masuk (${_controller.transactions.length})', style: AppTextStyles.headlineSmall.copyWith( color: AppColors.textPrimary, ), @@ -876,7 +876,7 @@ class _TransactionScreenState extends State { ), const SizedBox(height: 16), Text( - 'Belum ada Stock In', + 'Belum ada Stok Masuk', style: AppTextStyles.bodyMedium.copyWith( color: AppColors.textSecondary, ), @@ -987,7 +987,7 @@ class _TransactionScreenState extends State { ), BottomNavigationBarItem( icon: Icon(Icons.shopping_cart_outlined), - label: 'Stock In', + label: 'Stok Masuk', ), BottomNavigationBarItem( icon: Icon(Icons.trending_up_outlined), diff --git a/lib/services/ml_service.dart b/lib/services/ml_service.dart index 24733c6..2bf822c 100644 --- a/lib/services/ml_service.dart +++ b/lib/services/ml_service.dart @@ -5,7 +5,7 @@ class MLService { // API URL - Change based on environment // Untuk emulator Android: 10.0.2.2 // Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local - static const String baseUrl = 'http://10.103.65.129:5000'; + static const String baseUrl = 'http://192.168.18.30:5000'; static const int timeoutSeconds = 30; diff --git a/ml_model/app.py b/ml_model/app.py index 23c696c..9ee9770 100644 --- a/ml_model/app.py +++ b/ml_model/app.py @@ -29,6 +29,9 @@ DB_USER = 'root' DB_PASSWORD = '' # Ganti dengan password MySQL Anda jika ada DB_NAME = 'prediksi_stok_db' +# Table names (use backticks for names with spaces) +TRANSACTIONS_TABLE = "`Stock In`" + def get_db_connection(): """Get MySQL database connection""" try: @@ -686,8 +689,8 @@ def save_transaction(): return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500 cursor = connection.cursor() - cursor.execute(""" - INSERT INTO transactions + cursor.execute(f""" + INSERT INTO {TRANSACTIONS_TABLE} (product_name, category, quantity, unit_price, total_price, transaction_date) VALUES (%s, %s, %s, %s, %s, %s) """, ( @@ -762,8 +765,8 @@ def add_transaction_with_stock_update(): }), 404 # 2. Save transaction - cursor.execute(""" - INSERT INTO transactions + cursor.execute(f""" + INSERT INTO {TRANSACTIONS_TABLE} (product_name, category, quantity, unit_price, total_price, transaction_date) VALUES (%s, %s, %s, %s, %s, %s) """, ( @@ -821,7 +824,7 @@ def get_transactions(): cursor = connection.cursor(dictionary=True) # Build query - query = "SELECT * FROM transactions WHERE 1=1" + query = f"SELECT * FROM {TRANSACTIONS_TABLE} WHERE 1=1" params = [] if product_name: @@ -835,7 +838,7 @@ def get_transactions(): transactions = cursor.fetchall() # Get total count - cursor.execute("SELECT COUNT(*) as total FROM transactions" + + cursor.execute(f"SELECT COUNT(*) as total FROM {TRANSACTIONS_TABLE}" + (" WHERE product_name LIKE %s" if product_name else ""), ([f"%{product_name}%"] if product_name else [])) total = cursor.fetchone()['total'] diff --git a/pubspec.lock b/pubspec.lock index 9b4f46e..5401d6b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: @@ -57,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" + url: "https://pub.dev" + source: hosted + version: "0.66.2" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 06ebe5a..becb186 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,9 @@ dependencies: # Intl untuk date formatting intl: ^0.19.0 + # Charts + fl_chart: ^0.66.2 + dev_dependencies: flutter_test: sdk: flutter