586 lines
20 KiB
Dart
586 lines
20 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import '../../../services/auth_service.dart';
|
|
import '../../../services/notification_admin_service.dart';
|
|
import 'notification_admin_screen.dart';
|
|
import 'data_petugas_screen.dart';
|
|
import 'laporan_admin_screen.dart';
|
|
|
|
class AdminHome extends StatefulWidget {
|
|
const AdminHome({super.key});
|
|
|
|
@override
|
|
State<AdminHome> createState() => _AdminHomeState();
|
|
}
|
|
|
|
class _AdminHomeState extends State<AdminHome> {
|
|
Map<String, dynamic> userData = {};
|
|
late String _timeString;
|
|
late Timer _timer;
|
|
late Timer _notifTimer;
|
|
|
|
int totalPetugas = 0;
|
|
int laporanMasuk = 0;
|
|
int laporanDisetujui = 0;
|
|
int laporanPending = 0;
|
|
|
|
int notifCount = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_timeString = DateFormat('HH:mm:ss').format(DateTime.now());
|
|
|
|
_timer = Timer.periodic(
|
|
const Duration(seconds: 1),
|
|
(Timer t) => _getCurrentTime(),
|
|
);
|
|
|
|
// 🔥 AUTO REFRESH NOTIF
|
|
_notifTimer = Timer.periodic(
|
|
const Duration(seconds: 5),
|
|
(Timer t) => loadNotif(),
|
|
);
|
|
|
|
fetchDashboard();
|
|
loadProfile();
|
|
loadNotif(); // pertama kali load
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer.cancel();
|
|
_notifTimer.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _getCurrentTime() {
|
|
final String formattedDateTime = DateFormat(
|
|
'HH:mm:ss',
|
|
).format(DateTime.now());
|
|
setState(() {
|
|
_timeString = formattedDateTime;
|
|
});
|
|
}
|
|
|
|
Future<void> loadNotif() async {
|
|
final count = await NotificationAdminService.getUnreadCount(); // ✅ FIX
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
|
notifCount = count;
|
|
});
|
|
}
|
|
|
|
String getTanggalHari() {
|
|
final now = DateTime.now();
|
|
return DateFormat('EEEE, dd MMMM yyyy', 'id_ID').format(now);
|
|
}
|
|
|
|
Future<void> fetchDashboard() async {
|
|
try {
|
|
String? token = await AuthService.getToken();
|
|
final response = await http.get(
|
|
Uri.parse("${AuthService.baseUrl}/dashboard-statistik"),
|
|
headers: {
|
|
"Accept": "application/json",
|
|
"Authorization": "Bearer $token",
|
|
},
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
setState(() {
|
|
totalPetugas = data['total_petugas'] ?? 0;
|
|
laporanMasuk = data['laporan_masuk'] ?? 0;
|
|
laporanPending = data['laporan_pending'] ?? 0;
|
|
laporanDisetujui = data['laporan_disetujui'] ?? 0;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Error dashboard: $e");
|
|
}
|
|
}
|
|
|
|
Future<void> loadProfile() async {
|
|
final result = await AuthService.getProfile();
|
|
|
|
if (!mounted) return;
|
|
|
|
if (result['success']) {
|
|
setState(() {
|
|
userData = result['data'];
|
|
});
|
|
}
|
|
}
|
|
|
|
// ================= STAT CARD (PERCANTIK) =================
|
|
Widget _buildStatCard(
|
|
String title,
|
|
String count,
|
|
Color color,
|
|
IconData icon,
|
|
) {
|
|
return Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: color.withOpacity(0.08),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
border: Border.all(color: color.withOpacity(0.1), width: 1.5),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(icon, color: color, size: 20),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
count,
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ================= MENU CARD (PERCANTIK) =================
|
|
Widget _buildMenuCard(
|
|
BuildContext context,
|
|
String title,
|
|
IconData icon,
|
|
Color color,
|
|
VoidCallback onTap,
|
|
) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(24),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(24),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 6),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, color: color, size: 30),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
title,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF5F7FB),
|
|
body: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
// ================= HEADER =================
|
|
Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
Container(
|
|
height: 240,
|
|
width: double.infinity,
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [Color(0xFF1A39B1), Color(0xFF4263EB)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(40),
|
|
bottomRight: Radius.circular(40),
|
|
),
|
|
),
|
|
padding: const EdgeInsets.fromLTRB(25, 60, 25, 20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Patroli Kampus',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 26,
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 0.8,
|
|
),
|
|
),
|
|
Text(
|
|
'Politeknik Negeri Jember',
|
|
style: TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
Row(
|
|
children: [
|
|
Stack(
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.notifications_rounded,
|
|
color: Colors.white,
|
|
size: 28,
|
|
),
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder:
|
|
(_) =>
|
|
const NotificationAdminScreen(),
|
|
),
|
|
).then((_) => loadNotif());
|
|
},
|
|
),
|
|
if (notifCount > 0)
|
|
Positioned(
|
|
right: 6,
|
|
top: 6,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(5),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.red,
|
|
shape: BoxShape.circle,
|
|
),
|
|
constraints: const BoxConstraints(
|
|
minWidth: 18,
|
|
minHeight: 18,
|
|
),
|
|
child: Text(
|
|
notifCount > 9
|
|
? '9+'
|
|
: notifCount.toString(),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(width: 10),
|
|
|
|
Hero(
|
|
tag: 'logo_polije',
|
|
child: Image.asset(
|
|
'assets/images/logopolije.png',
|
|
height: 60,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 8,
|
|
horizontal: 12,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.15),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
Icons.calendar_today_rounded,
|
|
size: 14,
|
|
color: Colors.white,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
getTanggalHari(),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(width: 15),
|
|
const VerticalDivider(
|
|
color: Colors.white54,
|
|
thickness: 1,
|
|
),
|
|
const Icon(
|
|
Icons.watch_later_rounded,
|
|
size: 14,
|
|
color: Colors.white,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
_timeString,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// ================= CARD PROFIL =================
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 185, left: 20, right: 20),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(24),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.06),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 10),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(3),
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: const Color(0xFF4263EB),
|
|
width: 2,
|
|
),
|
|
),
|
|
child: CircleAvatar(
|
|
radius: 26,
|
|
backgroundColor: const Color(0xFFF0F3FF),
|
|
backgroundImage:
|
|
(userData['foto'] != null &&
|
|
userData['foto'].toString().isNotEmpty)
|
|
? NetworkImage(userData['foto'])
|
|
: null,
|
|
child:
|
|
(userData['foto'] == null ||
|
|
userData['foto'].toString().isEmpty)
|
|
? const Icon(
|
|
Icons.admin_panel_settings_rounded,
|
|
color: Color(0xFF1A39B1),
|
|
size: 30,
|
|
)
|
|
: null,
|
|
),
|
|
),
|
|
const SizedBox(width: 15),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Selamat Datang kembali,',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
Text(
|
|
userData['role'] == 'super_admin' ? 'Super Admin' : 'Admin',
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFF1E293B),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 45),
|
|
|
|
// ================= CONTENT =================
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Ringkasan Laporan',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFF1E293B),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
Row(
|
|
children: [
|
|
_buildStatCard(
|
|
"Petugas",
|
|
totalPetugas.toString(),
|
|
Colors.blue,
|
|
Icons.people_rounded,
|
|
),
|
|
const SizedBox(width: 14),
|
|
_buildStatCard(
|
|
"Masuk",
|
|
laporanMasuk.toString(),
|
|
Colors.purple,
|
|
Icons.assignment_returned_rounded,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
Row(
|
|
children: [
|
|
_buildStatCard(
|
|
"Disetujui",
|
|
laporanDisetujui.toString(),
|
|
Colors.green,
|
|
Icons.check_circle_rounded,
|
|
),
|
|
const SizedBox(width: 14),
|
|
_buildStatCard(
|
|
"Pending",
|
|
laporanPending.toString(),
|
|
Colors.orange,
|
|
Icons.hourglass_empty_rounded,
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
const Text(
|
|
'Menu Utama',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFF1E293B),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
GridView.count(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
crossAxisCount: 2,
|
|
crossAxisSpacing: 18,
|
|
mainAxisSpacing: 18,
|
|
childAspectRatio: 1.25,
|
|
children: [
|
|
_buildMenuCard(
|
|
context,
|
|
"Data Petugas",
|
|
Icons.badge_rounded,
|
|
Colors.teal,
|
|
() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => const DataPetugasScreen(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
_buildMenuCard(
|
|
context,
|
|
"Riwayat Aktivitas",
|
|
Icons.history_edu_rounded,
|
|
Colors.deepOrangeAccent,
|
|
() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => const LaporanAdminScreen(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|