feat: add feature bar chart to visualization trash perform

This commit is contained in:
pahmiudahgede 2025-05-27 05:34:18 +07:00
parent b997980f1c
commit c87d820a9d
13 changed files with 790 additions and 12 deletions

View File

@ -11,9 +11,9 @@ export 'package:rijig_mobile/features/auth/service/otp_service.dart';
export 'package:rijig_mobile/globaldata/trash/trash_repository.dart';
export 'package:rijig_mobile/globaldata/trash/trash_service.dart';
export 'package:rijig_mobile/globaldata/trash/trash_viewmodel.dart';
export 'package:rijig_mobile/features/home/presentation/viewmodel/about_vmod.dart';
export 'package:rijig_mobile/features/home/repositories/about_repository.dart';
export 'package:rijig_mobile/features/home/service/about_service.dart';
export 'package:rijig_mobile/globaldata/about/about_vmod.dart';
export 'package:rijig_mobile/globaldata/about/about_repository.dart';
export 'package:rijig_mobile/globaldata/about/about_service.dart';
export 'package:rijig_mobile/globaldata/article/article_repository.dart';
export 'package:rijig_mobile/globaldata/article/article_service.dart';
export 'package:rijig_mobile/globaldata/article/article_vmod.dart';

View File

@ -45,6 +45,7 @@ final router = GoRouter(
// Rute untuk halaman-halaman utama
GoRoute(path: '/home', builder: (context, state) => HomeScreen()),
GoRoute(path: '/dataperforma', builder: (context, state) => DatavisualizedScreen()),
GoRoute(path: '/activity', builder: (context, state) => ActivityScreen()),
GoRoute(
path: '/requestpickup',

View File

@ -3,6 +3,7 @@ export 'package:rijig_mobile/core/utils/navigation.dart';
export 'package:rijig_mobile/features/activity/presentation/screen/activity_screen.dart';
export 'package:rijig_mobile/features/cart/presentation/screens/cart_screen.dart';
export 'package:rijig_mobile/features/home/presentation/screen/home_screen.dart';
export 'package:rijig_mobile/features/home/datavisualized/presentation/screen/datavisualized_screen.dart';
export 'package:rijig_mobile/features/profil/presentation/screen/profil_screen.dart';
export 'package:rijig_mobile/features/requestpick/presentation/screen/requestpickup_screen.dart';
export 'package:rijig_mobile/features/auth/presentation/screen/inputpin_screen.dart';

View File

@ -0,0 +1,755 @@
import 'package:charts_painter/chart.dart';
import 'package:flutter/material.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
import 'package:rijig_mobile/widget/appbar.dart';
import 'package:el_tooltip/el_tooltip.dart';
class DatavisualizedScreen extends StatefulWidget {
const DatavisualizedScreen({super.key});
@override
State<DatavisualizedScreen> createState() => _DatavisualizedScreenState();
}
class _DatavisualizedScreenState extends State<DatavisualizedScreen> {
final List<double> dataSampahTerjual = [
15.5,
23.2,
18.7,
31.4,
28.9,
42.1,
35.8,
];
final List<String> namaHari = [
'Sen',
'Sel',
'Rab',
'Kam',
'Jum',
'Sab',
'Min',
];
int? selectedIndex;
final List<ElTooltipController> tooltipControllers = [];
@override
void initState() {
super.initState();
// Initialize tooltip controllers
for (int i = 0; i < dataSampahTerjual.length; i++) {
tooltipControllers.add(ElTooltipController());
}
}
@override
void dispose() {
_hideAllTooltips();
for (var controller in tooltipControllers) {
controller.dispose();
}
super.dispose();
}
// Simple method to hide all tooltips
void _hideAllTooltips() {
for (var controller in tooltipControllers) {
try {
controller.hide();
} catch (e) {
// Ignore errors - controller might already be disposed
}
}
}
// Main selection handler - used by both bar clicks and day name clicks
void _handleSelection(int index) {
// If same item is selected, deselect it
if (selectedIndex == index) {
_clearSelection();
return;
}
// Hide all tooltips first
_hideAllTooltips();
// Update selection
setState(() {
selectedIndex = index;
});
// Show tooltip after a brief delay
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted && selectedIndex == index) {
tooltipControllers[index].show();
}
});
}
// Clear selection and hide tooltips
void _clearSelection() {
_hideAllTooltips();
setState(() {
selectedIndex = null;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(judul: "Performa"),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header summary card
_buildHeaderCard(),
const SizedBox(height: 24),
// Chart section
_buildChartSection(),
const SizedBox(height: 12),
// Day labels (clickable)
_buildDayLabels(),
const SizedBox(height: 16),
// Selected item info card
if (selectedIndex != null) _buildSelectedItemCard(),
// Statistics cards
_buildStatisticsCards(),
],
),
),
);
}
Widget _buildHeaderCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
children: [
Icon(Icons.recycling, color: Colors.blue.shade700, size: 28),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sampah Terjual Minggu Ini',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
Text(
'Total: ${dataSampahTerjual.reduce((a, b) => a + b).toStringAsFixed(1)} kg',
style: TextStyle(
fontSize: 14,
color: Colors.blue.shade600,
),
),
],
),
],
),
);
}
Widget _buildChartSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Grafik Penjualan Sampah (kg)',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 16),
Container(
height: 300,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.shade300,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Stack(
children: [
// Chart
Chart<void>(
state: ChartState<void>(
behaviour: ChartBehaviour(
onItemClicked: (item) {
final clickedIndex = dataSampahTerjual.indexWhere(
(data) => data == item.item.value,
);
if (clickedIndex != -1) {
_handleSelection(clickedIndex);
}
},
),
data: ChartData.fromList(
dataSampahTerjual
.asMap()
.entries
.map((entry) => ChartItem<void>(entry.value))
.toList(),
),
itemOptions: BarItemOptions(
padding: const EdgeInsets.symmetric(horizontal: 4),
barItemBuilder: (itemBuilderData) {
final isSelected = selectedIndex == itemBuilderData.itemIndex;
return BarItem(
color: isSelected ? Colors.orange.shade600 : primaryColor,
);
},
),
backgroundDecorations: [
GridDecoration(
showVerticalGrid: false,
showHorizontalGrid: true,
horizontalAxisStep: 10,
gridColor: Colors.grey.shade300,
),
],
foregroundDecorations: [
SparkLineDecoration(
lineColor: Colors.orange.shade400,
lineWidth: 2,
),
],
),
height: 250,
),
// Tooltips overlay
..._buildTooltipOverlays(),
],
),
),
],
);
}
List<Widget> _buildTooltipOverlays() {
return dataSampahTerjual.asMap().entries.map((entry) {
final index = entry.key;
final value = entry.value;
final chartWidth = MediaQuery.of(context).size.width - 64;
final barWidth = chartWidth / dataSampahTerjual.length;
return Positioned(
left: (index * barWidth) + (barWidth / 2) - 20,
top: 50,
bottom: 50,
width: 40,
child: ElTooltip(
controller: tooltipControllers[index],
position: ElTooltipPosition.topCenter,
color: Colors.black87,
showArrow: true,
showModal: false,
showChildAboveOverlay: false,
content: _buildTooltipContent(index, value),
child: Container(width: 40, color: Colors.transparent),
),
);
}).toList();
}
Widget _buildTooltipContent(int index, double value) {
return GestureDetector(
onTap: () {
tooltipControllers[index].hide();
_showDetailDialog(index);
},
child: Container(
constraints: const BoxConstraints(minWidth: 100),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
namaHari[index],
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'${value.toStringAsFixed(1)} kg',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(10),
),
child: const Text(
'👆 Tap untuk detail',
style: TextStyle(color: Colors.white70, fontSize: 10),
),
),
],
),
),
);
}
Widget _buildDayLabels() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: namaHari.asMap().entries.map((entry) {
final index = entry.key;
final isSelected = selectedIndex == index;
return GestureDetector(
onTap: () => _handleSelection(index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? Colors.orange.shade100 : Colors.transparent,
borderRadius: BorderRadius.circular(6),
),
child: Text(
entry.value,
style: TextStyle(
fontSize: 12,
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
color: isSelected ? Colors.orange.shade700 : Colors.grey.shade700,
),
),
),
);
}).toList(),
),
);
}
Widget _buildSelectedItemCard() {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange.shade200),
),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.orange.shade700),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Data Terpilih: ${namaHari[selectedIndex!]}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
fontSize: 14,
),
),
Text(
'Sampah terjual: ${dataSampahTerjual[selectedIndex!].toStringAsFixed(1)} kg',
style: TextStyle(
color: Colors.orange.shade600,
fontSize: 13,
),
),
],
),
),
// Close button
GestureDetector(
onTap: _clearSelection,
child: Container(
padding: const EdgeInsets.all(4),
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: Colors.orange.shade200,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.close,
size: 16,
color: Colors.orange.shade700,
),
),
),
// Detail button
GestureDetector(
onTap: () => _navigateToDetailPage(selectedIndex!),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.orange.shade700,
borderRadius: BorderRadius.circular(6),
),
child: const Text(
'Detail',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}
Widget _buildStatisticsCards() {
return Column(
children: [
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildStatCard(
'Rata-rata',
'${(dataSampahTerjual.reduce((a, b) => a + b) / dataSampahTerjual.length).toStringAsFixed(1)} kg',
Icons.trending_up,
Colors.blue,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Tertinggi',
'${dataSampahTerjual.reduce((a, b) => a > b ? a : b).toStringAsFixed(1)} kg',
Icons.star,
Colors.orange,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatCard(
'Terendah',
'${dataSampahTerjual.reduce((a, b) => a < b ? a : b).toStringAsFixed(1)} kg',
Icons.trending_down,
Colors.red,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Hari Aktif',
'${dataSampahTerjual.where((data) => data > 0).length} hari',
Icons.calendar_today,
Colors.green,
),
),
],
),
],
);
}
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 8),
Text(
title,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}
void _showDetailDialog(int index) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Row(
children: [
Icon(Icons.bar_chart, color: primaryColor),
const SizedBox(width: 8),
Text('Detail ${namaHari[index]}'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDialogInfoRow('Hari:', namaHari[index]),
const SizedBox(height: 8),
_buildDialogInfoRow(
'Jumlah:',
'${dataSampahTerjual[index].toStringAsFixed(1)} kg',
),
const SizedBox(height: 8),
_buildDialogInfoRow(
'Persentase:',
'${((dataSampahTerjual[index] / dataSampahTerjual.reduce((a, b) => a + b)) * 100).toStringAsFixed(1)}%',
),
const SizedBox(height: 8),
_buildDialogInfoRow(
'Ranking:',
'#${_getRanking(index)} dari ${dataSampahTerjual.length} hari',
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Tutup'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_navigateToDetailPage(index);
},
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Lihat Detail'),
),
],
),
);
}
Widget _buildDialogInfoRow(String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
],
);
}
int _getRanking(int index) {
final sortedData = dataSampahTerjual
.asMap()
.entries
.toList()
..sort((a, b) => b.value.compareTo(a.value));
return sortedData.indexWhere((entry) => entry.key == index) + 1;
}
void _navigateToDetailPage(int index) {
_hideAllTooltips();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailHariScreen(
hari: namaHari[index],
jumlahSampah: dataSampahTerjual[index],
index: index,
allData: dataSampahTerjual,
allDays: namaHari,
),
),
).then((_) {
if (mounted) {
_clearSelection();
}
});
}
}
class DetailHariScreen extends StatelessWidget {
final String hari;
final double jumlahSampah;
final int index;
final List<double> allData;
final List<String> allDays;
const DetailHariScreen({
super.key,
required this.hari,
required this.jumlahSampah,
required this.index,
required this.allData,
required this.allDays,
});
@override
Widget build(BuildContext context) {
final totalSampah = allData.reduce((a, b) => a + b);
final persentase = (jumlahSampah / totalSampah) * 100;
final ranking = _getRanking();
return Scaffold(
appBar: AppBar(
title: Text('Detail $hari'),
backgroundColor: primaryColor,
foregroundColor: Colors.white,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [primaryColor.withValues(alpha: 0.1), Colors.white],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: primaryColor,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.recycling,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Penjualan Sampah',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
),
),
Text(
hari,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
const SizedBox(height: 16),
Text(
'${jumlahSampah.toStringAsFixed(1)} kg',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: primaryColor,
),
),
const SizedBox(height: 8),
Text(
'Persentase: ${persentase.toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
),
),
Text(
'Ranking: #$ranking dari ${allData.length} hari',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
),
),
],
),
),
),
],
),
),
);
}
int _getRanking() {
final sortedData = allData
.asMap()
.entries
.toList()
..sort((a, b) => b.value.compareTo(a.value));
return sortedData.indexWhere((entry) => entry.key == index) + 1;
}
}

View File

@ -4,7 +4,7 @@ import 'package:carousel_slider/carousel_slider.dart';
import 'package:provider/provider.dart';
import 'package:rijig_mobile/core/router.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
import 'package:rijig_mobile/features/home/presentation/viewmodel/about_vmod.dart';
import 'package:rijig_mobile/globaldata/about/about_vmod.dart';
import 'package:rijig_mobile/widget/skeletonize.dart';
class AboutComponent extends StatefulWidget {

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:provider/provider.dart';
import 'package:rijig_mobile/features/home/presentation/viewmodel/about_vmod.dart';
import 'package:rijig_mobile/globaldata/about/about_vmod.dart';
import 'package:rijig_mobile/widget/appbar.dart';
import 'package:rijig_mobile/widget/skeletonize.dart';

View File

@ -7,10 +7,11 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:iconsax_flutter/iconsax_flutter.dart';
import 'package:provider/provider.dart';
import 'package:rijig_mobile/core/router.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
import 'package:rijig_mobile/features/home/presentation/components/about_comp.dart';
import 'package:rijig_mobile/features/home/presentation/components/article_list.dart';
import 'package:rijig_mobile/features/home/presentation/viewmodel/about_vmod.dart';
import 'package:rijig_mobile/globaldata/about/about_vmod.dart';
import 'package:rijig_mobile/globaldata/article/article_vmod.dart';
import 'package:rijig_mobile/widget/card_withicon.dart';
@ -89,7 +90,9 @@ class _HomeScreenState extends State<HomeScreen> {
icon: Iconsax.trash,
text: 'Sampah',
number: '245 kg',
onTap: () {},
onTap: () {
router.push('/dataperforma');
},
),
CardWithIcon(
icon: Iconsax.timer,

View File

@ -1,5 +1,5 @@
import 'package:rijig_mobile/core/api/api_services.dart';
import 'package:rijig_mobile/features/home/model/about_model.dart';
import 'package:rijig_mobile/globaldata/about/about_model.dart';
class AboutRepository {
final Https _https = Https();

View File

@ -1,5 +1,5 @@
import 'package:rijig_mobile/features/home/repositories/about_repository.dart';
import 'package:rijig_mobile/features/home/model/about_model.dart';
import 'package:rijig_mobile/globaldata/about/about_repository.dart';
import 'package:rijig_mobile/globaldata/about/about_model.dart';
class AboutService {
final AboutRepository _aboutRepository;

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:rijig_mobile/features/home/service/about_service.dart';
import 'package:rijig_mobile/features/home/model/about_model.dart';
import 'package:rijig_mobile/globaldata/about/about_service.dart';
import 'package:rijig_mobile/globaldata/about/about_model.dart';
class AboutViewModel extends ChangeNotifier {
final AboutService _aboutService;

View File

@ -49,6 +49,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
charts_painter:
dependency: "direct main"
description:
name: charts_painter
sha256: "5314ef91979b59b60a869df86caba09e7a3ce56da1fb18530ea7bbd2da77fa70"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
checked_yaml:
dependency: transitive
description:
@ -169,6 +177,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.2"
el_tooltip:
dependency: "direct main"
description:
name: el_tooltip
sha256: "0860b00e9390a31dd98369dc16d3b6fa2668fc52df712bd00e86d8931787fc17"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
equatable:
dependency: transitive
description:

View File

@ -9,12 +9,14 @@ environment:
dependencies:
carousel_slider: ^5.0.0
charts_painter: ^3.1.1
collection: ^1.19.1
concentric_transition: ^1.0.3
connectivity_plus: ^6.1.4
cupertino_icons: ^1.0.8
custom_refresh_indicator: ^4.0.1
device_info_plus: ^11.4.0
el_tooltip: ^2.2.1
fl_chart: ^1.0.0
flutter:
sdk: flutter