Flutter Web Admin Geliştirme Rehberi
Yapay Zeka (Codex/Gemini) Asistanları için Hazır Promptlar ve Kod Şablonları
ADIM 1 Proje Kurulumu ve Git
Terminal Komutları
flutter create my_admin_panel
cd my_admin_panel
# Github.com'da New Repository oluştur
git init
# Windows/Mac gereksiz dosyaları engellemek için
curl -o .gitignore https://raw.githubusercontent.com/github/gitignore/main/Dart.gitignore
git add .
git commit -m "Initial Setup"
ADIM 2 Paket Yönetimi
Önce, yapay zekaya `pubspec.yaml` dosyasını temizletelim:
Projedeki `pubspec.yaml` dosyasını aç. Varsayılan gelen gereksiz yorum satırlarını ve `cupertino_icons` paketini kaldır.
Şimdi de aşağıdaki komutları terminalde çalıştırarak gerekli paketleri projemize ekleyelim.
1. Temel & Firebase Paketleri
flutter pub add firebase_core firebase_auth cloud_firestore firebase_storage cloud_functions provider go_router shared_preferences
2. UI Paketleri
flutter pub add google_fonts flutter_svg data_table_2
3. Yardımcı Paketler
flutter pub add file_picker multi_select_flutter http excel pdf intl logger freezed
flutter pub add dev:json_serializable
ADIM 3 Firebase Kurulumu
3.1 Firebase Console'da Create New Firebase Project oluşturun.
3.2 Project Settings/Add Web App.
3.3 Build/Authentication/Email-Password Enable + Add User
3.4 Build/Firestore Database/Create database (eur3)(Start in test mode)
3.5 Build/Functions/Add Billing Account + Storage
CLI Kurulumu
firebase login:list
dart pub global activate flutterfire_cli
flutterfire configure
Firebase Log out/In
firebase logout
firebase login
`lib/main.dart` dosyasını tamamen temizle ve yeniden yaz. 1. `firebase_options.dart` dosyasını import et. 2. `main` fonksiyonu içinde `WidgetsFlutterBinding.ensureInitialized()` çağır. 3. `Firebase.initializeApp` işlemini yap. 4. `kDebugMode` kontrolü ile `_ensureDevUserSignedIn` adında bir fonksiyon çağır. Bu fonksiyon, eğer emülatör/debug modundaysak sabit bir email/şifre (örn: admin@test.com) ile otomatik Firebase login yapsın (Auth yoksa kullanıcıyı oluştursun). 5. MaterialApp'e debugShowCheckedModeBanner: false, özelliği ekle. 6. Son olarak `runApp(const MyApp())` ile uygulamayı başlat.
Chrome Tarayıcıda Test Et
flutter run -d chrome
ADIM 4 Klasör Yapısı & Router
Önerilen Klasör Yapısı
lib/ ├── config/ │ ├── app_colors.dart │ ├── app_constants.dart │ └── app_theme.dart ├── core/ │ ├── models/ │ ├── services/ │ ├── utils/ │ └── widgets/ ├── features/ │ ├── auth/ │ ├── dashboard/ │ ├── users/ │ └── settings/ │ └── categories/ (Örnek: Yeni bir özellik) ├── layout/ │ ├── admin_layout.dart │ ├── sidebar.dart │ └── header.dart ├── router/ │ └── app_router.dart └── app.dart
Soru: "Kategoriler" veya "Ürünler" gibi yeni veri tabloları nereye eklenmeli?
Cevap: Her yeni özellik (Kategoriler, Ürünler, Siparişler vb.) `lib/features/` altında kendi klasörünü almalıdır. Örneğin, kategoriler için `lib/features/categories/` oluşturup ilgili tüm dosyaları (sayfa, model, veri kaynağı) buraya yerleştirebilirsiniz. Bu, projenizi modüler ve yönetilebilir tutar.
lib klasörü altında config, core, features, layout, router, providers adında 6 ana klasör oluştur.
1. config klasörü altında app_colors.dart, app_constants.dart, app_theme.dart dosyalarını oluştur. 2. core klasörü altında models, services, utils, widgets adında 4 alt klasör oluştur. 3. features klasörü altında auth, dashboard, users, settings adında 4 alt klasör oluştur. 4. layout klasörü altında admin_layout.dart, sidebar.dart, header.dart dosyalarını oluştur. 5. router klasörü altında app_router.dart dosyasını oluştur. 6. providers klasörü altında auth_provider.dart dosyasını oluştur.
2. lib-config-app_theme.dart dosyasını oluştur ve temel tema kodlarını Google Fonts ile yapılandır (Varsayılan Yazı Tipi: Inter). 3. lib-app.dart dosyasını oluştur. Router'ı burada başlat ve MaterialApp'e tema ve router'ı ekle.
router/app_router.dart içine kopyalayın
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// Importlar (Yolları kendi proje yapına göre düzenle)
import '../features/dashboard/dashboard_page.dart';
import '../features/auth/screens/login_screen.dart'; // Login sayfanın yolu
import '../layout/admin_layout.dart';
import '../provider/auth_provider.dart'; // AuthProvider yolu
class AppRouter {
// Router artık bir değişken değil, Provider alan bir metot
static GoRouter createRouter(AuthProvider authProvider) {
return GoRouter(
initialLocation: '/dashboard',
debugLogDiagnostics: true, // Debug konsolunda yönlendirmeleri görmek için
// 1. ÖNEMLİ: AuthProvider'ı dinle (login/logout olduğunda router tetiklenir)
refreshListenable: authProvider,
// 2. ÖNEMLİ: Yönlendirme Mantığı (Guard)
redirect: (BuildContext context, GoRouterState state) {
final bool isLoggedIn = authProvider.isAuthenticated;
final bool isLoggingIn = state.uri.toString() == '/login';
// Kullanıcı giriş yapmamışsa ve login sayfasında değilse -> Login'e at
if (!isLoggedIn && !isLoggingIn) {
return '/login';
}
// Kullanıcı giriş yapmışsa ve login sayfasındaysa -> Dashboard'a at
if (isLoggedIn && isLoggingIn) {
return '/dashboard';
}
// Diğer durumlarda olduğu yerde kalsın
return null;
},
routes: <RouteBase>[
// ------------------------------------------------------------------
// A. PUBLIC ROUTE (Layout YOK - Tam Ekran)
// ------------------------------------------------------------------
GoRoute(
path: '/login',
builder: (BuildContext context, GoRouterState state) {
return const LoginScreen(); // Login sayfan
},
),
// ------------------------------------------------------------------
// B. PROTECTED ROUTES (AdminLayout İÇİNDE - Sidebar Var)
// ------------------------------------------------------------------
ShellRoute(
builder: (BuildContext context, GoRouterState state, Widget child) {
return AdminLayout(child: child); // Senin mevcut layout yapın
},
routes: <RouteBase>[
GoRoute(
path: '/dashboard',
builder: (BuildContext context, GoRouterState state) {
return DashboardPage(); // Senin mevcut dashboard sayfan
},
),
GoRoute(
path: '/users',
builder: (BuildContext context, GoRouterState state) {
return const Center(child: Text("Users"));
},
),
GoRoute(
path: '/settings',
builder: (BuildContext context, GoRouterState state) {
return const Center(child: Text("Settings"));
},
),
],
),
],
);
}
}
providers/auth_provider.dart içine kopyalayın
// lib/providers/auth_provider.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class AuthProvider extends ChangeNotifier {
// Başlangıç durumu: Firebase'de kullanıcı var mı?
bool _isAuthenticated = FirebaseAuth.instance.currentUser != null;
bool get isAuthenticated => _isAuthenticated;
AuthProvider() {
_init();
}
void _init() {
// Firebase Auth durumunu dinle
// (Böylece main.dart'taki otomatik giriş veya logout anında algılanır)
FirebaseAuth.instance.authStateChanges().listen((User? user) {
final bool isLoggedIn = user != null;
// Sadece durum değiştiyse güncelle
if (_isAuthenticated != isLoggedIn) {
_isAuthenticated = isLoggedIn;
notifyListeners(); // Router'ı tetikler!
}
});
}
// Manuel Login (Login Ekranı için)
Future<void> login(String email, String password) async {
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email,
password: password
);
// authStateChanges() zaten dinlediği için burada notifyListeners() çağırmaya gerek yok,
// ama manuel akış kontrolü için beklenebilir.
}
// Şifre sıfırlama e-postası gönder
Future<void> sendPasswordResetEmail(String email) async {
await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
}
// Logout
Future<void> logout() async {
await FirebaseAuth.instance.signOut();
// authStateChanges() dinlediği için otomatik false olur ve login'e atar.
}
}
main.dart runApp metodunu güncelle
runApp(
MultiProvider(
providers: [
// AuthProvider'ı burada yaratıyoruz.
// Bu provider açıldığında Firebase'in mevcut kullanıcısını kontrol edecek.
ChangeNotifierProvider(create: (_) => AuthProvider()),
],
child: const CrmApp(),
),
);
lib/features/auth/login_page.dart içine kopyalayın
import 'package:crm_maa/providers/auth_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // Autofill için gerekli
import 'package:provider/provider.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
bool _isPasswordVisible = false; // Şifre görünürlük durumu
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
if (_formKey.currentState!.validate()) {
// Klavye/Autofill servisine formun bittiğini bildirir (Şifreyi kaydet önerisi çıkarır)
TextInput.finishAutofillContext();
setState(() => _isLoading = true);
try {
await context.read<AuthProvider>().login(
_emailController.text.trim(),
_passwordController.text.trim(),
);
// Router otomatik yönlendirecek...
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hata: ${e.toString()}'), backgroundColor: Colors.red),
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
}
// Şifremi Unuttum Dialogu
void _showForgotPasswordDialog() {
final resetEmailController = TextEditingController(text: _emailController.text);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Şifre Sıfırlama'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('E-posta adresinizi girin, size sıfırlama bağlantısı gönderelim.'),
const SizedBox(height: 16),
TextField(
controller: resetEmailController,
decoration: const InputDecoration(
labelText: 'E-posta',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('İptal'),
),
FilledButton(
onPressed: () async {
final email = resetEmailController.text.trim();
if (email.isEmpty) return;
Navigator.pop(context); // Dialogu kapat
try {
await context.read<AuthProvider>().sendPasswordResetEmail(email);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Sıfırlama bağlantısı gönderildi.')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hata: $e'), backgroundColor: Colors.red),
);
}
}
},
child: const Text('Gönder'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
margin: const EdgeInsets.all(24),
child: Padding(
padding: const EdgeInsets.all(32.0),
child: AutofillGroup( // 3. MADDE: Tarayıcı Desteği İçin Kritik Kapsayıcı
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(Icons.lock_person, size: 64, color: Colors.blue),
const SizedBox(height: 16),
Text(
'Yönetici Girişi',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 32),
// --- E-POSTA ---
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'E-posta',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next, // Enter'a basınca şifreye geç
autofillHints: const [AutofillHints.email], // Tarayıcıya ipucu
validator: (value) => (value != null && value.contains('@')) ? null : 'Geçerli e-posta giriniz',
),
const SizedBox(height: 16),
// --- ŞİFRE ---
TextFormField(
controller: _passwordController,
obscureText: !_isPasswordVisible, // 2. MADDE: Gizle/Göster
decoration: InputDecoration(
labelText: 'Şifre',
prefixIcon: const Icon(Icons.lock_outline),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
),
),
textInputAction: TextInputAction.done, // Enter'a basınca formu gönder
onFieldSubmitted: (_) => _handleLogin(),
autofillHints: const [AutofillHints.password], // Tarayıcıya ipucu
validator: (value) => (value == null || value.isEmpty) ? 'Şifre giriniz' : null,
),
// --- ŞİFREMİ UNUTTUM ---
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _showForgotPasswordDialog, // 1. MADDE
child: const Text('Şifremi Unuttum?'),
),
),
const SizedBox(height: 8),
// --- GİRİŞ BUTONU ---
SizedBox(
height: 48,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: _isLoading
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Text('Giriş Yap', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
),
],
),
),
),
),
),
),
),
);
}
}
ADIM 5 Admin Layout
Layout için gerekli sabitleri ilgili dosyalara ekleyerek başlayalım.
`lib/config/app_colors.dart`
import 'package:flutter/material.dart'; const Color primaryColor = Color(0xFF1F2937); const Color accentColor = Color(0xFF6366F1); const Color sidebarColor = Color(0xFF111827); const Color contentBgColor = Color(0xFFF3F4F6);
`lib/config/app_constants.dart`
// Yan Menü Genişlikleri const double sidebarWidthExpanded = 250.0; const double sidebarWidthCollapsed = 80.0;
`lib/layout/admin_layout.dart`
import 'package:flutter/material.dart';
import '../config/app_colors.dart';
import 'header.dart';
import 'sidebar.dart';
class AdminLayout extends StatefulWidget {
final Widget child;
const AdminLayout({super.key, required this.child});
@override
State<AdminLayout> createState() => _AdminLayoutState();
}
class _AdminLayoutState extends State<AdminLayout> {
bool isSidebarExpanded = true;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: contentBgColor,
body: Row(
children: [
AdminSidebar(
isExpanded: isSidebarExpanded,
onExpandToggle: () => setState(() => isSidebarExpanded = !isSidebarExpanded),
),
Expanded(
child: Column(
children: [const AdminHeader(), Expanded(child: widget.child)],
),
),
],
),
);
}
}
`lib/layout/sidebar.dart`
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../config/app_colors.dart';
import '../config/app_constants.dart';
class MenuItem {
final String title;
final IconData icon;
final String route;
const MenuItem(this.title, this.icon, this.route);
}
const List<MenuItem> menuItems = [
MenuItem('Ana Sayfa', Icons.dashboard_outlined, '/dashboard'),
MenuItem('Kullanıcılar', Icons.people_alt_outlined, '/users'),
MenuItem('Ürünler', Icons.inventory_2_outlined, '/products'),
MenuItem('Ayarlar', Icons.settings_outlined, '/settings'),
MenuItem('Raporlar', Icons.analytics_outlined, '/reports'),
];
class AdminSidebar extends StatelessWidget {
final bool isExpanded;
final VoidCallback onExpandToggle;
const AdminSidebar({
super.key,
required this.isExpanded,
required this.onExpandToggle,
});
@override
Widget build(BuildContext context) {
return Material(
color: sidebarColor,
elevation: 5,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: isExpanded ? sidebarWidthExpanded : sidebarWidthCollapsed,
child: Column(
children: [
Container(
height: 60,
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: isExpanded ? 20.0 : 0.0),
child: isExpanded
? const Text('Yönetim Paneli', style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold))
: const Center(child: Icon(Icons.shield_moon_outlined, color: accentColor, size: 30)),
),
const Divider(color: primaryColor, height: 1),
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: menuItems.map((item) => SidebarItem(item: item, isExpanded: isExpanded)).toList(),
),
),
const Divider(color: primaryColor, height: 1),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: IconButton(
icon: Icon(isExpanded ? Icons.arrow_back_ios_new : Icons.arrow_forward_ios, color: Colors.white70, size: 20),
onPressed: onExpandToggle,
tooltip: isExpanded ? 'Daralt' : 'Genişlet',
),
),
],
),
),
);
}
}
class SidebarItem extends StatelessWidget {
final MenuItem item;
final bool isExpanded;
const SidebarItem({super.key, required this.item, required this.isExpanded});
@override
Widget build(BuildContext context) {
return Tooltip(
message: isExpanded ? '' : item.title,
child: InkWell(
onTap: () => context.go(item.route),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 20.0),
child: Row(
mainAxisAlignment: isExpanded ? MainAxisAlignment.start : MainAxisAlignment.center,
children: [
Icon(item.icon, color: Colors.white70, size: 24),
if (isExpanded)
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 15.0),
child: Text(item.title, style: const TextStyle(color: Colors.white70, fontSize: 16), overflow: TextOverflow.ellipsis),
),
),
],
),
),
),
);
}
}
`lib/layout/header.dart`
import 'package:flutter/material.dart';
import '../config/app_colors.dart';
class AdminHeader extends StatelessWidget {
const AdminHeader({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: 60,
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
children: [
// Sayfa Başlığı (Dinamik hale getirilebilir)
const Text(
'Ana Sayfa',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: primaryColor,
),
),
const Spacer(),
Row(
children: [
const Text(
'Merhaba, Admin',
style: TextStyle(color: primaryColor, fontSize: 16),
),
const SizedBox(width: 10),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: accentColor, width: 2),
),
child: const CircleAvatar(
radius: 18,
backgroundColor: accentColor,
backgroundImage: NetworkImage('https://placehold.co/100x100/6366F1/FFFFFF?text=A'),
),
),
IconButton(
icon: const Icon(Icons.keyboard_arrow_down, color: primaryColor),
onPressed: () { /* Profil menüsü açma aksiyonu */ },
),
],
),
],
),
);
}
}
ADIM 6 Dashboard Page
Uygulamanın giriş sayfası olacak olan `DashboardPage` için örnek bir içerik yapısı.
`lib/features/dashboard/dashboard_page.dart`
import 'package:flutter/material.dart';
import '../../config/app_colors.dart';
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return ResponsiveContentGrid(screenSize: screenSize);
}
}
class ResponsiveContentGrid extends StatelessWidget {
final Size screenSize;
const ResponsiveContentGrid({super.key, required this.screenSize});
int _getColumnCount(double width) {
if (width > 1200) return 3;
if (width > 700) return 2;
return 1;
}
@override
Widget build(BuildContext context) {
final columnCount = _getColumnCount(screenSize.width);
return GridView.builder(
padding: const EdgeInsets.all(20.0),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columnCount,
crossAxisSpacing: 20.0,
mainAxisSpacing: 20.0,
childAspectRatio: 1.5,
),
itemCount: 3,
itemBuilder: (context, index) => ContentCard(index: index + 1),
);
}
}
class ContentCard extends StatelessWidget {
final int index;
const ContentCard({super.key, required this.index});
@override
Widget build(BuildContext context) {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
index == 1 ? Icons.bar_chart : (index == 2 ? Icons.storage : Icons.shopping_cart),
color: accentColor,
size: 30,
),
const SizedBox(width: 10),
Text('İçerik Alanı $index', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: primaryColor)),
],
),
const Divider(height: 25),
const Text('Bu kart, ana verileri veya widgetları barındırır. Ekran genişliğine göre düzenlenir.', style: TextStyle(color: Color(0xFF4B5563))),
const Spacer(),
Align(
alignment: Alignment.bottomRight,
child: Text('Detaylar...', style: TextStyle(color: accentColor.withOpacity(0.8), fontSize: 12)),
),
],
),
),
);
}
}
ADIM 7 Firestore Service & Model
Veritabanı Yapısını Hazırlayın
Projenizin taslağından yola çıkarak veritabanı şemasını ve örnek verileri yapay zeka ile oluşturun. Örnek verilerle birlikte assets/project_data.json dosyası oluşturun, pubsec.yaml'a asset olarak ekleyin. Bu dosya, veri modellerinizi tanımlamak ve koleksiyonlarınızı yapılandırmak için kullanılacaktır.
JSON'dan Data Models'leri Oluşturma
You are an expert Flutter Developer.
Create data models in lib /core/models/
Context: This is a ... Application for Admin Panel.
Architecture: ....
Requirements:
- Properly handle nullable fields (make fields nullable if they might not be present during initial onboarding).
- Add a 'fromFirestore' factory or equivalent logic if necessary, but standard fromJson is priority.
- Create Dart data model classes for each collection in the provided JSON structure.
- Use lib/project_data.json as the source of truth for the data structure.
Tekrar kullanılabilir, güvenli veri katmanı.
`lib/core/services/firestore_service.dart` dosyasını oluştur.
Singleton pattern (instance) kullanarak Generic bir yapı kur.
Şu metodlar olsun:
- getCollection
Referans: FirestoreService Kodu +
class FirestoreService {
FirestoreService._();
static final instance = FirestoreService._();
final FirebaseFirestore _db = FirebaseFirestore.instance;
Future<List<T>> getCollection<T>({
required String path,
required T Function(Map<String, dynamic> data, String id) fromMap,
Query Function(Query query)? queryBuilder,
}) async {
Query query = _db.collection(path);
if (queryBuilder != null) query = queryBuilder(query);
final snapshot = await query.get();
return snapshot.docs.map((d) => fromMap(d.data() as Map<String, dynamic>, d.id)).toList();
}
}
ADIM 8 Users Page & DataGrid
You are an expert Flutter Developer. I need you to finalize the `users_page.dart` for my Web Admin Panel.
Current Status:
I have a responsive layout using `DataTable2` inside an `Expanded` > `Card` widget. The UI is good, but the backend logic for Creating and Deleting users is incorrect (it currently uses direct Firestore writes).
Task:
Refactor `lib/features/users/users_page.dart` to integrate **Firebase Cloud Functions** for sensitive operations.
Requirements:
1. **Layout & UI (KEEP THIS):**
- Maintain the existing structure: `Scaffold` > `Padding` > `Column` > `Header` + `Expanded(Card(DataTable2))`.
- This layout is bug-free and responsive; do not break it.
- Keep the Search Bar, Export Buttons, and Role Badges as they are.
2. **"Add User" Feature (UPDATE):**
- **UI Change:** The `_showAddUserDialog` must now include a **Password TextField** (obscured) because we are creating a full Auth account.
- **Logic Change:** Instead of `firestore.collection.add`, you MUST call the Cloud Function:
```dart
final result = await FirebaseFunctions.instance.httpsCallable('createUserAccount').call({
'email': email,
'password': password,
'displayName': name,
'role': role,
});
```
- Handle loading state within the dialog (show a spinner while the function runs).
- Show success/error SnackBar based on the function result.
3. **"Delete User" Feature (UPDATE):**
- **Logic Change:** Instead of `firestore.doc.delete`, you MUST call the Cloud Function:
```dart
await FirebaseFunctions.instance.httpsCallable('deleteUserAccount').call({
'uid': user.uid,
});
```
- Show a confirmation dialog before calling the function.
4. **"Edit User" Feature (KEEP):**
- Updating `displayName` or `role` can remain as a direct Firestore update (`firestore.collection('users').doc(uid).update(...)`) for simplicity. No Cloud Function needed here yet.
5. **Data Fetching:**
- Keep using `StreamBuilder` with `DataTable2` to listen to the 'users' collection in real-time.
Dependencies:
- `cloud_functions`
- `data_table_2`
- `cloud_firestore`
Please provide the full, updated `users_page.dart` code.
ADIM 9 Cloud Functions (Backend)
Kurulum (Billing Account Gerekiyor)
firebase login
firebase logout
firebase projects:list
firebase use project-name
firebase init (Functions Seç) (TypeScript Seç) (Eslint No)
cd functions
npm install firebase-admin firebase-functions
cd ..
firebase deploy --only functions
firebase functions:secrets:set AI_API_KEY (Create Key in ChatGPT or Gemini)
functions/src/index.ts içine kopyalayın
import {onCall, HttpsError} from "firebase-functions/v2/https";
import * as admin from "firebase-admin";
admin.initializeApp();
interface AddUserAccountData {
email: string;
password: string;
displayName: string;
role: string;
}
export const addUserAccount = onCall(async (request) => {
// Check if the request is authenticated
if (!request.auth) {
throw new HttpsError(
"unauthenticated",
"User must be authenticated to create accounts."
);
}
// Check if the caller has admin role from Firestore
const callerUid = request.auth.uid;
const userDoc = await admin.firestore().collection("users").doc(callerUid).get();
if (!userDoc.exists || userDoc.data()?.role !== "admin") {
throw new HttpsError(
"permission-denied",
"Only administrators can create user accounts."
);
}
// Validate input data
const {email, password, displayName, role} = request.data as AddUserAccountData;
if (!email || typeof email !== "string") {
throw new HttpsError(
"invalid-argument",
"Email is required and must be a string."
);
}
if (!password || typeof password !== "string" || password.length < 6) {
throw new HttpsError(
"invalid-argument",
"Password is required and must be at least 6 characters."
);
}
if (!displayName || typeof displayName !== "string") {
throw new HttpsError(
"invalid-argument",
"Display name is required and must be a string."
);
}
if (!role || typeof role !== "string") {
throw new HttpsError(
"invalid-argument",
"Role is required and must be a string."
);
}
// Validate role is one of the allowed values
const allowedRoles = ["admin", "client", "employee"];
if (!allowedRoles.includes(role)) {
throw new HttpsError(
"invalid-argument",
`Role must be one of: ${allowedRoles.join(", ")}`
);
}
try {
// Create the user account
const userRecord = await admin.auth().createUser({
email: email,
password: password,
displayName: displayName,
emailVerified: false,
});
// Set custom claims for the role
await admin.auth().setCustomUserClaims(userRecord.uid, {
role: role,
});
// Create user document in Firestore
await admin.firestore().collection("users").doc(userRecord.uid).set({
email: email,
displayName: displayName,
role: role,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
createdBy: callerUid,
active: true,
});
return {
success: true,
message: `User ${email} created successfully with role ${role}.`,
userId: userRecord.uid,
};
} catch (error: any) {
console.error("Error creating user:", error);
// Handle specific Firebase Auth errors
if (error.code === "auth/email-already-exists") {
throw new HttpsError(
"already-exists",
"An account with this email already exists."
);
}
if (error.code === "auth/invalid-email") {
throw new HttpsError(
"invalid-argument",
"The email address is invalid."
);
}
if (error.code === "auth/weak-password") {
throw new HttpsError(
"invalid-argument",
"The password is too weak."
);
}
// Generic error
throw new HttpsError(
"internal",
`Failed to create user: ${error.message}`
);
}
});
ADIM 10 Proje Notları ve Son Adımlar
Tüm geliştirme adımlarını tamamladıktan sonra projenizi yayına hazırlamak ve yönetmek için bazı ek notlar ve ipuçları.
1. Değişiklikleri Kaydetme ve Test
Tüm adımları tamamladıktan sonra projenizin son halini Git'e kaydedin ve tarayıcıda test edin.
git push flutter run -d chrome
2. Web için Build Alma
Eğer projeniz bir alt klasörde (örn: `alanadiniz.com/panel`) yayınlanacaksa, `base-href` parametresini kullanın.
flutter build web --release --base-href="/panel/"
3. Build Sonrası Ayarlar
- Varlıklar: Proje ana dizinindeki `assets/logo.png` gibi dosyaları, build sonrası `build/web/assets/` klasörüne manuel olarak kopyalayın.
- Favicon: `build/web/favicon.png` dosyasını kendi logonuzla değiştirin. appicon.co gibi sitelerden 1024x1024 bir görselle tüm boyutları oluşturabilirsiniz.
- SEO Engelleme: Yönetim panellerinin arama motorları tarafından indekslenmemesi önemlidir. `build/web/index.html` dosyasının `` etiketleri arasına aşağıdaki meta etiketini ekleyin:
<meta name="robots" content="noindex, nofollow">
4. Sunucuya Yükleme
`build/web` klasörünün içeriğini FileZilla gibi bir FTP istemcisiyle sunucunuzdaki ilgili klasöre (örn: `/public_html/panel/`) yükleyin.
Ek Öneriler (İleri Seviye)
- Özelleştirilmiş Yükleme Ekranı: Flutter'ın yüklenmesi birkaç saniye sürebilir. Bu sırada boş bir sayfa yerine markanıza uygun bir yükleme animasyonu göstermek için `build/web/index.html` dosyasını düzenleyebilirsiniz.
- Hata Takibi: Yayındaki olası hataları takip etmek için projenize Sentry veya Firebase Crashlytics gibi servisleri entegre edin.
- Otomatik Dağıtım (CI/CD): Her `git push` işleminden sonra build ve dağıtım adımlarını otomatikleştirmek için GitHub Actions veya GitLab CI/CD gibi araçlarla bir pipeline kurmayı düşünün.