Files
bumrent_app/lib/app/modules/warga/views/warga_dashboard_view.dart
2025-07-09 16:01:10 +07:00

787 lines
25 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/warga_dashboard_controller.dart';
import '../views/warga_layout.dart';
import '../../../theme/app_colors.dart';
import '../../../widgets/app_drawer.dart';
import '../../../routes/app_routes.dart';
import 'package:intl/intl.dart';
class WargaDashboardView extends GetView<WargaDashboardController> {
const WargaDashboardView({super.key});
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
// Check if coming from login and trigger refresh
WidgetsBinding.instance.addPostFrameCallback((_) {
final args = Get.arguments;
final bool isFromLogin = args != null && args['from_login'] == true;
if (isFromLogin) {
// Trigger refresh after UI is built
controller.refreshData();
print(
'WargaDashboardView: Auto-refreshed data due to login navigation',
);
}
});
return WillPopScope(
onWillPop: () async => false, // Prevent back navigation
child: WargaLayout(
drawer: AppDrawer(
onNavItemTapped: controller.onNavItemTapped,
onLogout: controller.logout,
),
backgroundColor: AppColors.background,
appBar: AppBar(
elevation: 0,
backgroundColor: AppColors.primary,
title: const Text(
'Beranda',
style: TextStyle(fontWeight: FontWeight.w600),
),
centerTitle: true,
),
body: RefreshIndicator(
color: AppColors.primary,
onRefresh: () async {
// Re-fetch data when pulled down
await Future.delayed(const Duration(seconds: 1));
controller.refreshData();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildUserGreetingHeader(context),
_buildActionButtons(),
_buildActiveRentalsSection(context),
const SizedBox(height: 24),
],
),
),
),
),
);
}
// Modern welcome header with user profile
Widget _buildUserGreetingHeader(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// User avatar
Obx(() {
final avatarUrl = controller.userAvatar.value;
return Container(
height: 60,
width: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
color: Colors.white.withOpacity(0.2),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(30),
child:
avatarUrl != null && avatarUrl.isNotEmpty
? Image.network(
avatarUrl,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) =>
_buildAvatarFallback(),
loadingBuilder: (context, child, progress) {
if (progress == null) return child;
return _buildAvatarFallback();
},
)
: _buildAvatarFallback(),
),
);
}),
const SizedBox(width: 16),
// Greeting and name
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_getGreeting(),
style: const TextStyle(
fontSize: 15,
color: Colors.white70,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Obx(
() => Text(
controller.userName.value,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
],
),
),
),
);
}
// Action buttons in a horizontal scroll
Widget _buildActionButtons() {
// Define services - removed Langganan and Pengaduan
final services = [
{
'title': 'Aset Tunggal',
'icon': Icons.home_work_outlined,
'color': const Color(0xFF4CAF50),
'route': () => controller.navigateToRentals(),
},
{
'title': 'Paket',
'icon': Icons.widgets_outlined,
'color': const Color(0xFF2196F3),
'route': () => controller.toSewaAsetTabPaket(),
},
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
child: Text(
'Layanan Sewa',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
),
// Service cards in grid
GridView.count(
crossAxisCount: 2,
childAspectRatio: 1.5,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsets.symmetric(horizontal: 16),
mainAxisSpacing: 16,
crossAxisSpacing: 16,
children:
services
.map(
(service) => _buildServiceCard(
title: service['title'] as String,
icon: service['icon'] as IconData,
color: service['color'] as Color,
onTap: service['route'] as VoidCallback,
),
)
.toList(),
),
// Activity Summaries Section
Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 10),
child: Text(
'Ringkasan Aktivitas',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
),
// Summary Cards
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
// Sewa Diterima
Obx(
() => _buildActivityCard(
title: 'Sewa Diterima',
value: controller.diterimaCount.value.toString(),
icon: Icons.check_circle_outline,
color: AppColors.success,
onTap:
() =>
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 2}),
),
),
const SizedBox(height: 12),
// Tagihan Aktif
Obx(
() => _buildActivityCard(
title: 'Tagihan Aktif',
value: controller.tagihanAktifCount.value.toString(),
icon: Icons.receipt_long_outlined,
color: AppColors.warning,
onTap:
() =>
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 0}),
),
),
const SizedBox(height: 12),
// Denda Aktif
Obx(
() => _buildActivityCard(
title: 'Denda Aktif',
value: controller.dendaAktifCount.value.toString(),
icon: Icons.warning_amber_outlined,
color: AppColors.error,
onTap:
() =>
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 0}),
),
),
],
),
),
],
);
}
Widget _buildServiceCard({
required String title,
required IconData icon,
required Color color,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [color.withOpacity(0.7), color],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 6),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Stack(
children: [
// Background decoration
Positioned(
right: -15,
bottom: -15,
child: Container(
width: 90,
height: 90,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
shape: BoxShape.circle,
),
),
),
Positioned(
left: -20,
top: -20,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
shape: BoxShape.circle,
),
),
),
// Icon and text
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Icon(icon, color: Colors.white, size: 24),
),
Text(
title,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
],
),
),
),
);
}
// Active rentals section with improved card design
Widget _buildActiveRentalsSection(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Sewa Aktif',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
TextButton.icon(
onPressed: () => controller.onNavItemTapped(1),
icon: const Icon(Icons.arrow_forward, size: 18),
label: const Text('Lihat Semua'),
style: TextButton.styleFrom(foregroundColor: AppColors.primary),
),
],
),
const SizedBox(height: 12),
Obx(() {
if (controller.activeRentals.isEmpty) {
return _buildEmptyState(
message: 'Belum ada sewa aset yang aktif',
icon: Icons.inventory_2_outlined,
buttonText: 'Sewa Sekarang',
onPressed: () => controller.navigateToRentals(),
);
}
return Column(
children:
controller.activeRentals
.map((rental) => _buildModernRentalCard(rental))
.toList(),
);
}),
],
),
);
}
// Empty state widget with consistent design
Widget _buildEmptyState({
required String message,
required IconData icon,
required String buttonText,
required VoidCallback onPressed,
}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
border: Border.all(color: Colors.grey.shade100),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(icon, size: 40, color: AppColors.primary),
),
const SizedBox(height: 20),
Text(
message,
style: TextStyle(
fontSize: 16,
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
child: Text(
buttonText,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
],
),
);
}
// Modern rental card with better layout
Widget _buildModernRentalCard(Map<String, dynamic> rental) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 4),
blurRadius: 15,
),
],
border: Border.all(color: Colors.grey.shade100, width: 1.0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with glass effect
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.primary.withOpacity(0.04),
AppColors.primary.withOpacity(0.08),
],
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Row(
children: [
// Asset icon/gambar
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
color: AppColors.primary.withOpacity(0.08),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(14),
child:
rental['imageUrl'] != null &&
rental['imageUrl'].toString().isNotEmpty
? Image.network(
rental['imageUrl'],
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) => Icon(
Icons.local_shipping,
color: AppColors.primary,
size: 28,
),
)
: Icon(
Icons.local_shipping,
color: AppColors.primary,
size: 28,
),
),
),
const SizedBox(width: 16),
// Asset details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
rental['name'] ?? '-',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 4),
Text(
rental['waktuSewa'] ?? '',
style: TextStyle(
fontSize: 13,
color: AppColors.textSecondary,
),
),
],
),
),
// Price tag
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColors.primary.withOpacity(0.3),
width: 1.0,
),
),
child: Text(
rental['totalPrice'] ?? 'Rp 0',
style: TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
],
),
),
// Details and actions
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
child: Column(
children: [
// Details row
Row(
children: [
Expanded(
child: _buildInfoItem(
icon: Icons.timer_outlined,
title: 'Durasi',
value: rental['duration'] ?? '-',
),
),
Expanded(
child: _buildInfoItem(
icon: Icons.calendar_today_outlined,
title: 'Status',
value: rental['status'] ?? '-',
valueColor: AppColors.success,
),
),
],
),
const SizedBox(height: 16),
// Action buttons
if ((rental['can_extend'] ?? false) == true)
OutlinedButton.icon(
onPressed: () => controller.extendRental(rental['id']),
icon: const Icon(Icons.update, size: 18),
label: const Text('Perpanjang'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
side: BorderSide(color: AppColors.primary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
),
),
],
),
),
],
),
);
}
// Info item for displaying details
Widget _buildInfoItem({
required IconData icon,
required String title,
required String value,
Color? valueColor,
}) {
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(fontSize: 13, color: AppColors.textSecondary),
),
const SizedBox(height: 4),
Row(
children: [
Icon(icon, size: 16, color: AppColors.primary),
const SizedBox(width: 4),
Text(
value,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: valueColor ?? AppColors.textPrimary,
),
),
],
),
],
),
);
}
// Build avatar fallback for when image is not available
Widget _buildAvatarFallback() {
return Center(child: Icon(Icons.person, color: Colors.white70, size: 30));
}
// Get appropriate greeting based on time of day
String _getGreeting() {
final hour = DateTime.now().hour;
if (hour < 12) {
return 'Selamat Pagi';
} else if (hour < 17) {
return 'Selamat Siang';
} else {
return 'Selamat Malam';
}
}
// Build a summary card for activities
Widget _buildActivityCard({
required String title,
required String value,
required IconData icon,
required Color color,
required VoidCallback onTap,
}) {
return Card(
elevation: 1,
shadowColor: Colors.black12,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade200),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: AppColors.textSecondary.withOpacity(0.5),
size: 16,
),
],
),
),
),
);
}
}