453 lines
18 KiB
Dart
453 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
|
|
import 'dashboard_admin.dart';
|
|
import 'dashboard_guru.dart';
|
|
import 'dashboard_ortu.dart';
|
|
|
|
class LoginPage extends StatefulWidget {
|
|
const LoginPage({super.key});
|
|
|
|
@override
|
|
State<LoginPage> createState() => _LoginPageState();
|
|
}
|
|
|
|
class _LoginPageState extends State<LoginPage> {
|
|
final email = TextEditingController();
|
|
final password = TextEditingController();
|
|
|
|
bool isLoading = false;
|
|
bool obscurePassword = true;
|
|
|
|
@override
|
|
void dispose() {
|
|
email.dispose();
|
|
password.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> login() async {
|
|
if (email.text.trim().isEmpty || password.text.trim().isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Email dan Password wajib diisi'),
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() => isLoading = true);
|
|
|
|
try {
|
|
final userCred = await FirebaseAuth.instance.signInWithEmailAndPassword(
|
|
email: email.text.trim(),
|
|
password: password.text.trim(),
|
|
);
|
|
|
|
final doc = await FirebaseFirestore.instance
|
|
.collection('users')
|
|
.doc(userCred.user!.uid)
|
|
.get();
|
|
|
|
final data = doc.data();
|
|
if (data == null) throw Exception("Data pengguna tidak ditemukan");
|
|
|
|
final role = data['role'];
|
|
if (!mounted) return;
|
|
|
|
if (role == 'admin') {
|
|
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const DashboardAdmin()));
|
|
} else if (role == 'guru') {
|
|
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const DashboardGuru()));
|
|
} else {
|
|
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const DashboardOrtu()));
|
|
}
|
|
} on FirebaseAuthException catch (e) {
|
|
String pesan = "Login gagal";
|
|
if (e.code == "user-not-found") pesan = "Pengguna tidak ditemukan";
|
|
else if (e.code == "wrong-password") pesan = "Kata sandi salah";
|
|
else if (e.code == "invalid-email") pesan = "Format email tidak valid";
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(pesan),
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text("Kesalahan: $e"),
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
if (mounted) setState(() => isLoading = false);
|
|
}
|
|
|
|
// 📝 Diperkecil tinggi container field dari 54 menjadi 48
|
|
Widget inputField({
|
|
required TextEditingController controller,
|
|
required String hint,
|
|
required IconData icon,
|
|
bool obscure = false,
|
|
Widget? suffixIcon,
|
|
}) {
|
|
return Container(
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: const [
|
|
BoxShadow(
|
|
color: Color(0x0F7F56D9),
|
|
blurRadius: 8,
|
|
offset: Offset(0, 3),
|
|
)
|
|
],
|
|
border: Border.all(color: const Color(0xffE6E0FF), width: 1),
|
|
),
|
|
child: TextField(
|
|
controller: controller,
|
|
obscureText: obscure,
|
|
style: const TextStyle(fontSize: 14, color: Color(0xff2D2669)),
|
|
decoration: InputDecoration(
|
|
border: InputBorder.none,
|
|
hintText: hint,
|
|
hintStyle: const TextStyle(color: Color(0xffA098C7), fontSize: 13),
|
|
prefixIcon: Icon(icon, color: const Color(0xff7F56D9), size: 18),
|
|
suffixIcon: suffixIcon,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final screenSize = MediaQuery.of(context).size;
|
|
final screenHeight = screenSize.height;
|
|
|
|
// ⚡ Penyesuaian proporsi tinggi komponen khusus layar Vivo Y02t agar lebih ringkas
|
|
final double illustrationHeight = screenHeight * 0.14; // Diturunkan ke ~14%
|
|
final double spacingTop = screenHeight * 0.015;
|
|
final double spacingMiddle = screenHeight * 0.018;
|
|
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xffF5F2FF),
|
|
resizeToAvoidBottomInset: true,
|
|
body: SafeArea(
|
|
child: SingleChildScrollView(
|
|
physics: const ClampingScrollPhysics(),
|
|
child: Stack(
|
|
children: [
|
|
// DEKORASI LATAR BELAKANG
|
|
_buildBackgroundDecorations(screenHeight),
|
|
|
|
// KONTEN UTAMA
|
|
Center(
|
|
child: Container(
|
|
constraints: const BoxConstraints(maxWidth: 400),
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
SizedBox(height: spacingTop),
|
|
|
|
// Logo Sekolah (Diperkecil sedikit rasio layarnya)
|
|
Image.asset(
|
|
'lib/assets/sekolah.png',
|
|
height: screenHeight * 0.07,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (_, __, ___) => const Icon(
|
|
Icons.school,
|
|
size: 55,
|
|
color: Color(0xff7F56D9),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
// Ukuran font judul dikurangi agar tidak memakan tempat banyak
|
|
const Text(
|
|
"ABSENSI",
|
|
style: TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xff2D2669),
|
|
letterSpacing: 1.2,
|
|
),
|
|
),
|
|
const Text(
|
|
"— SISTEM ABSENSI PGRI —",
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Color(0xff7F56D9),
|
|
fontWeight: FontWeight.w500,
|
|
letterSpacing: 0.8,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
const Text(
|
|
"TK PGRI BHAKTI LESTARI",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xff2D2669),
|
|
),
|
|
),
|
|
|
|
SizedBox(height: spacingMiddle),
|
|
|
|
// ILUSTRASI ANAK (RESPONSIF & DIBATASI LEBIH KECIL)
|
|
_buildStudentIllustration(illustrationHeight),
|
|
|
|
SizedBox(height: spacingMiddle),
|
|
|
|
// KOTAK LOGIN (Padding dikurangi)
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: const [
|
|
BoxShadow(
|
|
color: Color(0x127F56D9),
|
|
blurRadius: 15,
|
|
offset: Offset(0, 4),
|
|
)
|
|
],
|
|
border: Border.all(color: const Color(0xffE6E0FF), width: 1.2),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
const Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text("≡ ", style: TextStyle(fontSize: 16, color: Color(0xff7F56D9), fontWeight: FontWeight.bold)),
|
|
Text("Login", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xff2D2669))),
|
|
Text(" ≡", style: TextStyle(fontSize: 16, color: Color(0xff7F56D9), fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
inputField(
|
|
controller: email,
|
|
hint: "Email",
|
|
icon: Icons.email_outlined,
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
inputField(
|
|
controller: password,
|
|
hint: "Password",
|
|
icon: Icons.lock_outline,
|
|
obscure: obscurePassword,
|
|
suffixIcon: IconButton(
|
|
icon: Icon(
|
|
obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined,
|
|
color: const Color(0xffA098C7),
|
|
size: 18,
|
|
),
|
|
onPressed: () => setState(() => obscurePassword = !obscurePassword),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Tombol Login (Tinggi diturunkan dari 50 ke 44)
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 44,
|
|
child: ElevatedButton(
|
|
onPressed: isLoading ? null : login,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xff7F56D9),
|
|
disabledBackgroundColor: const Color(0xffB7A3E8),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
elevation: 1.5,
|
|
shadowColor: const Color(0xff7F56D9).withOpacity(0.2),
|
|
),
|
|
child: isLoading
|
|
? const SizedBox(
|
|
height: 20,
|
|
width: 20,
|
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.2),
|
|
)
|
|
: const Text(
|
|
"Login",
|
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
const Text(
|
|
"Gunakan akun yang telah terdaftar",
|
|
style: TextStyle(fontSize: 10, color: Color(0xff7F56D9)),
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
// Garis pemisah
|
|
Row(
|
|
children: [
|
|
const Expanded(child: Divider(color: Color(0xffE0DBF5), thickness: 1, indent: 6, endIndent: 6)),
|
|
Container(
|
|
padding: const EdgeInsets.all(5),
|
|
decoration: const BoxDecoration(color: Color(0xffF5F2FF), shape: BoxShape.circle),
|
|
child: const Icon(Icons.shield_outlined, color: Color(0xff7F56D9), size: 12),
|
|
),
|
|
const Expanded(child: Divider(color: Color(0xffE0DBF5), thickness: 1, indent: 6, endIndent: 6)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// INFORMASI PENGEMBANG (Diperkecil padding dan ukuran teksnya)
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xffEDE9FF),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xffDCD4FF)),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text("Dikembangkan oleh:", style: TextStyle(fontSize: 9, color: Color(0xff7F56D9), fontWeight: FontWeight.w500)),
|
|
SizedBox(height: 1),
|
|
Text("Quri Amaliatas Solehah", style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xff2D2669))),
|
|
SizedBox(height: 1),
|
|
Text("Politeknik Negeri Jember", style: TextStyle(fontSize: 10, color: Color(0xff4A4385))),
|
|
Text("Jurusan Teknologi Informasi | Teknik Komputer 2023", style: TextStyle(fontSize: 8, color: Color(0xff7F56D9))),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Image.asset(
|
|
'lib/assets/jti.png',
|
|
height: 18,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (_, __, ___) => const Icon(Icons.school_outlined, size: 18, color: Color(0xff7F56D9)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBackgroundDecorations(double height) {
|
|
return Positioned.fill(
|
|
child: Stack(
|
|
children: [
|
|
Positioned(
|
|
top: -50,
|
|
left: -50,
|
|
child: Container(width: 140, height: 140, decoration: BoxDecoration(color: const Color(0xff7F56D9).withOpacity(0.05), shape: BoxShape.circle)),
|
|
),
|
|
Positioned(
|
|
top: 40,
|
|
right: -25,
|
|
child: Container(width: 80, height: 80, decoration: BoxDecoration(color: const Color(0xff7F56D9).withOpacity(0.04), shape: BoxShape.circle)),
|
|
),
|
|
Positioned(
|
|
bottom: -30,
|
|
left: -15,
|
|
child: Container(width: 100, height: 100, decoration: BoxDecoration(color: const Color(0xff7F56D9).withOpacity(0.04), shape: BoxShape.circle)),
|
|
),
|
|
Positioned(top: 40, left: 20, child: _buildDotGrid()),
|
|
Positioned(top: 60, right: 30, child: _buildDotGrid()),
|
|
Positioned(bottom: 80, right: 20, child: _buildDotGrid()),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDotGrid() {
|
|
return Opacity(
|
|
opacity: 0.15,
|
|
child: Column(
|
|
children: List.generate(3, (_) => Row(
|
|
children: List.generate(3, (_) => Container(
|
|
width: 2.5,
|
|
height: 2.5,
|
|
margin: const EdgeInsets.all(2.0),
|
|
decoration: const BoxDecoration(color: Color(0xff7F56D9), shape: BoxShape.circle),
|
|
)),
|
|
)),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStudentIllustration(double dynamicHeight) {
|
|
return Container(
|
|
width: double.infinity,
|
|
height: dynamicHeight.clamp(110.0, 140.0), // Rentang clamp diturunkan agar pas di layar Vivo Y02t
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [const Color(0xffEDE9FF), const Color(0xffDCD4FF).withOpacity(0.5)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xffD1C8FF), width: 1),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Positioned(top: 8, left: 12, child: Icon(Icons.cloud_outlined, color: const Color(0xff7F56D9).withOpacity(0.25), size: 18)),
|
|
Positioned(bottom: 12, right: 16, child: Icon(Icons.star_border, color: const Color(0xff7F56D9).withOpacity(0.25), size: 16)),
|
|
Center(
|
|
child: Image.asset(
|
|
'lib/assets/anak_rfid.png',
|
|
height: (dynamicHeight - 20).clamp(90.0, 120.0),
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (_, __, ___) => const Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.image_not_supported, size: 35, color: Color(0xff7F56D9)),
|
|
SizedBox(height: 2),
|
|
Text("Masukkan gambar ilustrasi_anak.png", style: TextStyle(color: Color(0xff7F56D9), fontSize: 10)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |