Modul Pertemuan 4: Cloud Firestore, Role Management, & Dashboard Guru
🎯 Tujuan Pembelajaran
-
Siswa mampu mengatur Security Rules Cloud Firestore untuk aplikasi yang memiliki banyak pengguna.
-
Siswa mampu membuat sistem Role-Based Access Control (Membedakan Siswa dan Guru).
-
Siswa mampu mensinkronkan data dari Firebase Auth ke dalam dokumen Firestore (terutama untuk pengguna Google Sign-In).
-
Siswa mampu membuat modul CRUD khusus di Dashboard Guru untuk mengelola data tempat PKL siswa.
1. Setup Firestore & Aturan Keamanan (Rules)
Langkah pertama adalah menyiapkan database untuk menyimpan data users (pengguna) dan attendances (absensi).
Langkah-langkah:
-
Buka dashboard Firebase Console, masuk ke Build -> Firestore Database.
-
Klik Create database, pilih lokasi server (
asia-southeast2), lalu pilih Start in production mode. -
Setelah database selesai dibuat, masuk ke tab Rules.
-
Karena kita sekarang memiliki peran (Role), kita akan mengatur agar siapa pun yang sudah login bisa membaca data, tapi hanya pemilik akun (dan mungkin nanti guru) yang bisa mengubahnya. Ubah kodenya menjadi:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Aturan dasar: Wajib login untuk baca/tulis data apapun
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
- Klik Publish.
2. Sinkronisasi Data Auth ke Firestore
Saat siswa Login atau Register (termasuk menggunakan Google), email mereka tersimpan di Firebase Auth. Namun, kita tidak bisa menambahkan data "Tempat PKL" atau "Role" di Auth. Oleh karena itu, kita harus membuat duplikat data profilnya ke dalam Firestore (Collection users).
Buka file lib/providers/auth_provider.dart dan tambahkan fungsi pengecekan profil di bawah ini:
import 'package:cloud_firestore/cloud_firestore.dart';
// ... import yang sudah ada
class AuthProvider with ChangeNotifier {
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
User? _user;
String? _userRole; // Menyimpan status role: 'student' atau 'teacher'
User? get user => _user;
String? get userRole => _userRole;
// ... (KODE CONSTRUCTOR authStateChanges TETAP ADA)
// 1. FUNGSI BARU: Menyimpan & Mengecek Data User di Firestore
Future<void> _syncUserToFirestore(User currentUser) async {
final docRef = _firestore.collection('users').doc(currentUser.uid);
final docSnap = await docRef.get();
// Jika dokumen sudah ada, cukup ambil role-nya
if (docSnap.exists) {
_userRole = docSnap.data()?['role'] ?? 'student';
notifyListeners();
return;
}
// Jika belum ada (baru pertama kali register/Google Sign-In), buat dokumen baru
await docRef.set({
'email': currentUser.email,
'name': currentUser.displayName ?? 'Siswa Tanpa Nama',
'role': 'student', // Default otomatis jadi siswa
'company_name': '', // Tempat PKL (Nanti diisi oleh Guru)
'company_address': '',
'job_position': '',
'created_at': Timestamp.now(),
});
_userRole = 'student';
notifyListeners();
}
// 2. UPDATE FUNGSI LOGIN (Panggil fungsi sync di dalamnya)
Future<void> login(String email, String password) async {
try {
final credential = await _auth.signInWithEmailAndPassword(
email: email, password: password
);
if (credential.user == null) return;
// Sinkronkan data
await _syncUserToFirestore(credential.user!);
} catch (e) {
throw Exception('Gagal login: Periksa email dan password.');
}
}
// (Lakukan pemanggilan _syncUserToFirestore yang sama pada fungsi Login Google nantinya)
}
3. Navigasi Cerdas Berdasarkan Role (GoRouter)
Sekarang aplikasi kita sudah tahu apakah yang sedang login itu siswa atau guru. Kita harus memperbarui sistem "Penjaga Pintu" (Route Guard) di GoRouter.
Buka file lib/core/routes/app_router.dart dan ubah fungsi redirect:
// PASTIKAN ANDA MENGIRIMKAN AuthProvider KE DALAM GOROUTER
// Contoh: final GoRouter appRouter = GoRouter( refreshListenable: authProviderInstance, ...
redirect: (context, state) {
final authProvider = context.read<AuthProvider>();
final bool isLoggedIn = authProvider.user != null;
final String? role = authProvider.userRole;
final bool isGoingToLogin = state.uri.toString() == '/login';
// 1. Jika belum login, paksa ke halaman login
if (!isLoggedIn && !isGoingToLogin) return '/login';
// 2. Jika sudah login, tentukan arah dashboard berdasarkan role
if (isLoggedIn && isGoingToLogin) {
if (role == 'teacher') return '/dashboard-guru';
return '/dashboard'; // Default ke siswa
}
return null;
},
4. Logika CRUD Guru (Teacher Provider)
Guru membutuhkan alat untuk menarik daftar semua siswa dan mengubah data PKL mereka.
Buat file baru bernama lib/providers/teacher_provider.dart:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class TeacherProvider with ChangeNotifier {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
// READ: Mengambil daftar semua user yang memiliki role 'student'
Stream<QuerySnapshot> getStudentsList() {
return _firestore
.collection('users')
.where('role', isEqualTo: 'student')
.snapshots();
}
// UPDATE: Guru melengkapi data PKL siswa
Future<void> updateStudentPklData(
String uid,
String companyName,
String companyAddress,
String jobPosition
) async {
try {
await _firestore.collection('users').doc(uid).update({
'company_name': companyName,
'company_address': companyAddress,
'job_position': jobPosition,
});
} catch (e) {
throw Exception('Gagal memperbarui data siswa: $e');
}
}
}
5. UI Dashboard Guru (Menampilkan List Siswa)
(Guru SMK: Silakan instruksikan siswa untuk menyesuaikan kode ini dengan desain template UI (Glass Card/dsb) yang sudah Anda siapkan sebelumnya).
Buat/Buka file lib/screens/teacher_dashboard_screen.dart:
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:provider/provider.dart';
// import provider kalian...
class TeacherDashboardScreen extends StatelessWidget {
const TeacherDashboardScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Dashboard Guru')),
body: StreamBuilder<QuerySnapshot>(
stream: context.read<TeacherProvider>().getStudentsList(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return const Center(child: Text('Belum ada siswa yang mendaftar.'));
}
return ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
var student = snapshot.data!.docs[index].data() as Map<String, dynamic>;
String uid = snapshot.data!.docs[index].id;
return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(student['name'] ?? 'Tanpa Nama'),
subtitle: Text(student['email'] ?? ''),
trailing: IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () {
// Navigasi ke halaman form edit data sambil membawa UID dan data lama
context.push('/edit-siswa', extra: {
'uid': uid,
'data': student,
});
},
),
),
);
},
);
},
),
);
}
}
6. UI Form Edit Data Siswa (Guru Melengkapi PKL)
Buat file baru lib/screens/edit_student_screen.dart. Ini adalah halaman tempat guru bisa memasukkan alamat dan tempat PKL siswa.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// import provider kalian...
class EditStudentScreen extends StatefulWidget {
final String uid;
final Map<String, dynamic> studentData;
const EditStudentScreen({Key? key, required this.uid, required this.studentData}) : super(key: key);
@override
State<EditStudentScreen> createState() => _EditStudentScreenState();
}
class _EditStudentScreenState extends State<EditStudentScreen> {
late TextEditingController _companyController;
late TextEditingController _addressController;
late TextEditingController _positionController;
bool _isLoading = false;
@override
void initState() {
super.initState();
// Isi otomatis form dengan data yang sudah ada sebelumnya
_companyController = TextEditingController(text: widget.studentData['company_name']);
_addressController = TextEditingController(text: widget.studentData['company_address']);
_positionController = TextEditingController(text: widget.studentData['job_position']);
}
void _saveData() async {
// Validasi singkat (If Short Statement)
if (_companyController.text.isEmpty) return;
setState(() => _isLoading = true);
try {
await context.read<TeacherProvider>().updateStudentPklData(
widget.uid,
_companyController.text,
_addressController.text,
_positionController.text,
);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Data berhasil disimpan!')));
context.pop(); // Kembali ke dashboard guru
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Edit Tempat PKL')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _companyController,
decoration: const InputDecoration(labelText: 'Nama Perusahaan (Tempat PKL)'),
),
const SizedBox(height: 16),
TextField(
controller: _addressController,
decoration: const InputDecoration(labelText: 'Alamat Perusahaan'),
),
const SizedBox(height: 16),
TextField(
controller: _positionController,
decoration: const InputDecoration(labelText: 'Posisi / Role PKL'),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _saveData,
child: _isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text('Simpan Data'),
),
)
],
),
),
);
}
}
7. Penutup Pertemuan 4
Pada modul ini, kalian sudah menerapkan logika tingkat lanjut (Advanced Logic) di Flutter. Kalian bisa melihat betapa fleksibelnya NoSQL: meskipun siswa login melalui Google dan data perusahaannya masih kosong, struktur datanya tidak akan pecah. Guru dapat menambahkannya belakangan tanpa perlu mengubah skema tabel seperti pada SQL!