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:

AI PROMPT

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
AI PROMPT

`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.

AI PROMPT

lib klasörü altında config, core, features, layout, router, providers adında 6 ana klasör oluştur.

AI PROMPT

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.

AI PROMPT

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

AI PROMPT

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ı.

AI PROMPT

`lib/core/services/firestore_service.dart` dosyasını oluştur. Singleton pattern (instance) kullanarak Generic bir yapı kur. Şu metodlar olsun: - getCollection (FromMap fonksiyonu almalı) - getDocument - setData (merge: true desteği ile) - deleteData

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

AI PROMPT

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.