Initialer Commit: Projekt Start

This commit is contained in:
Tim Leikauf
2026-01-03 15:24:36 +01:00
commit 3773f94303
168 changed files with 228080 additions and 0 deletions

View File

@@ -0,0 +1,372 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../providers/cart_provider.dart';
import '../services/coupon_service.dart';
import '../models/coupon.dart';
import 'product_detail_screen.dart';
import 'checkout_screen.dart';
class _CartScreenState extends State<CartScreen> {
final CouponService _couponService = CouponService();
final TextEditingController _couponController = TextEditingController();
Coupon? _appliedCoupon;
bool _isValidatingCoupon = false;
@override
void dispose() {
_couponController.dispose();
super.dispose();
}
Future<void> _applyCoupon() async {
if (_couponController.text.trim().isEmpty) return;
setState(() {
_isValidatingCoupon = true;
});
final coupon = await _couponService.validateCoupon(_couponController.text.trim());
setState(() {
_appliedCoupon = coupon.isValid ? coupon : null;
_isValidatingCoupon = false;
});
if (!coupon.isValid) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(coupon.errorMessage ?? 'Ungültiger Gutschein-Code'),
backgroundColor: Colors.red,
),
);
}
}
}
void _removeCoupon() {
setState(() {
_appliedCoupon = null;
_couponController.clear();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Warenkorb'),
),
body: Consumer<CartProvider>(
builder: (context, cart, child) {
if (cart.items.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.shopping_cart_outlined,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Dein Warenkorb ist leer',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'Füge Produkte hinzu, um zu beginnen',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[500],
),
),
],
),
);
}
return Column(
children: [
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: cart.items.length,
itemBuilder: (context, index) {
final item = cart.items[index];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailScreen(
product: item.product,
),
),
);
},
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
// Produktbild
if (item.product.imageUrl != null)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: item.product.imageUrl!,
width: 80,
height: 80,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
width: 80,
height: 80,
color: Colors.grey[300],
child: const Center(
child: CircularProgressIndicator(),
),
),
errorWidget: (context, url, error) =>
Container(
width: 80,
height: 80,
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported),
),
),
)
else
Container(
width: 80,
height: 80,
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported),
),
const SizedBox(width: 12),
// Produktinfo
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.product.name,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'${item.product.price}',
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(
color: const Color(0xFF8B6F47),
fontWeight: FontWeight.bold,
),
),
],
),
),
// Mengensteuerung
Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () => cart.removeItem(item.product),
),
Text(
'${item.quantity}',
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () => cart.addItem(item.product),
),
],
),
Text(
'${item.totalPrice.toStringAsFixed(2)}',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
// Entfernen
IconButton(
icon: const Icon(Icons.delete_outline),
color: Colors.red,
onPressed: () {
cart.removeItemCompletely(item.product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${item.product.name} entfernt'),
),
);
},
),
],
),
),
),
);
},
),
),
// Gesamtsumme und Checkout
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Column(
children: [
// Gutschein-Eingabe
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _couponController,
decoration: InputDecoration(
hintText: 'Gutschein-Code',
border: InputBorder.none,
isDense: true,
),
enabled: !_isValidatingCoupon && _appliedCoupon == null,
),
),
if (_appliedCoupon != null)
IconButton(
icon: const Icon(Icons.check_circle, color: Colors.green),
onPressed: _removeCoupon,
)
else
IconButton(
icon: _isValidatingCoupon
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.add),
onPressed: _isValidatingCoupon ? null : _applyCoupon,
),
],
),
),
if (_appliedCoupon != null) ...[
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Rabatt (${_appliedCoupon!.code}):',
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
'-${_couponService.calculateDiscount(_appliedCoupon!, cart.totalPrice).toStringAsFixed(2)}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
],
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Gesamtsumme:',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'${(cart.totalPrice - (_appliedCoupon != null ? _couponService.calculateDiscount(_appliedCoupon!, cart.totalPrice) : 0)).toStringAsFixed(2)}',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: const Color(0xFF8B6F47),
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CheckoutScreen(),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF8B6F47),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Zur Kasse',
style: TextStyle(fontSize: 16),
),
),
),
],
),
),
],
);
},
),
);
}
}
class CartScreen extends StatefulWidget {
const CartScreen({super.key});
@override
State<CartScreen> createState() => _CartScreenState();
}

View File

@@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../models/category.dart';
import '../services/woocommerce_service.dart';
import '../services/analytics_service.dart';
import '../widgets/loading_widget.dart';
import '../widgets/retry_widget.dart';
import '../utils/error_handler.dart';
import 'products_screen.dart';
class CategoriesScreen extends StatefulWidget {
const CategoriesScreen({super.key});
@override
State<CategoriesScreen> createState() => _CategoriesScreenState();
}
class _CategoriesScreenState extends State<CategoriesScreen> {
final WooCommerceService _wooCommerceService = WooCommerceService();
List<Category> _categories = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadCategories();
}
Future<void> _loadCategories() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final categories = await _wooCommerceService.getCategories();
setState(() {
_categories = categories;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
ErrorHandler.showError(context, e);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Kategorien'),
),
body: _isLoading
? const LoadingWidget()
: _error != null
? RetryWidget(
message: 'Fehler beim Laden der Kategorien',
onRetry: _loadCategories,
)
: _categories.isEmpty
? Center(
child: Text(
'Keine Kategorien verfügbar',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.grey[600],
),
),
)
: RefreshIndicator(
onRefresh: _loadCategories,
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.8,
),
itemCount: _categories.length,
itemBuilder: (context, index) {
final category = _categories[index];
return _buildCategoryCard(category);
},
),
),
);
}
Widget _buildCategoryCard(Category category) {
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
AnalyticsService.trackCategoryView(category.name);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductsScreen(
categoryId: category.id.toString(),
categoryName: category.name,
),
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: category.imageUrl != null
? CachedNetworkImage(
imageUrl: category.imageUrl!,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: Colors.grey[300],
child: const Center(
child: CircularProgressIndicator(),
),
),
errorWidget: (context, url, error) => Container(
color: Colors.grey[300],
child: const Icon(Icons.category, size: 48),
),
)
: Container(
color: Colors.grey[300],
child: const Icon(Icons.category, size: 48),
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
category.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (category.count > 0) ...[
const SizedBox(height: 4),
Text(
'${category.count} Produkte',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
],
],
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,241 @@
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.dart';
import '../services/woocommerce_service.dart';
class CheckoutScreen extends StatefulWidget {
const CheckoutScreen({super.key});
@override
State<CheckoutScreen> createState() => _CheckoutScreenState();
}
class _CheckoutScreenState extends State<CheckoutScreen> {
late final WebViewController _controller;
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_initializeWebView();
}
void _initializeWebView() {
final cart = Provider.of<CartProvider>(context, listen: false);
// Erstelle WooCommerce Checkout URL mit Warenkorb-Daten
final checkoutUrl = _buildCheckoutUrl(cart);
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'FlutterChannel',
onMessageReceived: (JavaScriptMessage message) {
// Handle messages from JavaScript if needed
if (message.message == 'order_success') {
_handleOrderSuccess();
}
},
)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (String url) {
setState(() {
_isLoading = true;
_error = null;
});
},
onPageFinished: (String url) async {
setState(() {
_isLoading = false;
});
// Prüfe ob Bestellung erfolgreich war
if (url.contains('order-received') || url.contains('order-received/')) {
_handleOrderSuccess();
}
// Füge Produkte zum WooCommerce-Warenkorb hinzu, wenn wir auf der Cart/Checkout-Seite sind
if (url.contains('/cart/') || url.contains('/checkout/')) {
await _addProductsToCart(cart);
}
},
onWebResourceError: (WebResourceError error) {
setState(() {
_error = 'Fehler beim Laden der Seite: ${error.description}';
_isLoading = false;
});
},
),
)
..loadRequest(Uri.parse(checkoutUrl));
}
/// Fügt Produkte mit JavaScript zum WooCommerce-Warenkorb hinzu
Future<void> _addProductsToCart(CartProvider cart) async {
if (cart.items.isEmpty) return;
// Erstelle JavaScript-Code, um Produkte zum Warenkorb hinzuzufügen
final jsCode = StringBuffer();
jsCode.writeln('(function() {');
// Für jedes Produkt im Warenkorb
for (var item in cart.items) {
jsCode.writeln('''
fetch('${WooCommerceService.baseUrl}/?wc-ajax=add_to_cart', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'product_id=${item.product.id}&quantity=${item.quantity}'
}).then(response => response.json()).then(data => {
if (data.error) {
console.error('Fehler beim Hinzufügen:', data.error);
}
});
''');
}
jsCode.writeln('})();');
try {
await _controller.runJavaScript(jsCode.toString());
// Warte kurz und leite dann zum Checkout weiter
await Future.delayed(const Duration(milliseconds: 500));
await _controller.runJavaScript('window.location.href = "${WooCommerceService.baseUrl}/checkout/";');
} catch (e) {
// Falls JavaScript-Fehler auftreten, verwende die einfache URL-Methode
print('JavaScript-Fehler: $e');
}
}
String _buildCheckoutUrl(CartProvider cart) {
final baseUrl = WooCommerceService.baseUrl;
if (cart.items.isEmpty) {
return '$baseUrl/checkout/';
}
// Starte mit der Cart-Seite, damit wir die Produkte hinzufügen können
// Die JavaScript-Funktion _addProductsToCart wird dann die Produkte hinzufügen
return '$baseUrl/cart/';
}
void _handleOrderSuccess() {
// Warenkorb leeren nach erfolgreicher Bestellung
final cart = Provider.of<CartProvider>(context, listen: false);
cart.clearCart();
// Zeige Erfolgsmeldung
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('Bestellung erfolgreich!'),
content: const Text(
'Vielen Dank für deine Bestellung. Du erhältst eine Bestätigungs-E-Mail.',
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Dialog schließen
Navigator.of(context).pop(); // Checkout schließen
Navigator.of(context).pop(); // Warenkorb schließen (falls von dort gekommen)
},
child: const Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Zur Kasse'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
// Frage ob wirklich abbrechen
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Checkout abbrechen?'),
content: const Text('Möchtest du wirklich abbrechen? Dein Warenkorb bleibt erhalten.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Weiter einkaufen'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Dialog
Navigator.of(context).pop(); // Checkout
},
child: const Text('Abbrechen'),
),
],
),
);
},
),
),
body: Stack(
children: [
if (_error != null)
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'Fehler',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_error!,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
_initializeWebView();
},
child: const Text('Erneut versuchen'),
),
],
),
)
else
WebViewWidget(controller: _controller),
if (_isLoading && _error == null)
Container(
color: Colors.white,
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Checkout wird geladen...'),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,427 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.dart';
import '../providers/user_provider.dart';
import 'products_screen.dart';
import 'product_detail_screen.dart';
import 'cart_screen.dart';
import 'login_screen.dart';
import 'orders_screen.dart';
import 'search_screen.dart';
import 'categories_screen.dart';
import '../widgets/product_card.dart';
import '../services/woocommerce_service.dart';
import '../services/analytics_service.dart';
import '../models/product.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final WooCommerceService _wooCommerceService = WooCommerceService();
List<Product> _featuredProducts = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadFeaturedProducts();
}
Future<void> _loadFeaturedProducts() async {
try {
setState(() {
_isLoading = true;
_error = null;
});
final products = await _wooCommerceService.getFeaturedProducts(limit: 6);
// Falls keine Featured Products vorhanden sind, lade normale Produkte
if (products.isEmpty) {
final allProducts = await _wooCommerceService.getProducts(perPage: 6);
setState(() {
_featuredProducts = allProducts;
_isLoading = false;
});
} else {
setState(() {
_featuredProducts = products;
_isLoading = false;
});
}
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('HyggeCraftery'),
actions: [
// Such-Button
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SearchScreen(),
),
);
},
),
// Kategorien-Button
IconButton(
icon: const Icon(Icons.category),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CategoriesScreen(),
),
);
},
),
// Account-Button
Consumer<UserProvider>(
builder: (context, userProvider, child) {
return IconButton(
icon: Icon(
userProvider.isLoggedIn ? Icons.account_circle : Icons.login,
),
onPressed: () {
if (userProvider.isLoggedIn) {
_showAccountMenu(context, userProvider);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LoginScreen(),
),
);
}
},
);
},
),
// Warenkorb-Button
Consumer<CartProvider>(
builder: (context, cart, child) {
return Stack(
children: [
IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CartScreen(),
),
);
},
),
if (cart.itemCount > 0)
Positioned(
right: 8,
top: 8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
'${cart.itemCount}',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
textAlign: TextAlign.center,
),
),
),
],
);
},
),
],
),
body: _buildHomeTab(),
);
}
Widget _buildHomeTab() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'Fehler beim Laden der Produkte',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
_error!,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadFeaturedProducts,
child: const Text('Erneut versuchen'),
),
],
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Willkommensbereich
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFF8B6F47).withOpacity(0.8),
const Color(0xFFD4A574).withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Willkommen bei HyggeCraftery',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Entdecke unsere gemütlichen Handwerksprodukte',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.white.withOpacity(0.9),
),
),
],
),
),
const SizedBox(height: 32),
// Quick Actions
Row(
children: [
Expanded(
child: _buildQuickActionCard(
context,
icon: Icons.search,
title: 'Suchen',
color: Colors.blue,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SearchScreen(),
),
);
},
),
),
const SizedBox(width: 12),
Expanded(
child: _buildQuickActionCard(
context,
icon: Icons.category,
title: 'Kategorien',
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CategoriesScreen(),
),
);
},
),
),
],
),
const SizedBox(height: 32),
// Featured Products
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Ausgewählte Produkte',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: const Color(0xFF8B6F47),
),
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProductsScreen(),
),
);
},
child: const Text('Alle anzeigen'),
),
],
),
const SizedBox(height: 16),
if (_featuredProducts.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.all(32.0),
child: Text('Keine Produkte verfügbar'),
),
)
else
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.75,
),
itemCount: _featuredProducts.length,
itemBuilder: (context, index) {
return ProductCard(
product: _featuredProducts[index],
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailScreen(
product: _featuredProducts[index],
),
),
);
},
);
},
),
],
),
);
}
void _showAccountMenu(BuildContext context, UserProvider userProvider) {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (userProvider.user != null) ...[
ListTile(
leading: const Icon(Icons.person),
title: Text(userProvider.user!.fullName),
subtitle: Text(userProvider.user!.email),
),
const Divider(),
],
ListTile(
leading: const Icon(Icons.shopping_bag),
title: const Text('Meine Bestellungen'),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const OrdersScreen(),
),
);
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.logout, color: Colors.red),
title: const Text('Abmelden', style: TextStyle(color: Colors.red)),
onTap: () async {
await userProvider.logout();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erfolgreich abgemeldet'),
backgroundColor: Colors.green,
),
);
},
),
],
),
),
);
}
Widget _buildQuickActionCard(
BuildContext context, {
required IconData icon,
required String title,
required Color color,
required VoidCallback onTap,
}) {
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(icon, size: 32, color: color),
const SizedBox(height: 8),
Text(
title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,253 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/user_provider.dart';
import 'register_screen.dart';
import 'password_reset_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
String? _errorMessage;
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_errorMessage = null;
});
final userProvider = Provider.of<UserProvider>(context, listen: false);
final success = await userProvider.login(
_usernameController.text.trim(),
_passwordController.text,
);
if (success) {
if (mounted) {
Navigator.of(context).pop(); // Zurück zur vorherigen Seite
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erfolgreich eingeloggt!'),
backgroundColor: Colors.green,
),
);
}
} else {
setState(() {
_errorMessage = 'Ungültige Anmeldedaten. Bitte versuche es erneut.';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Anmelden'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 32),
// Logo oder Titel
Icon(
Icons.account_circle,
size: 80,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 16),
Text(
'Willkommen zurück',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Melde dich an, um deine Bestellungen zu sehen',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
// Fehlermeldung
if (_errorMessage != null)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red[300]!),
),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red[700]),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(color: Colors.red[700]),
),
),
],
),
),
// Benutzername/E-Mail Feld
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'Benutzername oder E-Mail',
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte gib deinen Benutzernamen oder E-Mail ein';
}
return null;
},
),
const SizedBox(height: 16),
// Passwort Feld
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Passwort',
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _handleLogin(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte gib dein Passwort ein';
}
return null;
},
),
const SizedBox(height: 24),
// Anmelden Button
Consumer<UserProvider>(
builder: (context, userProvider, child) {
return ElevatedButton(
onPressed: userProvider.isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF8B6F47),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: userProvider.isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text(
'Anmelden',
style: TextStyle(fontSize: 16),
),
);
},
),
const SizedBox(height: 8),
// Passwort vergessen Link
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const PasswordResetScreen(),
),
);
},
child: const Text('Passwort vergessen?'),
),
),
const SizedBox(height: 16),
// Registrieren Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Noch kein Konto? '),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RegisterScreen(),
),
);
},
child: const Text('Jetzt registrieren'),
),
],
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.dart';
import '../providers/user_provider.dart';
import '../services/analytics_service.dart';
import 'home_screen.dart';
import 'categories_screen.dart';
import 'cart_screen.dart';
import 'profile_screen.dart';
import 'login_screen.dart';
class MainNavigationScreen extends StatefulWidget {
const MainNavigationScreen({super.key});
@override
State<MainNavigationScreen> createState() => _MainNavigationScreenState();
}
class _MainNavigationScreenState extends State<MainNavigationScreen> {
int _currentIndex = 0;
final List<Widget> _screens = [
const HomeScreen(),
const CategoriesScreen(),
const CartScreen(),
const ProfileScreen(),
];
@override
void initState() {
super.initState();
// Track initial page view
AnalyticsService.trackPageView('/home');
}
void _onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
// Track page views
switch (index) {
case 0:
AnalyticsService.trackPageView('/home');
break;
case 1:
AnalyticsService.trackPageView('/categories');
break;
case 2:
AnalyticsService.trackPageView('/cart');
break;
case 3:
AnalyticsService.trackPageView('/profile');
break;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _screens,
),
bottomNavigationBar: Consumer2<CartProvider, UserProvider>(
builder: (context, cart, userProvider, child) {
return NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: _onTabTapped,
destinations: [
const NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: 'Home',
),
const NavigationDestination(
icon: Icon(Icons.category_outlined),
selectedIcon: Icon(Icons.category),
label: 'Kategorien',
),
NavigationDestination(
icon: Stack(
children: [
const Icon(Icons.shopping_cart_outlined),
if (cart.itemCount > 0)
Positioned(
right: 0,
top: 0,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
'${cart.itemCount}',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
),
selectedIcon: Stack(
children: [
const Icon(Icons.shopping_cart),
if (cart.itemCount > 0)
Positioned(
right: 0,
top: 0,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
'${cart.itemCount}',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
),
label: 'Warenkorb',
),
NavigationDestination(
icon: Icon(
userProvider.isLoggedIn
? Icons.account_circle_outlined
: Icons.login_outlined,
),
selectedIcon: Icon(
userProvider.isLoggedIn ? Icons.account_circle : Icons.login,
),
label: userProvider.isLoggedIn ? 'Profil' : 'Anmelden',
),
],
);
},
),
);
}
}

View File

@@ -0,0 +1,293 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:intl/intl.dart';
import '../models/order.dart';
class OrderDetailScreen extends StatelessWidget {
final Order order;
const OrderDetailScreen({
super.key,
required this.order,
});
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('dd.MM.yyyy HH:mm');
final statusColor = _getStatusColor(order.status);
return Scaffold(
appBar: AppBar(
title: Text('Bestellung #${order.id}'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Status
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: statusColor),
),
child: Row(
children: [
Icon(Icons.info_outline, color: statusColor),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
Text(
order.statusDisplay,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
// Bestelldatum
_buildInfoRow(
context,
'Bestelldatum',
dateFormat.format(order.dateCreated),
Icons.calendar_today,
),
const SizedBox(height: 16),
// Produkte
Text(
'Produkte',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
...order.items.map((item) => _buildOrderItem(context, item)),
const SizedBox(height: 16),
// Zahlungsmethode
if (order.paymentMethodTitle != null) ...[
_buildInfoRow(
context,
'Zahlungsmethode',
order.paymentMethodTitle!,
Icons.payment,
),
const SizedBox(height: 16),
],
// Rechnungsadresse
if (order.billing != null) ...[
Text(
'Rechnungsadresse',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
order.billing!.fullAddress,
style: Theme.of(context).textTheme.bodyMedium,
),
),
const SizedBox(height: 16),
],
// Lieferadresse
if (order.shipping != null) ...[
Text(
'Lieferadresse',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
order.shipping!.fullAddress,
style: Theme.of(context).textTheme.bodyMedium,
),
),
const SizedBox(height: 16),
],
// Gesamtsumme
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF8B6F47).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Gesamtsumme:',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'${order.total} ${order.currency}',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: const Color(0xFF8B6F47),
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
),
);
}
Widget _buildInfoRow(
BuildContext context,
String label,
String value,
IconData icon,
) {
return Row(
children: [
Icon(icon, size: 20, color: Colors.grey[600]),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
Text(
value,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
),
],
);
}
Widget _buildOrderItem(BuildContext context, OrderItem item) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
if (item.imageUrl != null)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: item.imageUrl!,
width: 60,
height: 60,
fit: BoxFit.cover,
errorWidget: (context, url, error) => Container(
width: 60,
height: 60,
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported),
),
),
)
else
Container(
width: 60,
height: 60,
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.name,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Menge: ${item.quantity}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
Text(
'${item.price}',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: const Color(0xFF8B6F47),
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
Color _getStatusColor(String status) {
switch (status) {
case 'completed':
return Colors.green;
case 'processing':
return Colors.blue;
case 'pending':
return Colors.orange;
case 'on-hold':
return Colors.amber;
case 'cancelled':
return Colors.red;
case 'refunded':
return Colors.purple;
case 'failed':
return Colors.red;
default:
return Colors.grey;
}
}
}

View File

@@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:intl/intl.dart';
import '../models/order.dart';
import '../services/order_service.dart';
import 'order_detail_screen.dart';
class OrdersScreen extends StatefulWidget {
const OrdersScreen({super.key});
@override
State<OrdersScreen> createState() => _OrdersScreenState();
}
class _OrdersScreenState extends State<OrdersScreen> {
final OrderService _orderService = OrderService();
List<Order> _orders = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadOrders();
}
Future<void> _loadOrders() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final orders = await _orderService.getOrders();
setState(() {
_orders = orders;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Meine Bestellungen'),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'Fehler beim Laden der Bestellungen',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_error!,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadOrders,
child: const Text('Erneut versuchen'),
),
],
),
)
: _orders.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.shopping_bag_outlined,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Noch keine Bestellungen',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'Deine Bestellungen werden hier angezeigt',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[500],
),
),
],
),
)
: RefreshIndicator(
onRefresh: _loadOrders,
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _orders.length,
itemBuilder: (context, index) {
final order = _orders[index];
return _buildOrderCard(order);
},
),
),
);
}
Widget _buildOrderCard(Order order) {
final dateFormat = DateFormat('dd.MM.yyyy');
final statusColor = _getStatusColor(order.status);
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OrderDetailScreen(order: order),
),
);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bestellung #${order.id}',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
dateFormat.format(order.dateCreated),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: statusColor),
),
child: Text(
order.statusDisplay,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 12),
// Produktvorschau
if (order.items.isNotEmpty) ...[
Row(
children: [
if (order.items[0].imageUrl != null)
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: order.items[0].imageUrl!,
width: 40,
height: 40,
fit: BoxFit.cover,
errorWidget: (context, url, error) => Container(
width: 40,
height: 40,
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported, size: 20),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
order.items.length == 1
? order.items[0].name
: '${order.items[0].name} + ${order.items.length - 1} weitere',
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 12),
],
// Gesamtsumme
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Gesamtsumme:',
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
'${order.total} ${order.currency}',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: const Color(0xFF8B6F47),
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
),
);
}
Color _getStatusColor(String status) {
switch (status) {
case 'completed':
return Colors.green;
case 'processing':
return Colors.blue;
case 'pending':
return Colors.orange;
case 'on-hold':
return Colors.amber;
case 'cancelled':
return Colors.red;
case 'refunded':
return Colors.purple;
case 'failed':
return Colors.red;
default:
return Colors.grey;
}
}
}

View File

@@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:email_validator/email_validator.dart';
import '../services/auth_service.dart';
import '../utils/error_handler.dart';
class PasswordResetScreen extends StatefulWidget {
const PasswordResetScreen({super.key});
@override
State<PasswordResetScreen> createState() => _PasswordResetScreenState();
}
class _PasswordResetScreenState extends State<PasswordResetScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
bool _isLoading = false;
bool _emailSent = false;
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
Future<void> _handlePasswordReset() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
final authService = AuthService();
final result = await authService.requestPasswordReset(
_emailController.text.trim(),
);
if (mounted) {
setState(() {
_isLoading = false;
_emailSent = result['success'] == true;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result['message'] ?? 'E-Mail wurde gesendet'),
backgroundColor: result['success'] == true ? Colors.green : Colors.orange,
),
);
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
ErrorHandler.showError(context, e);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Passwort zurücksetzen'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
Icons.lock_reset,
size: 80,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 16),
Text(
_emailSent ? 'E-Mail gesendet' : 'Passwort zurücksetzen',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
_emailSent
? 'Wir haben dir eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts gesendet.'
: 'Gib deine E-Mail-Adresse ein und wir senden dir einen Link zum Zurücksetzen deines Passworts.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
if (!_emailSent) ...[
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'E-Mail',
prefixIcon: const Icon(Icons.email),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _handlePasswordReset(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte gib deine E-Mail ein';
}
if (!EmailValidator.validate(value)) {
return 'Bitte gib eine gültige E-Mail ein';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _handlePasswordReset,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF8B6F47),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text(
'Reset-Link senden',
style: TextStyle(fontSize: 16),
),
),
] else ...[
Icon(
Icons.check_circle,
size: 64,
color: Colors.green,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Zurück zum Login'),
),
],
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,259 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../models/product.dart';
import '../providers/cart_provider.dart';
class ProductDetailScreen extends StatelessWidget {
final Product product;
const ProductDetailScreen({
super.key,
required this.product,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(product.name),
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Produktbild
if (product.imageUrl != null)
SizedBox(
height: 300,
width: double.infinity,
child: CachedNetworkImage(
imageUrl: product.imageUrl!,
fit: BoxFit.cover,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => Container(
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported, size: 64),
),
),
)
else
Container(
height: 300,
width: double.infinity,
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported, size: 64),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Produktname
Text(
product.name,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Preis
Row(
children: [
if (product.isOnSale) ...[
Text(
'${product.regularPrice}',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
decoration: TextDecoration.lineThrough,
color: Colors.grey,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Sale',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
const Spacer(),
Text(
'${product.price}',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: const Color(0xFF8B6F47),
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
// Verfügbarkeit
Row(
children: [
Icon(
product.inStock ? Icons.check_circle : Icons.cancel,
color: product.inStock ? Colors.green : Colors.red,
size: 20,
),
const SizedBox(width: 8),
Text(
product.inStock
? 'Auf Lager'
: 'Nicht verfügbar',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: product.inStock ? Colors.green : Colors.red,
),
),
],
),
const SizedBox(height: 24),
// Beschreibung
if (product.description.isNotEmpty) ...[
Text(
'Beschreibung',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
product.description.replaceAll(RegExp(r'<[^>]*>'), ''),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
],
// Zusätzliche Bilder
if (product.images.length > 1) ...[
Text(
'Weitere Bilder',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: product.images.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: CachedNetworkImage(
imageUrl: product.images[index],
width: 100,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
width: 100,
color: Colors.grey[300],
child: const Center(
child: CircularProgressIndicator(),
),
),
errorWidget: (context, url, error) => Container(
width: 100,
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported),
),
),
);
},
),
),
const SizedBox(height: 24),
],
],
),
),
],
),
),
bottomNavigationBar: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Consumer<CartProvider>(
builder: (context, cart, child) {
final cartItem = cart.items.firstWhere(
(item) => item.product.id == product.id,
orElse: () => CartItem(product: product, quantity: 0),
);
return Row(
children: [
if (cartItem.quantity > 0) ...[
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () => cart.removeItem(product),
),
Text(
'${cartItem.quantity}',
style: Theme.of(context).textTheme.titleLarge,
),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () => cart.addItem(product),
),
] else
Expanded(
child: ElevatedButton(
onPressed: product.inStock
? () {
cart.addItem(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${product.name} zum Warenkorb hinzugefügt'),
duration: const Duration(seconds: 2),
),
);
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF8B6F47),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
product.inStock ? 'Zum Warenkorb hinzufügen' : 'Nicht verfügbar',
style: const TextStyle(fontSize: 16),
),
),
),
],
);
},
),
),
);
}
}

View File

@@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.dart';
import '../models/product.dart';
import '../services/woocommerce_service.dart';
import '../widgets/product_card.dart';
import 'product_detail_screen.dart';
class ProductsScreen extends StatefulWidget {
final String? categoryId;
final String? categoryName;
const ProductsScreen({
super.key,
this.categoryId,
this.categoryName,
});
@override
State<ProductsScreen> createState() => _ProductsScreenState();
}
class _ProductsScreenState extends State<ProductsScreen> {
final WooCommerceService _wooCommerceService = WooCommerceService();
List<Product> _products = [];
bool _isLoading = true;
bool _isLoadingMore = false;
String? _error;
int _currentPage = 1;
final int _perPage = 20;
bool _hasMore = true;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_loadProducts();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent * 0.9) {
if (!_isLoadingMore && _hasMore) {
_loadMoreProducts();
}
}
}
Future<void> _loadProducts() async {
try {
setState(() {
_isLoading = true;
_error = null;
});
final products = await _wooCommerceService.getProducts(
perPage: _perPage,
page: 1,
category: widget.categoryId,
);
setState(() {
_products = products;
_isLoading = false;
_currentPage = 1;
_hasMore = products.length == _perPage;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Future<void> _loadMoreProducts() async {
if (_isLoadingMore || !_hasMore) return;
try {
setState(() {
_isLoadingMore = true;
});
final nextPage = _currentPage + 1;
final products = await _wooCommerceService.getProducts(
perPage: _perPage,
page: nextPage,
);
setState(() {
if (products.isEmpty) {
_hasMore = false;
} else {
_products.addAll(products);
_currentPage = nextPage;
_hasMore = products.length == _perPage;
}
_isLoadingMore = false;
});
} catch (e) {
setState(() {
_isLoadingMore = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.categoryName ?? 'Alle Produkte'),
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'Fehler beim Laden der Produkte',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_error!,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadProducts,
child: const Text('Erneut versuchen'),
),
],
),
);
}
if (_products.isEmpty) {
return const Center(
child: Text('Keine Produkte verfügbar'),
);
}
return RefreshIndicator(
onRefresh: _loadProducts,
child: GridView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.75,
),
itemCount: _products.length + (_isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == _products.length) {
return const Center(child: CircularProgressIndicator());
}
return ProductCard(
product: _products[index],
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailScreen(
product: _products[index],
),
),
);
},
);
},
),
);
}
}

View File

@@ -0,0 +1,446 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/user_provider.dart';
import '../services/auth_service.dart';
import '../services/order_service.dart';
import '../models/address.dart';
import '../models/order.dart';
import '../utils/error_handler.dart';
import '../widgets/loading_widget.dart';
import 'orders_screen.dart';
import 'login_screen.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final AuthService _authService = AuthService();
final OrderService _orderService = OrderService();
Address? _billingAddress;
Address? _shippingAddress;
bool _isLoadingAddresses = false;
@override
void initState() {
super.initState();
_loadAddresses();
}
Address? _convertBillingAddress(BillingAddress? billing) {
if (billing == null) return null;
return Address(
firstName: billing.firstName,
lastName: billing.lastName,
company: billing.company,
address1: billing.address1,
address2: billing.address2,
city: billing.city,
state: billing.state,
postcode: billing.postcode,
country: billing.country,
email: billing.email,
phone: billing.phone,
);
}
Address? _convertShippingAddress(ShippingAddress? shipping) {
if (shipping == null) return null;
return Address(
firstName: shipping.firstName,
lastName: shipping.lastName,
company: shipping.company,
address1: shipping.address1,
address2: shipping.address2,
city: shipping.city,
state: shipping.state,
postcode: shipping.postcode,
country: shipping.country,
);
}
Future<void> _loadAddresses() async {
setState(() {
_isLoadingAddresses = true;
});
try {
// Hole letzte Bestellung für Adressen
final orders = await _orderService.getOrders(perPage: 1);
if (orders.isNotEmpty) {
setState(() {
_billingAddress = _convertBillingAddress(orders[0].billing);
_shippingAddress = _convertShippingAddress(orders[0].shipping);
});
}
} catch (e) {
// Fehler ignorieren - Adressen sind optional
} finally {
setState(() {
_isLoadingAddresses = false;
});
}
}
Future<void> _updateProfile() async {
final userProvider = Provider.of<UserProvider>(context, listen: false);
if (userProvider.user == null) return;
final firstName = userProvider.user!.firstName ?? '';
final lastName = userProvider.user!.lastName ?? '';
showDialog(
context: context,
builder: (context) => _ProfileEditDialog(
firstName: firstName,
lastName: lastName,
email: userProvider.user!.email,
onSave: (firstName, lastName, email) async {
final updatedUser = await _authService.updateProfile(
firstName: firstName,
lastName: lastName,
email: email,
);
if (updatedUser != null && mounted) {
await userProvider.refreshUser();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Profil erfolgreich aktualisiert'),
backgroundColor: Colors.green,
),
);
} else {
if (mounted) {
Navigator.pop(context);
ErrorHandler.showError(context, 'Fehler beim Aktualisieren des Profils');
}
}
},
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Mein Profil'),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: _updateProfile,
),
],
),
body: Consumer<UserProvider>(
builder: (context, userProvider, child) {
if (userProvider.user == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.account_circle, size: 64, color: Colors.grey),
const SizedBox(height: 16),
const Text('Bitte melde dich an'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LoginScreen(),
),
);
},
child: const Text('Anmelden'),
),
],
),
);
}
final user = userProvider.user!;
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Profil-Header
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 40,
backgroundColor: const Color(0xFF8B6F47),
child: Text(
user.fullName[0].toUpperCase(),
style: const TextStyle(
fontSize: 32,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.fullName,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
user.email,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
],
),
),
),
const SizedBox(height: 24),
// Menü-Optionen
Text(
'Konto',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Card(
child: Column(
children: [
ListTile(
leading: const Icon(Icons.shopping_bag),
title: const Text('Meine Bestellungen'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const OrdersScreen(),
),
);
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.person),
title: const Text('Profil bearbeiten'),
trailing: const Icon(Icons.chevron_right),
onTap: _updateProfile,
),
],
),
),
const SizedBox(height: 24),
// Adressen
Text(
'Adressen',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
if (_isLoadingAddresses)
const LoadingWidget()
else ...[
if (_billingAddress != null && !_billingAddress!.isEmpty)
Card(
child: ListTile(
leading: const Icon(Icons.home),
title: const Text('Rechnungsadresse'),
subtitle: Text(_billingAddress!.fullAddress),
trailing: const Icon(Icons.chevron_right),
onTap: () {
_showAddressDialog('Rechnungsadresse', _billingAddress!);
},
),
),
if (_shippingAddress != null && !_shippingAddress!.isEmpty)
Card(
margin: const EdgeInsets.only(top: 8),
child: ListTile(
leading: const Icon(Icons.local_shipping),
title: const Text('Lieferadresse'),
subtitle: Text(_shippingAddress!.fullAddress),
trailing: const Icon(Icons.chevron_right),
onTap: () {
_showAddressDialog('Lieferadresse', _shippingAddress!);
},
),
),
if ((_billingAddress == null || _billingAddress!.isEmpty) &&
(_shippingAddress == null || _shippingAddress!.isEmpty))
Card(
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Keine Adressen gespeichert'),
),
),
],
const SizedBox(height: 24),
// Abmelden
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () async {
await userProvider.logout();
if (mounted) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => const LoginScreen(),
),
(route) => false,
);
}
},
icon: const Icon(Icons.logout, color: Colors.red),
label: const Text(
'Abmelden',
style: TextStyle(color: Colors.red),
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
],
),
);
},
),
);
}
void _showAddressDialog(String title, Address address) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: SingleChildScrollView(
child: Text(address.fullAddress),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Schließen'),
),
],
),
);
}
}
class _ProfileEditDialog extends StatefulWidget {
final String firstName;
final String lastName;
final String email;
final Function(String, String, String) onSave;
const _ProfileEditDialog({
required this.firstName,
required this.lastName,
required this.email,
required this.onSave,
});
@override
State<_ProfileEditDialog> createState() => _ProfileEditDialogState();
}
class _ProfileEditDialogState extends State<_ProfileEditDialog> {
late TextEditingController _firstNameController;
late TextEditingController _lastNameController;
late TextEditingController _emailController;
@override
void initState() {
super.initState();
_firstNameController = TextEditingController(text: widget.firstName);
_lastNameController = TextEditingController(text: widget.lastName);
_emailController = TextEditingController(text: widget.email);
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Profil bearbeiten'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _firstNameController,
decoration: const InputDecoration(
labelText: 'Vorname',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _lastNameController,
decoration: const InputDecoration(
labelText: 'Nachname',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'E-Mail',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Abbrechen'),
),
ElevatedButton(
onPressed: () {
widget.onSave(
_firstNameController.text.trim(),
_lastNameController.text.trim(),
_emailController.text.trim(),
);
},
child: const Text('Speichern'),
),
],
);
}
}

View File

@@ -0,0 +1,353 @@
import 'package:flutter/material.dart';
import 'package:email_validator/email_validator.dart';
import '../services/auth_service.dart';
import '../utils/error_handler.dart';
import 'login_screen.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
bool _isLoading = false;
bool _acceptTerms = false;
@override
void dispose() {
_emailController.dispose();
_usernameController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_firstNameController.dispose();
_lastNameController.dispose();
super.dispose();
}
Future<void> _handleRegister() async {
if (!_formKey.currentState!.validate()) {
return;
}
if (!_acceptTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Bitte akzeptiere die Nutzungsbedingungen'),
backgroundColor: Colors.red,
),
);
return;
}
setState(() {
_isLoading = true;
});
try {
final authService = AuthService();
final result = await authService.register(
email: _emailController.text.trim(),
username: _usernameController.text.trim(),
password: _passwordController.text,
firstName: _firstNameController.text.trim(),
lastName: _lastNameController.text.trim(),
);
if (mounted) {
setState(() {
_isLoading = false;
});
if (result['success'] == true) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Registrierung erfolgreich! Du kannst dich jetzt anmelden.'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result['message'] ?? 'Registrierung fehlgeschlagen'),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
ErrorHandler.showError(context, e);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Registrieren'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
Icons.person_add,
size: 80,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 16),
Text(
'Account erstellen',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Vorname
TextFormField(
controller: _firstNameController,
decoration: InputDecoration(
labelText: 'Vorname',
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
// Nachname
TextFormField(
controller: _lastNameController,
decoration: InputDecoration(
labelText: 'Nachname',
prefixIcon: const Icon(Icons.person_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
// E-Mail
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'E-Mail',
prefixIcon: const Icon(Icons.email),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte gib deine E-Mail ein';
}
if (!EmailValidator.validate(value)) {
return 'Bitte gib eine gültige E-Mail ein';
}
return null;
},
),
const SizedBox(height: 16),
// Benutzername
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'Benutzername',
prefixIcon: const Icon(Icons.account_circle),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte gib einen Benutzernamen ein';
}
if (value.length < 3) {
return 'Benutzername muss mindestens 3 Zeichen lang sein';
}
return null;
},
),
const SizedBox(height: 16),
// Passwort
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Passwort',
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
obscureText: _obscurePassword,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte gib ein Passwort ein';
}
if (value.length < 6) {
return 'Passwort muss mindestens 6 Zeichen lang sein';
}
return null;
},
),
const SizedBox(height: 16),
// Passwort bestätigen
TextFormField(
controller: _confirmPasswordController,
decoration: InputDecoration(
labelText: 'Passwort bestätigen',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
obscureText: _obscureConfirmPassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _handleRegister(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte bestätige dein Passwort';
}
if (value != _passwordController.text) {
return 'Passwörter stimmen nicht überein';
}
return null;
},
),
const SizedBox(height: 16),
// Nutzungsbedingungen
Row(
children: [
Checkbox(
value: _acceptTerms,
onChanged: (value) {
setState(() {
_acceptTerms = value ?? false;
});
},
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_acceptTerms = !_acceptTerms;
});
},
child: Text(
'Ich akzeptiere die Nutzungsbedingungen und Datenschutzerklärung',
style: Theme.of(context).textTheme.bodySmall,
),
),
),
],
),
const SizedBox(height: 24),
// Registrieren Button
ElevatedButton(
onPressed: _isLoading ? null : _handleRegister,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF8B6F47),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text(
'Registrieren',
style: TextStyle(fontSize: 16),
),
),
const SizedBox(height: 16),
// Link zum Login
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Bereits ein Konto? '),
TextButton(
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const LoginScreen(),
),
);
},
child: const Text('Jetzt anmelden'),
),
],
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import '../models/product.dart';
import '../services/woocommerce_service.dart';
import '../services/analytics_service.dart';
import '../widgets/product_card.dart';
import '../widgets/loading_widget.dart';
import '../widgets/retry_widget.dart';
import '../utils/error_handler.dart';
import 'product_detail_screen.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final WooCommerceService _wooCommerceService = WooCommerceService();
final TextEditingController _searchController = TextEditingController();
List<Product> _products = [];
bool _isLoading = false;
String? _error;
String _lastSearchQuery = '';
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _performSearch(String query) async {
if (query.trim().isEmpty) {
setState(() {
_products = [];
_error = null;
});
return;
}
setState(() {
_isLoading = true;
_error = null;
_lastSearchQuery = query;
});
// Track Search
AnalyticsService.trackSearch(query);
try {
final products = await _wooCommerceService.getProducts(
search: query,
perPage: 50,
);
setState(() {
_products = products;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
ErrorHandler.showError(context, e);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Suche'),
),
body: Column(
children: [
// Suchleiste
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Produkte suchen...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_performSearch('');
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Colors.grey[100],
),
textInputAction: TextInputAction.search,
onSubmitted: _performSearch,
onChanged: (value) {
setState(() {});
// Optional: Live-Suche mit Debounce
// _debounceSearch(value);
},
),
),
// Ergebnisse
Expanded(
child: _isLoading
? const LoadingWidget(message: 'Suche läuft...')
: _error != null
? RetryWidget(
message: 'Fehler bei der Suche',
onRetry: () => _performSearch(_lastSearchQuery),
)
: _products.isEmpty && _lastSearchQuery.isNotEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Keine Ergebnisse gefunden',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'Versuche es mit anderen Suchbegriffen',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[500],
),
),
],
),
)
: _products.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Suche nach Produkten',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.grey[600],
),
),
],
),
)
: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.75,
),
itemCount: _products.length,
itemBuilder: (context, index) {
return ProductCard(
product: _products[index],
onTap: () {
AnalyticsService.trackProductView(
_products[index].id,
_products[index].name,
);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailScreen(
product: _products[index],
),
),
);
},
);
},
),
),
],
),
);
}
}