MIF_E31230892/sim_mobile/lib/features/dashboard/dashboard_page.dart

890 lines
29 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lib/features/dashboard/dashboard_page.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/api/api_service.dart';
// ─── Color palette ───
const _primary = Color(0xFF6FBA9D);
const _primaryDark = Color(0xFF4D987B);
const _primaryLight = Color(0xFFE8F7F2);
const _bg = Color(0xFFF8FBF9);
class DashboardPage extends StatefulWidget {
const DashboardPage({super.key});
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
final _api = ApiService();
Map<String, dynamic>? _santriData;
bool _isLoading = true;
bool _saldoVisible = false;
// Summary data
String _saldo = '-';
String _sppStatus = '-';
bool _sppLunas = false;
List<Map<String, dynamic>> _beritaList = [];
List<int> _kehadiran = List.filled(7, -1); // -1=belum, 1=hadir, 0=alpa
// Kepulangan data
bool _sedangPulang = false;
String _tanggalKembali = '';
int _sisaHari = 0;
String _statusKepulangan = '';
@override
void initState() {
super.initState();
_loadData();
}
// ─── Data loading (existing logic preserved) ───
Future<void> _loadData() async {
final prefs = await SharedPreferences.getInstance();
final santriJson = prefs.getString('santri_data');
if (santriJson != null) {
setState(() {
_santriData = json.decode(santriJson);
_isLoading = false;
});
} else {
await _fetchProfile();
}
_fetchSummaries();
}
Future<void> _fetchProfile() async {
setState(() => _isLoading = true);
final result = await _api.getProfile();
if (mounted) {
if (result['success'] == true && result['data'] != null) {
setState(() {
_santriData = result['data'];
_isLoading = false;
});
} else {
setState(() => _isLoading = false);
if (result['message'] != null) _showSnackBar(result['message']);
}
}
}
Future<void> _fetchSummaries() async {
// Saldo uang saku
try {
final saldoRes = await _api.getSaldoUangSaku();
if (mounted && saldoRes['success'] == true) {
final data = saldoRes['data'];
if (data != null && data is Map) {
final saldoVal = data['saldo'] ?? data['total_saldo'] ?? 0;
setState(() => _saldo = _formatRupiah(saldoVal));
}
}
} catch (_) {}
// SPP bulan ini
try {
final sppRes = await _api.getStatusSppBulanIni();
if (mounted && sppRes['success'] == true) {
final data = sppRes['data'];
if (data != null && data is Map) {
final lunas = data['status'] == 'Lunas' || data['lunas'] == true;
setState(() {
_sppLunas = lunas;
_sppStatus = lunas ? 'Lunas' : 'Belum Lunas';
});
}
}
} catch (_) {}
// Berita terbaru
try {
final beritaRes = await _api.getBerita(page: 1);
if (mounted && beritaRes['success'] == true) {
final raw = beritaRes['data'];
List items = [];
if (raw is List) {
items = raw;
} else if (raw is Map && raw['data'] is List) {
items = raw['data'] as List;
}
if (items.isNotEmpty) {
setState(() {
_beritaList = items
.take(3)
.whereType<Map<String, dynamic>>()
.toList();
});
}
}
} catch (_) {}
// Kehadiran minggu ini placeholder sampai API tersedia
if (mounted) {
setState(() {
_kehadiran = [1, 1, 1, 0, 1, -1, -1];
});
}
// Notifikasi kepulangan
try {
final kepRes = await _api.getNotifikasiKepulangan();
if (mounted && kepRes['success'] == true) {
final data = kepRes['data'];
if (data != null && data is Map) {
setState(() {
_sedangPulang = data['sedang_pulang'] == true;
_tanggalKembali = data['tanggal_kembali_formatted'] ?? data['tanggal_kembali'] ?? '';
_sisaHari = data['sisa_hari'] ?? 0;
_statusKepulangan = data['status'] ?? '';
});
}
}
} catch (_) {}
}
String _formatRupiah(dynamic value) {
final n = int.tryParse(value.toString()) ?? 0;
final s = n.abs().toString();
final buf = StringBuffer();
for (var i = 0; i < s.length; i++) {
if (i > 0 && (s.length - i) % 3 == 0) buf.write('.');
buf.write(s[i]);
}
return 'Rp ${n < 0 ? '-' : ''}${buf.toString()}';
}
Future<void> _handleLogout() async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Row(
children: [
Icon(Icons.logout_rounded, color: Colors.redAccent, size: 22),
SizedBox(width: 8),
Text('Konfirmasi Logout', style: TextStyle(fontSize: 16)),
],
),
content: const Text('Yakin ingin keluar dari aplikasi?'),
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text('Batal', style: TextStyle(color: Colors.grey[600])),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
child: const Text('Logout'),
),
],
),
);
if (confirm == true && mounted) {
await _api.logout();
if (mounted) {
Navigator.pushNamedAndRemoveUntil(context, '/login', (r) => false);
}
}
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message, maxLines: 2, overflow: TextOverflow.ellipsis),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// BUILD
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _bg,
body: _isLoading
? const Center(child: CircularProgressIndicator(color: _primary))
: RefreshIndicator(
color: _primary,
onRefresh: () async {
await _fetchProfile();
await _fetchSummaries();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 16),
if (_sedangPulang) ...[
_buildKepulanganBanner(),
const SizedBox(height: 16),
],
_buildSummaryCards(),
const SizedBox(height: 20),
_buildKehadiranSection(),
const SizedBox(height: 20),
_buildBeritaSection(),
const SizedBox(height: 20),
_buildMenuSection(),
const SizedBox(height: 24),
],
),
),
),
);
}
// ━━━ 1) HEADER ━━━
Widget _buildHeader() {
final nama = _santriData?['nama_lengkap'] ?? 'Santri';
final kelas = _santriData?['kelas'] ?? '-';
final status = _santriData?['status'] ?? 'Aktif';
final namaWali =
_santriData?['nama_wali'] ?? _santriData?['wali'] ?? '';
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [_primary, _primaryDark],
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(28),
bottomRight: Radius.circular(28),
),
),
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── top row: greeting + actions ──
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Assalamualaikum! \u{1F44B}',
style: TextStyle(
fontSize: 14,
color: Colors.white.withValues(alpha: 0.8),
),
),
const SizedBox(height: 2),
Text(
namaWali.isNotEmpty ? namaWali : 'Wali Santri',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
_headerIcon(Icons.notifications_none_rounded, () {}),
const SizedBox(width: 8),
_headerIcon(Icons.logout_rounded, _handleLogout),
],
),
const SizedBox(height: 16),
// ── santri info card ──
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: _primaryDark.withValues(alpha: 0.18),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [_primary, _primaryDark]),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(Icons.person_rounded,
color: Colors.white, size: 26),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
nama,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Color(0xFF1A1A2E),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text('Kelas $kelas',
style: TextStyle(
fontSize: 12, color: Colors.grey[600])),
],
),
),
// status badge
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: status == 'Aktif'
? _primaryLight
: const Color(0xFFFFF3E0),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color:
status == 'Aktif' ? _primary : Colors.orange,
),
),
const SizedBox(width: 5),
Text(
status,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: status == 'Aktif'
? _primaryDark
: Colors.orange[800],
),
),
],
),
),
],
),
),
],
),
),
),
);
}
Widget _headerIcon(IconData icon, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(11),
),
child: Icon(icon, color: Colors.white, size: 20),
),
);
}
// ━━━ KEPULANGAN BANNER ━━━
Widget _buildKepulanganBanner() {
final isLate = _statusKepulangan == 'terlambat';
final bgColor = isLate ? const Color(0xFFFEE2E2) : const Color(0xFFFFF7ED);
final accentColor = isLate ? const Color(0xFFDC2626) : const Color(0xFFF59E0B);
final textColor = isLate ? const Color(0xFF991B1B) : const Color(0xFF92400E);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: accentColor.withValues(alpha: 0.3)),
),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
isLate ? Icons.warning_rounded : Icons.home_rounded,
color: accentColor,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isLate ? 'Terlambat Kembali!' : 'Sedang Pulang',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: textColor,
),
),
const SizedBox(height: 4),
Text(
isLate
? 'Sudah lewat ${_sisaHari.abs()} hari dari jadwal kembali'
: 'Kembali $_tanggalKembali ($_sisaHari hari lagi)',
style: TextStyle(fontSize: 12, color: textColor.withValues(alpha: 0.8)),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: accentColor,
borderRadius: BorderRadius.circular(20),
),
child: Text(
isLate ? 'Terlambat' : '$_sisaHari hari',
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
),
],
),
),
);
}
// ━━━ 2) SUMMARY CARDS ━━━
Widget _buildSummaryCards() {
return SizedBox(
height: 100,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
_summaryCard(
icon: Icons.account_balance_wallet_rounded,
label: 'Saldo Uang Saku',
value: _saldoVisible ? _saldo : '\u2022\u2022\u2022\u2022\u2022\u2022',
gradient: const [_primary, _primaryDark],
trailing: GestureDetector(
onTap: () => setState(() => _saldoVisible = !_saldoVisible),
child: Icon(
_saldoVisible
? Icons.visibility_rounded
: Icons.visibility_off_rounded,
color: Colors.white70,
size: 20,
),
),
),
const SizedBox(width: 12),
_summaryCard(
icon: Icons.receipt_long_rounded,
label: 'SPP Bulan Ini',
value: _sppStatus,
gradient: _sppLunas
? [const Color(0xFF10B981), const Color(0xFF059669)]
: [const Color(0xFFF59E0B), const Color(0xFFD97706)],
trailing: Icon(
_sppLunas
? Icons.check_circle_rounded
: Icons.warning_rounded,
color: Colors.white70,
size: 20,
),
),
],
),
);
}
Widget _summaryCard({
required IconData icon,
required String label,
required String value,
required List<Color> gradient,
Widget? trailing,
}) {
return Container(
width: 200,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: gradient,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: gradient.first.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(icon, color: Colors.white, size: 18),
const SizedBox(width: 6),
Expanded(
child: Text(label,
style:
const TextStyle(color: Colors.white70, fontSize: 11),
overflow: TextOverflow.ellipsis),
),
if (trailing != null) trailing,
],
),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
// ━━━ 3) KEHADIRAN MINGGU INI ━━━
Widget _buildKehadiranSection() {
const days = ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Ahd'];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Kehadiran Minggu Ini', Icons.calendar_today_rounded),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(14),
decoration: _cardDeco(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(7, (i) {
final st = _kehadiran[i];
final Color bg;
final Color fg;
final IconData? ico;
if (st == 1) {
bg = _primary;
fg = Colors.white;
ico = Icons.check_rounded;
} else if (st == 0) {
bg = const Color(0xFFEF4444);
fg = Colors.white;
ico = Icons.close_rounded;
} else {
bg = Colors.grey[200]!;
fg = Colors.grey[400]!;
ico = null;
}
return Column(
children: [
Text(days[i],
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: 34,
height: 34,
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(10),
boxShadow: st == 1
? [
BoxShadow(
color:
_primary.withValues(alpha: 0.3),
blurRadius: 6,
offset: const Offset(0, 2))
]
: null,
),
child: ico != null
? Icon(ico, size: 18, color: fg)
: Center(
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: fg, shape: BoxShape.circle),
),
),
),
],
);
}),
),
),
],
),
);
}
// ━━━ 4) BERITA TERBARU ━━━
Widget _buildBeritaSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
_sectionTitle('Berita Terbaru', Icons.article_rounded),
const Spacer(),
GestureDetector(
onTap: () => Navigator.pushNamed(context, '/berita'),
child: const Text('Lihat Semua',
style: TextStyle(
fontSize: 12,
color: _primary,
fontWeight: FontWeight.w600)),
),
],
),
),
const SizedBox(height: 12),
SizedBox(
height: 140,
child: _beritaList.isEmpty
? Center(
child: Text('Belum ada berita',
style:
TextStyle(color: Colors.grey[400], fontSize: 12)))
: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: _beritaList.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (_, i) => _beritaCard(_beritaList[i]),
),
),
],
);
}
Widget _beritaCard(Map<String, dynamic> b) {
final judul = b['judul'] ?? b['title'] ?? 'Berita';
final tanggal = b['tanggal'] ?? b['created_at'] ?? '';
final kategori = b['kategori'] ?? b['category'] ?? '';
return GestureDetector(
onTap: () => Navigator.pushNamed(context, '/berita'),
child: Container(
width: 220,
padding: const EdgeInsets.all(14),
decoration: _cardDeco(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (kategori.toString().isNotEmpty)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: _primaryLight,
borderRadius: BorderRadius.circular(6),
),
child: Text(kategori.toString(),
style: const TextStyle(
fontSize: 9,
color: _primaryDark,
fontWeight: FontWeight.w600)),
),
if (kategori.toString().isNotEmpty) const SizedBox(height: 8),
Expanded(
child: Text(
judul.toString(),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A2E),
height: 1.3,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 6),
Row(
children: [
Icon(Icons.access_time_rounded,
size: 12, color: Colors.grey[400]),
const SizedBox(width: 4),
Expanded(
child: Text(
tanggal.toString().length > 10
? tanggal.toString().substring(0, 10)
: tanggal.toString(),
style: TextStyle(fontSize: 10, color: Colors.grey[400]),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
);
}
// ━━━ 5) MENU LAINNYA ━━━
Widget _buildMenuSection() {
final items = [
_QMenu(Icons.flight_takeoff_rounded, 'Kepulangan', '/kepulangan'),
_QMenu(Icons.calendar_month_rounded, 'Absensi', '/absensi'),
_QMenu(Icons.warning_amber_rounded, 'Pelanggaran', '/pelanggaran'),
_QMenu(Icons.payments_rounded, 'SPP', '/spp'),
_QMenu(Icons.local_hospital_rounded, 'Kesehatan', '/kesehatan'),
_QMenu(Icons.account_balance_wallet_rounded, 'Uang Saku', '/uang-saku'),
];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Menu Lainnya', Icons.grid_view_rounded),
const SizedBox(height: 12),
GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1,
children: items.map(_menuTile).toList(),
),
],
),
);
}
Widget _menuTile(_QMenu m) {
return GestureDetector(
onTap: () => Navigator.pushNamed(context, m.route),
child: Container(
decoration: _cardDeco(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: _primaryLight,
borderRadius: BorderRadius.circular(13),
),
child: Icon(m.icon, size: 22, color: _primary),
),
const SizedBox(height: 8),
Text(
m.label,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A2E),
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
// ─── shared helpers ───
Widget _sectionTitle(String text, IconData icon) {
return Row(
children: [
Icon(icon, size: 18, color: _primary),
const SizedBox(width: 6),
Text(text,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Color(0xFF1A1A2E))),
],
);
}
BoxDecoration _cardDeco() => BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
);
}
class _QMenu {
final IconData icon;
final String label;
final String route;
const _QMenu(this.icon, this.label, this.route);
}