879 lines
33 KiB
Dart
879 lines
33 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import '../utils/api_config.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'search_route_screen.dart';
|
|
import 'history_screen.dart';
|
|
import 'history_detail_screen.dart';
|
|
import 'profile_screen.dart'; // Tambahkan import Profil
|
|
// Import session manager untuk nama dinamis
|
|
import '../utils/session_manager.dart';
|
|
import '../widgets/responsive_layout.dart';
|
|
|
|
class HomeScreen extends StatefulWidget {
|
|
const HomeScreen({super.key});
|
|
|
|
@override
|
|
State<HomeScreen> createState() => _HomeScreenState();
|
|
}
|
|
|
|
class _HomeScreenState extends State<HomeScreen> {
|
|
int _selectedIndex = 0;
|
|
|
|
final List<Widget> _pages;
|
|
|
|
_HomeScreenState() : _pages = [] {
|
|
_pages.addAll([
|
|
HomeDashboardContent(onPlanRoute: () => _onItemTapped(1)),
|
|
const SearchRouteScreen(),
|
|
const HistoryScreen(),
|
|
const ProfileScreen(),
|
|
]);
|
|
}
|
|
|
|
void _onItemTapped(int index) {
|
|
setState(() {
|
|
_selectedIndex = index;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ResponsiveLayout(
|
|
child: PopScope(
|
|
canPop: false,
|
|
onPopInvoked: (didPop) {
|
|
if (didPop) return;
|
|
if (_selectedIndex != 0) {
|
|
setState(() {
|
|
_selectedIndex = 0;
|
|
});
|
|
}
|
|
},
|
|
child: Scaffold(
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
body: _pages[_selectedIndex],
|
|
bottomNavigationBar: BottomNavigationBar(
|
|
currentIndex: _selectedIndex,
|
|
onTap: _onItemTapped,
|
|
type: BottomNavigationBarType.fixed,
|
|
selectedItemColor: Colors.blue,
|
|
unselectedItemColor: Colors.grey,
|
|
selectedLabelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
|
unselectedLabelStyle: const TextStyle(fontSize: 12),
|
|
items: const [
|
|
BottomNavigationBarItem(icon: Icon(Icons.home_filled), label: 'Home'),
|
|
BottomNavigationBarItem(icon: Icon(Icons.location_searching), label: 'Cari Rute'),
|
|
BottomNavigationBarItem(icon: Icon(Icons.history), label: 'Riwayat'),
|
|
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profil'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HomeDashboardContent extends StatefulWidget {
|
|
final VoidCallback onPlanRoute;
|
|
const HomeDashboardContent({super.key, required this.onPlanRoute});
|
|
|
|
@override
|
|
State<HomeDashboardContent> createState() => _HomeDashboardContentState();
|
|
}
|
|
|
|
class _HomeDashboardContentState extends State<HomeDashboardContent> {
|
|
bool _isWeekly = true; // Toggle Mingguan/Bulanan
|
|
List<dynamic> _history = [];
|
|
List<dynamic> _recentRoutes = [];
|
|
double _totalDistance = 0;
|
|
int _totalSeconds = 0;
|
|
bool _isLoading = true;
|
|
int _selectedMonth = DateTime.now().month;
|
|
int _selectedYear = DateTime.now().year;
|
|
int _selectedWeekIndex = 0; // 0: Semua Minggu (Bulanan), 1-4: Minggu spesifik
|
|
int? _selectedIndex; // Index titik yang dipilih di grafik
|
|
List<double> _chartDataPoints = [];
|
|
List<String> _chartLabels = [];
|
|
double _maxDistance = 10.0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadDashboardData();
|
|
}
|
|
|
|
String _getMonthName(int month) {
|
|
const names = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];
|
|
return names[month - 1];
|
|
}
|
|
|
|
String _getShortMonthName(int month) {
|
|
const names = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"];
|
|
return names[month - 1];
|
|
}
|
|
|
|
List<Map<String, DateTime>> _getWeekRanges(int month, int year) {
|
|
List<Map<String, DateTime>> ranges = [];
|
|
DateTime date = DateTime(year, month, 1);
|
|
int daysToSunday = (7 - date.weekday) % 7;
|
|
DateTime currentSunday = date.add(Duration(days: daysToSunday));
|
|
|
|
while (currentSunday.month == month) {
|
|
DateTime saturday = currentSunday.add(const Duration(days: 6));
|
|
ranges.add({
|
|
'start': currentSunday,
|
|
'end': saturday,
|
|
});
|
|
currentSunday = currentSunday.add(const Duration(days: 7));
|
|
}
|
|
return ranges;
|
|
}
|
|
|
|
List<String> _generateWeeks(int month, int year) {
|
|
List<String> weeks = ["Semua Minggu"];
|
|
List<Map<String, DateTime>> ranges = _getWeekRanges(month, year);
|
|
|
|
for (var range in ranges) {
|
|
DateTime start = range['start']!;
|
|
DateTime end = range['end']!;
|
|
|
|
if (start.month == end.month) {
|
|
weeks.add("${start.day} - ${end.day} ${_getMonthName(start.month)}");
|
|
} else {
|
|
weeks.add("${start.day} ${_getMonthName(start.month)} - ${end.day} ${_getMonthName(end.month)}");
|
|
}
|
|
}
|
|
return weeks;
|
|
}
|
|
|
|
Future<void> _loadDashboardData() async {
|
|
final url = Uri.parse("${ApiConfig.baseUrl}/get_history.php?id_users=${SessionManager.userId}");
|
|
try {
|
|
final response = await http.get(url);
|
|
if (response.statusCode == 200) {
|
|
List<dynamic> decodedHistory = jsonDecode(response.body);
|
|
setState(() {
|
|
_history = decodedHistory;
|
|
_recentRoutes = decodedHistory.take(3).toList();
|
|
_calculateStats();
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Gagal memuat data riwayat dari server: $e");
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _calculateStats() {
|
|
double dist = 0;
|
|
int secs = 0;
|
|
|
|
DateTime startDate;
|
|
DateTime endDate;
|
|
|
|
List<Map<String, DateTime>> ranges = _getWeekRanges(_selectedMonth, _selectedYear);
|
|
|
|
if (_selectedWeekIndex == 0) {
|
|
_isWeekly = false;
|
|
if (ranges.isNotEmpty) {
|
|
startDate = ranges.first['start']!;
|
|
endDate = ranges.last['end']!;
|
|
|
|
_chartDataPoints = List.filled(ranges.length, 0.0);
|
|
_chartLabels = [];
|
|
|
|
for (var range in ranges) {
|
|
DateTime start = range['start']!;
|
|
DateTime end = range['end']!;
|
|
if (start.month == end.month) {
|
|
_chartLabels.add("${start.day}-${end.day} ${_getShortMonthName(start.month)}");
|
|
} else {
|
|
_chartLabels.add("${start.day} ${_getShortMonthName(start.month)}-${end.day} ${_getShortMonthName(end.month)}");
|
|
}
|
|
}
|
|
} else {
|
|
startDate = DateTime(_selectedYear, _selectedMonth, 1);
|
|
endDate = DateTime(_selectedYear, _selectedMonth + 1, 0);
|
|
_chartDataPoints = [];
|
|
_chartLabels = [];
|
|
}
|
|
} else {
|
|
_isWeekly = true;
|
|
int index = _selectedWeekIndex - 1;
|
|
if (index >= 0 && index < ranges.length) {
|
|
startDate = ranges[index]['start']!;
|
|
endDate = ranges[index]['end']!;
|
|
|
|
int daysCount = endDate.difference(startDate).inDays + 1;
|
|
_chartDataPoints = List.filled(daysCount, 0.0);
|
|
|
|
const dayNames = ["Min", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab"];
|
|
_chartLabels = List.generate(daysCount, (i) {
|
|
DateTime d = startDate.add(Duration(days: i));
|
|
return "${dayNames[d.weekday % 7]}, ${d.day}";
|
|
});
|
|
} else {
|
|
startDate = DateTime(_selectedYear, _selectedMonth, 1);
|
|
endDate = DateTime(_selectedYear, _selectedMonth + 1, 0);
|
|
_chartDataPoints = [];
|
|
_chartLabels = [];
|
|
}
|
|
}
|
|
|
|
for (var trip in _history) {
|
|
DateTime tripDate = DateTime.parse(trip['tanggal']);
|
|
|
|
if (tripDate.isAfter(startDate.subtract(const Duration(seconds: 1))) &&
|
|
tripDate.isBefore(endDate.add(const Duration(days: 1)))) {
|
|
|
|
double tripDist = double.tryParse(trip['jarak_tempuh']?.toString() ?? "0") ?? 0;
|
|
int tripSecs = int.tryParse(trip['waktu_tempuh']?.toString() ?? "0") ?? 0;
|
|
|
|
int dataIndex = -1;
|
|
if (_isWeekly) {
|
|
dataIndex = tripDate.difference(startDate).inDays;
|
|
} else {
|
|
for (int i = 0; i < ranges.length; i++) {
|
|
if (tripDate.isAfter(ranges[i]['start']!.subtract(const Duration(seconds: 1))) &&
|
|
tripDate.isBefore(ranges[i]['end']!.add(const Duration(days: 1)))) {
|
|
dataIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dataIndex >= 0 && dataIndex < _chartDataPoints.length) {
|
|
_chartDataPoints[dataIndex] += tripDist;
|
|
|
|
if (_selectedIndex == null || _selectedIndex == dataIndex) {
|
|
dist += tripDist;
|
|
secs += tripSecs;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
double maxVal = _chartDataPoints.isNotEmpty ? _chartDataPoints.reduce((a, b) => a > b ? a : b) : 0;
|
|
_maxDistance = maxVal > 0 ? maxVal : 10.0;
|
|
|
|
setState(() {
|
|
_totalDistance = dist;
|
|
_totalSeconds = secs;
|
|
});
|
|
}
|
|
|
|
String _formatDuration(int seconds) {
|
|
if (seconds <= 0) return "0 menit";
|
|
int h = seconds ~/ 3600;
|
|
int m = (seconds % 3600) ~/ 60;
|
|
if (h > 0) return "$h jam $m menit";
|
|
return "$m menit";
|
|
}
|
|
|
|
String getStatSubtitle(bool isDistance) {
|
|
if (_selectedIndex != null) {
|
|
if (_isWeekly) {
|
|
return isDistance
|
|
? "Total jarak yang Anda tempuh pada hari ini"
|
|
: "Total durasi yang Anda tempuh pada hari ini";
|
|
} else {
|
|
return isDistance
|
|
? "Total jarak yang Anda tempuh pada minggu ini"
|
|
: "Total durasi yang Anda tempuh pada minggu ini";
|
|
}
|
|
} else {
|
|
if (_isWeekly) {
|
|
return isDistance
|
|
? "Total jarak yang Anda tempuh pada minggu ini"
|
|
: "Total durasi yang Anda tempuh pada minggu ini";
|
|
} else {
|
|
return isDistance
|
|
? "Total jarak yang Anda tempuh pada bulan ini"
|
|
: "Total durasi yang Anda tempuh pada bulan ini";
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: isDark
|
|
? [const Color(0xFF121212), const Color(0xFF1E1E1E), const Color(0xFF121212)]
|
|
: [const Color(0xFFF8FAFC), const Color(0xFFF1F5F9), const Color(0xFFE2E8F0)],
|
|
),
|
|
),
|
|
child: SafeArea(
|
|
child: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: SingleChildScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Selamat ${DateTime.now().hour < 12 ? "Pagi" : DateTime.now().hour < 15 ? "Siang" : DateTime.now().hour < 18 ? "Sore" : "Malam"},',
|
|
style: TextStyle(fontSize: 14, color: isDark ? Colors.blueGrey.shade200 : Colors.blueGrey, fontWeight: FontWeight.w500)),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
SessionManager.userName ?? "User",
|
|
style: TextStyle(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w900,
|
|
color: isDark ? Colors.white : const Color(0xFF1E293B),
|
|
letterSpacing: -0.5
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
Text('Tren Aktivitas Anda', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: isDark ? Colors.white : const Color(0xFF1E293B), letterSpacing: -0.5)),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF252836) : Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade200),
|
|
),
|
|
child: DropdownButtonHideUnderline(
|
|
child: DropdownButton<int>(
|
|
value: _selectedMonth,
|
|
isExpanded: true,
|
|
items: List.generate(12, (i) => DropdownMenuItem(
|
|
value: i + 1,
|
|
child: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(_getMonthName(i + 1)),
|
|
),
|
|
)),
|
|
onChanged: (val) {
|
|
setState(() {
|
|
_selectedMonth = val!;
|
|
_selectedWeekIndex = 0;
|
|
_selectedIndex = null;
|
|
_calculateStats();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF252836) : Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade200),
|
|
),
|
|
child: DropdownButtonHideUnderline(
|
|
child: DropdownButton<int>(
|
|
value: _selectedWeekIndex,
|
|
isExpanded: true,
|
|
items: List.generate(_generateWeeks(_selectedMonth, _selectedYear).length, (i) => DropdownMenuItem(
|
|
value: i,
|
|
child: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(_generateWeeks(_selectedMonth, _selectedYear)[i]),
|
|
),
|
|
)),
|
|
onChanged: (val) {
|
|
setState(() {
|
|
_selectedWeekIndex = val!;
|
|
_selectedIndex = null;
|
|
_calculateStats();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
_buildActivityChart(),
|
|
const SizedBox(height: 24),
|
|
|
|
// --- KARTU STATISTIK ---
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildStatCard(
|
|
'Jarak',
|
|
'${_totalDistance.toStringAsFixed(1)} km',
|
|
Icons.directions_bike,
|
|
Colors.blue,
|
|
getStatSubtitle(true)
|
|
),
|
|
const SizedBox(width: 16),
|
|
_buildStatCard(
|
|
'Waktu',
|
|
_formatDuration(_totalSeconds),
|
|
Icons.timer,
|
|
Colors.orange,
|
|
getStatSubtitle(false)
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 32),
|
|
|
|
// --- KARTU VISUAL GRADIENT ---
|
|
_buildPlanRouteCard(context),
|
|
const SizedBox(height: 32),
|
|
|
|
Text('Rute Terakhir', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: isDark ? Colors.white : const Color(0xFF1E293B), letterSpacing: -0.5)),
|
|
const SizedBox(height: 16),
|
|
_recentRoutes.isEmpty
|
|
? const Text("Belum ada rute yang diselesaikan", style: TextStyle(color: Colors.grey))
|
|
: Column(
|
|
children: _recentRoutes.map((route) => _buildRecentRouteItem(route)).toList(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFilterChip(String label, bool isActive, VoidCallback onTap) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: isActive ? Colors.blueAccent : (isDark ? const Color(0xFF1E1E1E) : Colors.white),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: isActive ? Colors.blueAccent : (isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade200)),
|
|
boxShadow: isActive ? [BoxShadow(color: Colors.blue.withOpacity(isDark ? 0.1 : 0.2), blurRadius: 8)] : null,
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: isActive ? Colors.white : Colors.grey.shade600,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActivityChart() {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return Container(
|
|
height: 240,
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.fromLTRB(15, 25, 20, 10),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
|
|
borderRadius: BorderRadius.circular(28),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(isDark ? 0.3 : 0.04),
|
|
blurRadius: 24,
|
|
offset: const Offset(0, 12),
|
|
),
|
|
],
|
|
border: isDark ? Border.all(color: Colors.white.withOpacity(0.05), width: 1) : null,
|
|
),
|
|
child: _chartDataPoints.isEmpty
|
|
? const Center(child: Text("Tidak ada data", style: TextStyle(color: Colors.grey)))
|
|
: LineChart(
|
|
LineChartData(
|
|
gridData: FlGridData(
|
|
show: true,
|
|
drawVerticalLine: true,
|
|
horizontalInterval: 0.2,
|
|
verticalInterval: 1,
|
|
getDrawingHorizontalLine: (value) => FlLine(color: isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade100, strokeWidth: 1),
|
|
getDrawingVerticalLine: (value) => FlLine(color: isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade100, strokeWidth: 1),
|
|
),
|
|
titlesData: FlTitlesData(
|
|
show: true,
|
|
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 30,
|
|
interval: 1,
|
|
getTitlesWidget: (value, meta) {
|
|
int index = value.toInt();
|
|
if (index >= 0 && index < _chartLabels.length) {
|
|
return SideTitleWidget(
|
|
axisSide: meta.axisSide,
|
|
child: Text(
|
|
_chartLabels[index],
|
|
style: TextStyle(
|
|
color: _selectedIndex == index ? Colors.orangeAccent : (isDark ? Colors.white54 : Colors.grey),
|
|
fontSize: 10,
|
|
fontWeight: _selectedIndex == index ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return const SizedBox();
|
|
},
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 45,
|
|
interval: _maxDistance > 0 ? (_maxDistance / 5).ceilToDouble().clamp(1.0, 100.0) : 2.0,
|
|
getTitlesWidget: (value, meta) {
|
|
return SideTitleWidget(
|
|
axisSide: meta.axisSide,
|
|
child: Text(
|
|
"${value.toStringAsFixed(0)} km",
|
|
style: TextStyle(color: isDark ? Colors.white54 : Colors.grey, fontSize: 10),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
borderData: FlBorderData(
|
|
show: false,
|
|
),
|
|
minX: 0,
|
|
maxX: (_chartDataPoints.length - 1).toDouble(),
|
|
minY: 0,
|
|
maxY: (_maxDistance * 1.25).ceilToDouble(),
|
|
lineBarsData: [
|
|
LineChartBarData(
|
|
spots: List.generate(_chartDataPoints.length, (i) => FlSpot(i.toDouble(), _chartDataPoints[i])),
|
|
isCurved: true,
|
|
color: Colors.blueAccent,
|
|
barWidth: 3,
|
|
isStrokeCapRound: true,
|
|
dotData: FlDotData(
|
|
show: true,
|
|
getDotPainter: (spot, percent, barData, index) {
|
|
bool isSelected = _selectedIndex == index;
|
|
return FlDotCirclePainter(
|
|
radius: isSelected ? 6 : 4,
|
|
color: isSelected ? Colors.orangeAccent : Colors.blue,
|
|
strokeWidth: 2,
|
|
strokeColor: Colors.white,
|
|
);
|
|
},
|
|
),
|
|
belowBarData: BarAreaData(
|
|
show: true,
|
|
color: Colors.blueAccent.withOpacity(0.05),
|
|
),
|
|
),
|
|
],
|
|
lineTouchData: LineTouchData(
|
|
touchCallback: (FlTouchEvent event, LineTouchResponse? touchResponse) {
|
|
if (!event.isInterestedForInteractions || touchResponse == null || touchResponse.lineBarSpots == null) {
|
|
return;
|
|
}
|
|
if (event is FlTapDownEvent || event is FlTapUpEvent) {
|
|
final spotIndex = touchResponse.lineBarSpots!.first.spotIndex;
|
|
setState(() {
|
|
if (_selectedIndex == spotIndex) {
|
|
_selectedIndex = null;
|
|
} else {
|
|
_selectedIndex = spotIndex;
|
|
}
|
|
_calculateStats();
|
|
});
|
|
}
|
|
},
|
|
handleBuiltInTouches: true,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatCard(String title, String value, IconData icon, Color color, String subtitle) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return Expanded(
|
|
child: Container(
|
|
clipBehavior: Clip.antiAlias,
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF252836) : Colors.white,
|
|
borderRadius: BorderRadius.circular(24),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: isDark ? Colors.black.withOpacity(0.4) : color.withOpacity(0.1),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
border: Border.all(
|
|
color: isDark ? Colors.white.withOpacity(0.05) : Colors.grey.shade200,
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
// Thick left accent border
|
|
Positioned(
|
|
left: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
child: Container(width: 6, color: color),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.15),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(icon, color: color, size: 22),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w900,
|
|
color: isDark ? Colors.white : const Color(0xFF1E293B),
|
|
letterSpacing: -0.5
|
|
)
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
color: isDark ? Colors.white.withOpacity(0.9) : Colors.blueGrey.shade700,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700
|
|
)
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(
|
|
color: isDark ? Colors.white54 : Colors.grey.shade500,
|
|
fontSize: 11,
|
|
fontStyle: FontStyle.italic,
|
|
height: 1.2
|
|
)
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPlanRouteCard(BuildContext context) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF3B82F6), Color(0xFF2563EB)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(28),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.blue.withOpacity(0.3),
|
|
blurRadius: 25,
|
|
offset: const Offset(0, 12),
|
|
)
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Siap untuk gowes hari ini?',
|
|
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w900, letterSpacing: -0.5)),
|
|
const SizedBox(height: 8),
|
|
const Text('Temukan rute terbaik yang aman dan nyaman untukmu.',
|
|
style: TextStyle(color: Colors.white70, fontSize: 14, fontWeight: FontWeight.w500)),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton(
|
|
onPressed: widget.onPlanRoute,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.white,
|
|
foregroundColor: Colors.blue.shade700,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
),
|
|
child: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text('Mulai Jelajah', style: TextStyle(fontWeight: FontWeight.w800)),
|
|
SizedBox(width: 8),
|
|
Icon(Icons.arrow_forward_rounded, size: 16),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNotificationBadge(BuildContext context) {
|
|
return Stack(
|
|
children: [
|
|
IconButton(
|
|
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Belum ada notifikasi baru"))),
|
|
icon: const Icon(Icons.notifications_active, color: Colors.blueAccent, size: 30),
|
|
),
|
|
Positioned(
|
|
right: 8, top: 8,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
|
|
constraints: const BoxConstraints(minWidth: 8, minHeight: 8),
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildRecentRouteItem(dynamic route) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
String name = route['nama_rute'] ?? '-';
|
|
String distance = '${route['jarak_tempuh'] ?? route['jarak'] ?? 0} km';
|
|
String statusText = route['status']?.toString().toUpperCase() ?? 'GAGAL';
|
|
if (statusText == "0") statusText = "GAGAL";
|
|
|
|
return GestureDetector(
|
|
onTap: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => HistoryDetailScreen(historyItem: route),
|
|
),
|
|
);
|
|
},
|
|
child: Container(
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
|
|
borderRadius: BorderRadius.circular(24),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(isDark ? 0.2 : 0.03),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
border: isDark ? Border.all(color: Colors.white.withOpacity(0.05), width: 1) : null,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [Colors.blue.shade400, Colors.blue.shade700],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(color: Colors.blue.withOpacity(isDark ? 0.1 : 0.3), blurRadius: 8, offset: const Offset(0, 4))
|
|
],
|
|
),
|
|
child: const Icon(Icons.directions_bike_rounded, color: Colors.white, size: 24),
|
|
),
|
|
const SizedBox(width: 18),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
name,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 15,
|
|
color: isDark ? Colors.white : const Color(0xFF1E293B)
|
|
),
|
|
overflow: TextOverflow.ellipsis
|
|
),
|
|
const SizedBox(height: 6),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.straighten, size: 14, color: isDark ? Colors.blueGrey.shade400 : Colors.blueGrey.shade300),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
distance,
|
|
style: TextStyle(
|
|
color: isDark ? Colors.blueGrey.shade300 : Colors.blueGrey.shade400,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500
|
|
)
|
|
),
|
|
const SizedBox(width: 12),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: statusText == "BERHASIL" ? Colors.green.withOpacity(0.1) : Colors.red.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
statusText,
|
|
style: TextStyle(
|
|
color: statusText == "BERHASIL" ? Colors.greenAccent : Colors.redAccent,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w900
|
|
)
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.arrow_forward_ios_rounded, size: 16, color: isDark ? Colors.white24 : Colors.blueGrey.shade200),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|