435 lines
16 KiB
Dart
435 lines
16 KiB
Dart
import 'dart:io';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:firebase_database/firebase_database.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:excel/excel.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
import 'rfid/nilai_praktik.dart';
|
|
import 'riwayat_page.dart';
|
|
|
|
class HomePage extends StatefulWidget {
|
|
const HomePage({super.key});
|
|
|
|
@override
|
|
State<HomePage> createState() => _HomePageState();
|
|
}
|
|
|
|
class _HomePageState extends State<HomePage> {
|
|
// 🎨 THEME WARNA BARU (SESUAI PERMINTAAN)
|
|
static const Color primaryColor = Color(0xff4E7C34);
|
|
static const Color secondaryColor = Color(0xff6A994E);
|
|
static const Color backgroundColor = Color(0xffFFF8E8);
|
|
|
|
// 🔥 EXPORT EXCEL
|
|
|
|
Future<void> exportToExcel() async {
|
|
if (!mounted) return;
|
|
|
|
try {
|
|
final storagePermission = await Permission.storage.request();
|
|
if (!storagePermission.isGranted) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text("Izin penyimpanan belum diberikan")));
|
|
return;
|
|
}
|
|
|
|
final snapshot = await FirebaseDatabase.instance.ref("cards").get();
|
|
|
|
if (!snapshot.exists || snapshot.value == null) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text("Data kosong")));
|
|
return;
|
|
}
|
|
|
|
final data = Map<String, dynamic>.from(snapshot.value as Map);
|
|
|
|
final excel = Excel.createExcel();
|
|
final Sheet sheet = excel['Nilai'];
|
|
|
|
sheet.appendRow([
|
|
TextCellValue('UID'),
|
|
TextCellValue('Nama'),
|
|
TextCellValue('Nilai'),
|
|
TextCellValue('Tanggal & Jam'),
|
|
]);
|
|
|
|
for (final entry in data.entries) {
|
|
final uid = entry.key;
|
|
final card = Map<String, dynamic>.from(entry.value as Map);
|
|
final nilai = card['nilai'] ?? 0;
|
|
|
|
final historySnapshot = await FirebaseDatabase.instance
|
|
.ref("history/$uid")
|
|
.get();
|
|
|
|
String tanggalJam = "-";
|
|
|
|
if (historySnapshot.exists && historySnapshot.value != null) {
|
|
final historyData = Map<String, dynamic>.from(
|
|
historySnapshot.value as Map,
|
|
);
|
|
|
|
final historyList = historyData.entries
|
|
.map((item) => Map<String, dynamic>.from(item.value as Map))
|
|
.toList();
|
|
|
|
historyList.sort((a, b) {
|
|
final aTime = ((a['timestamp'] ?? 0) as num).toInt();
|
|
final bTime = ((b['timestamp'] ?? 0) as num).toInt();
|
|
return bTime.compareTo(aTime);
|
|
});
|
|
|
|
if (historyList.isNotEmpty) {
|
|
final lastHistory = historyList.first;
|
|
final timestamp = (lastHistory['timestamp'] as num?)?.toInt() ?? 0;
|
|
|
|
if (timestamp > 0) {
|
|
final date = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
|
tanggalJam =
|
|
"${DateFormat('dd-MM-yyyy').format(date)} ${DateFormat('HH:mm:ss').format(date)}";
|
|
}
|
|
}
|
|
}
|
|
|
|
sheet.appendRow([
|
|
TextCellValue(uid),
|
|
TextCellValue(card['name'] ?? '-'),
|
|
IntCellValue(nilai),
|
|
TextCellValue(tanggalJam),
|
|
]);
|
|
}
|
|
|
|
Directory directory;
|
|
|
|
if (Platform.isAndroid) {
|
|
final downloadsDir = Directory('/storage/emulated/0/Download');
|
|
if (await downloadsDir.exists()) {
|
|
directory = downloadsDir;
|
|
} else {
|
|
final appDir = await getApplicationDocumentsDirectory();
|
|
directory = appDir;
|
|
}
|
|
} else {
|
|
directory = await getApplicationDocumentsDirectory();
|
|
}
|
|
|
|
final path =
|
|
"${directory.path}/nilai_${DateTime.now().millisecondsSinceEpoch}.xlsx";
|
|
final file = File(path);
|
|
|
|
if (await file.exists()) {
|
|
await file.delete();
|
|
}
|
|
|
|
final bytes = excel.encode();
|
|
|
|
if (bytes != null) {
|
|
await file.create(recursive: true);
|
|
await file.writeAsBytes(bytes);
|
|
}
|
|
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text("Berhasil download:\n$path")));
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text("Error: $e")));
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final userUid = FirebaseAuth.instance.currentUser!.uid;
|
|
|
|
return FutureBuilder<DocumentSnapshot>(
|
|
future: FirebaseFirestore.instance.collection("users").doc(userUid).get(),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const Scaffold(
|
|
body: Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
|
|
final data = snapshot.data?.data() as Map<String, dynamic>?;
|
|
final userName = data?['name'] ?? 'USER';
|
|
|
|
return Scaffold(
|
|
backgroundColor: backgroundColor,
|
|
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
// 🔥 HEADER
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
backgroundColor,
|
|
secondaryColor.withOpacity(0.15),
|
|
],
|
|
),
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 70,
|
|
height: 70,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(18),
|
|
color: Colors.white,
|
|
),
|
|
child: const Padding(
|
|
padding: EdgeInsets.all(8),
|
|
child: Image(image: AssetImage("assets/logo.png")),
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 14),
|
|
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"HALO, ${userName.toUpperCase()}",
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 22,
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
const Text(
|
|
"Monitoring Nilai Papan Pembelajaran",
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: secondaryColor,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
const Text(
|
|
"Silahkan Daftarkan Nama Siswa di Halaman Kartu",
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// 🔥 BUTTON DOWNLOAD
|
|
Container(
|
|
width: double.infinity,
|
|
height: 65,
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [primaryColor, secondaryColor],
|
|
),
|
|
borderRadius: BorderRadius.circular(22),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
|
|
child: ElevatedButton.icon(
|
|
onPressed: exportToExcel,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.transparent,
|
|
shadowColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(22),
|
|
),
|
|
),
|
|
|
|
icon: const Icon(
|
|
Icons.download,
|
|
color: Colors.white,
|
|
size: 28,
|
|
),
|
|
|
|
label: const Text(
|
|
"Unduh Data Nilai Siswa",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 22),
|
|
|
|
// 🔥 LIST SISWA
|
|
Expanded(
|
|
child: StreamBuilder(
|
|
stream: FirebaseDatabase.instance.ref("cards").onValue,
|
|
|
|
builder: (context, snapshot) {
|
|
if (!snapshot.hasData ||
|
|
snapshot.data!.snapshot.value == null) {
|
|
return const Center(child: Text("Belum ada data"));
|
|
}
|
|
|
|
final cards = Map<String, dynamic>.from(
|
|
snapshot.data!.snapshot.value as Map,
|
|
);
|
|
|
|
final entries = cards.entries.toList();
|
|
|
|
return ListView.builder(
|
|
itemCount: entries.length,
|
|
itemBuilder: (context, index) {
|
|
final uid = entries[index].key;
|
|
|
|
final card = Map<String, dynamic>.from(
|
|
entries[index].value,
|
|
);
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 10),
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 55,
|
|
height: 55,
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [primaryColor, secondaryColor],
|
|
),
|
|
borderRadius: BorderRadius.circular(18),
|
|
),
|
|
child: const Icon(
|
|
Icons.person,
|
|
color: Colors.white,
|
|
size: 28,
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
card["name"] ?? "-",
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
"Nilai: ${card['nilai'] ?? 0}",
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) =>
|
|
RiwayatPage(uid: uid),
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(
|
|
Icons.history,
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
|
|
IconButton(
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => CardDetailPage(
|
|
uid: uid,
|
|
name: card["name"] ?? "-",
|
|
),
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(
|
|
Icons.arrow_forward_ios,
|
|
size: 18,
|
|
color: secondaryColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|