Initialer Commit: Projekt Start
This commit is contained in:
BIN
lib/.DS_Store
vendored
Normal file
BIN
lib/.DS_Store
vendored
Normal file
Binary file not shown.
7
lib/env.dart
Normal file
7
lib/env.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
// Environment Variables
|
||||
// Diese Datei wird für Umgebungsvariablen verwendet (falls benötigt)
|
||||
|
||||
// Du kannst hier Umgebungsvariablen definieren, falls du sie später brauchst
|
||||
// Beispiel:
|
||||
// const String apiUrl = String.fromEnvironment('API_URL', defaultValue: 'https://hyggecraftery.com');
|
||||
|
||||
51
lib/main.dart
Normal file
51
lib/main.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hyggecraftery/providers/cart_provider.dart';
|
||||
import 'package:hyggecraftery/providers/user_provider.dart';
|
||||
import 'package:hyggecraftery/screens/main_navigation_screen.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const HyggeCrafteryApp());
|
||||
}
|
||||
|
||||
class HyggeCrafteryApp extends StatelessWidget {
|
||||
const HyggeCrafteryApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => UserProvider()),
|
||||
ChangeNotifierProvider(create: (_) => CartProvider()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'HyggeCraftery',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.brown,
|
||||
primaryColor: const Color(0xFF8B6F47),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF8B6F47),
|
||||
primary: const Color(0xFF8B6F47),
|
||||
secondary: const Color(0xFFD4A574),
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFFFAF7F2),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xFF8B6F47),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const MainNavigationScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
78
lib/models/address.dart
Normal file
78
lib/models/address.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
class Address {
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? company;
|
||||
final String? address1;
|
||||
final String? address2;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? postcode;
|
||||
final String? country;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
|
||||
Address({
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.company,
|
||||
this.address1,
|
||||
this.address2,
|
||||
this.city,
|
||||
this.state,
|
||||
this.postcode,
|
||||
this.country,
|
||||
this.email,
|
||||
this.phone,
|
||||
});
|
||||
|
||||
factory Address.fromJson(Map<String, dynamic> json) {
|
||||
return Address(
|
||||
firstName: json['first_name'],
|
||||
lastName: json['last_name'],
|
||||
company: json['company'],
|
||||
address1: json['address_1'] ?? json['address1'],
|
||||
address2: json['address_2'] ?? json['address2'],
|
||||
city: json['city'],
|
||||
state: json['state'],
|
||||
postcode: json['postcode'],
|
||||
country: json['country'],
|
||||
email: json['email'],
|
||||
phone: json['phone'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'company': company,
|
||||
'address_1': address1,
|
||||
'address_2': address2,
|
||||
'city': city,
|
||||
'state': state,
|
||||
'postcode': postcode,
|
||||
'country': country,
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
};
|
||||
}
|
||||
|
||||
String get fullAddress {
|
||||
final parts = <String>[];
|
||||
if (address1 != null && address1!.isNotEmpty) parts.add(address1!);
|
||||
if (address2 != null && address2!.isNotEmpty) parts.add(address2!);
|
||||
if (postcode != null && postcode!.isNotEmpty) parts.add(postcode!);
|
||||
if (city != null && city!.isNotEmpty) parts.add(city!);
|
||||
if (state != null && state!.isNotEmpty) parts.add(state!);
|
||||
if (country != null && country!.isNotEmpty) parts.add(country!);
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
bool get isEmpty {
|
||||
return (firstName == null || firstName!.isEmpty) &&
|
||||
(lastName == null || lastName!.isEmpty) &&
|
||||
(address1 == null || address1!.isEmpty) &&
|
||||
(city == null || city!.isEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
44
lib/models/category.dart
Normal file
44
lib/models/category.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
class Category {
|
||||
final int id;
|
||||
final String name;
|
||||
final String? slug;
|
||||
final String? description;
|
||||
final int? parent;
|
||||
final int count;
|
||||
final String? imageUrl;
|
||||
|
||||
Category({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.slug,
|
||||
this.description,
|
||||
this.parent,
|
||||
this.count = 0,
|
||||
this.imageUrl,
|
||||
});
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json) {
|
||||
return Category(
|
||||
id: json['id'] ?? 0,
|
||||
name: json['name'] ?? '',
|
||||
slug: json['slug'],
|
||||
description: json['description'],
|
||||
parent: json['parent'],
|
||||
count: json['count'] ?? 0,
|
||||
imageUrl: json['image']?['src'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'slug': slug,
|
||||
'description': description,
|
||||
'parent': parent,
|
||||
'count': count,
|
||||
'image': imageUrl != null ? {'src': imageUrl} : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
42
lib/models/coupon.dart
Normal file
42
lib/models/coupon.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
class Coupon {
|
||||
final String code;
|
||||
final String? description;
|
||||
final String discountType; // fixed_cart, percent, fixed_product, percent_product
|
||||
final String amount;
|
||||
final bool isValid;
|
||||
final String? errorMessage;
|
||||
|
||||
Coupon({
|
||||
required this.code,
|
||||
this.description,
|
||||
required this.discountType,
|
||||
required this.amount,
|
||||
this.isValid = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
factory Coupon.fromJson(Map<String, dynamic> json) {
|
||||
return Coupon(
|
||||
code: json['code'] ?? '',
|
||||
description: json['description'],
|
||||
discountType: json['discount_type'] ?? 'fixed_cart',
|
||||
amount: json['amount'] ?? '0',
|
||||
isValid: json['is_valid'] ?? false,
|
||||
errorMessage: json['error_message'],
|
||||
);
|
||||
}
|
||||
|
||||
String get discountDisplay {
|
||||
switch (discountType) {
|
||||
case 'percent':
|
||||
case 'percent_product':
|
||||
return '$amount%';
|
||||
case 'fixed_cart':
|
||||
case 'fixed_product':
|
||||
return '$amount €';
|
||||
default:
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
201
lib/models/order.dart
Normal file
201
lib/models/order.dart
Normal file
@@ -0,0 +1,201 @@
|
||||
class Order {
|
||||
final int id;
|
||||
final String status;
|
||||
final String total;
|
||||
final String currency;
|
||||
final DateTime dateCreated;
|
||||
final List<OrderItem> items;
|
||||
final BillingAddress? billing;
|
||||
final ShippingAddress? shipping;
|
||||
final String? paymentMethod;
|
||||
final String? paymentMethodTitle;
|
||||
|
||||
Order({
|
||||
required this.id,
|
||||
required this.status,
|
||||
required this.total,
|
||||
required this.currency,
|
||||
required this.dateCreated,
|
||||
required this.items,
|
||||
this.billing,
|
||||
this.shipping,
|
||||
this.paymentMethod,
|
||||
this.paymentMethodTitle,
|
||||
});
|
||||
|
||||
factory Order.fromJson(Map<String, dynamic> json) {
|
||||
final items = (json['line_items'] as List<dynamic>?)
|
||||
?.map((item) => OrderItem.fromJson(item))
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
return Order(
|
||||
id: json['id'] ?? 0,
|
||||
status: json['status'] ?? '',
|
||||
total: json['total'] ?? '0',
|
||||
currency: json['currency'] ?? 'EUR',
|
||||
dateCreated: json['date_created'] != null
|
||||
? DateTime.parse(json['date_created'])
|
||||
: DateTime.now(),
|
||||
items: items,
|
||||
billing: json['billing'] != null
|
||||
? BillingAddress.fromJson(json['billing'])
|
||||
: null,
|
||||
shipping: json['shipping'] != null
|
||||
? ShippingAddress.fromJson(json['shipping'])
|
||||
: null,
|
||||
paymentMethod: json['payment_method'],
|
||||
paymentMethodTitle: json['payment_method_title'],
|
||||
);
|
||||
}
|
||||
|
||||
String get statusDisplay {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'Ausstehend';
|
||||
case 'processing':
|
||||
return 'In Bearbeitung';
|
||||
case 'on-hold':
|
||||
return 'Wartend';
|
||||
case 'completed':
|
||||
return 'Abgeschlossen';
|
||||
case 'cancelled':
|
||||
return 'Storniert';
|
||||
case 'refunded':
|
||||
return 'Erstattet';
|
||||
case 'failed':
|
||||
return 'Fehlgeschlagen';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OrderItem {
|
||||
final int id;
|
||||
final String name;
|
||||
final int quantity;
|
||||
final String price;
|
||||
final String? imageUrl;
|
||||
|
||||
OrderItem({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.quantity,
|
||||
required this.price,
|
||||
this.imageUrl,
|
||||
});
|
||||
|
||||
factory OrderItem.fromJson(Map<String, dynamic> json) {
|
||||
return OrderItem(
|
||||
id: json['id'] ?? 0,
|
||||
name: json['name'] ?? '',
|
||||
quantity: json['quantity'] ?? 0,
|
||||
price: json['price'] ?? '0',
|
||||
imageUrl: json['image']?['src'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BillingAddress {
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? company;
|
||||
final String? address1;
|
||||
final String? address2;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? postcode;
|
||||
final String? country;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
|
||||
BillingAddress({
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.company,
|
||||
this.address1,
|
||||
this.address2,
|
||||
this.city,
|
||||
this.state,
|
||||
this.postcode,
|
||||
this.country,
|
||||
this.email,
|
||||
this.phone,
|
||||
});
|
||||
|
||||
factory BillingAddress.fromJson(Map<String, dynamic> json) {
|
||||
return BillingAddress(
|
||||
firstName: json['first_name'],
|
||||
lastName: json['last_name'],
|
||||
company: json['company'],
|
||||
address1: json['address_1'],
|
||||
address2: json['address_2'],
|
||||
city: json['city'],
|
||||
state: json['state'],
|
||||
postcode: json['postcode'],
|
||||
country: json['country'],
|
||||
email: json['email'],
|
||||
phone: json['phone'],
|
||||
);
|
||||
}
|
||||
|
||||
String get fullAddress {
|
||||
final parts = <String>[];
|
||||
if (address1 != null && address1!.isNotEmpty) parts.add(address1!);
|
||||
if (address2 != null && address2!.isNotEmpty) parts.add(address2!);
|
||||
if (postcode != null && postcode!.isNotEmpty) parts.add(postcode!);
|
||||
if (city != null && city!.isNotEmpty) parts.add(city!);
|
||||
if (country != null && country!.isNotEmpty) parts.add(country!);
|
||||
return parts.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
class ShippingAddress {
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? company;
|
||||
final String? address1;
|
||||
final String? address2;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? postcode;
|
||||
final String? country;
|
||||
|
||||
ShippingAddress({
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.company,
|
||||
this.address1,
|
||||
this.address2,
|
||||
this.city,
|
||||
this.state,
|
||||
this.postcode,
|
||||
this.country,
|
||||
});
|
||||
|
||||
factory ShippingAddress.fromJson(Map<String, dynamic> json) {
|
||||
return ShippingAddress(
|
||||
firstName: json['first_name'],
|
||||
lastName: json['last_name'],
|
||||
company: json['company'],
|
||||
address1: json['address_1'],
|
||||
address2: json['address_2'],
|
||||
city: json['city'],
|
||||
state: json['state'],
|
||||
postcode: json['postcode'],
|
||||
country: json['country'],
|
||||
);
|
||||
}
|
||||
|
||||
String get fullAddress {
|
||||
final parts = <String>[];
|
||||
if (address1 != null && address1!.isNotEmpty) parts.add(address1!);
|
||||
if (address2 != null && address2!.isNotEmpty) parts.add(address2!);
|
||||
if (postcode != null && postcode!.isNotEmpty) parts.add(postcode!);
|
||||
if (city != null && city!.isNotEmpty) parts.add(city!);
|
||||
if (country != null && country!.isNotEmpty) parts.add(country!);
|
||||
return parts.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
96
lib/models/product.dart
Normal file
96
lib/models/product.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
class Product {
|
||||
final int id;
|
||||
final String name;
|
||||
final String description;
|
||||
final String price;
|
||||
final String? regularPrice;
|
||||
final String? salePrice;
|
||||
final String? imageUrl;
|
||||
final List<String> images;
|
||||
final bool inStock;
|
||||
final int stockQuantity;
|
||||
final String? sku;
|
||||
final List<Map<String, dynamic>>? categories;
|
||||
final double? rating;
|
||||
final int? ratingCount;
|
||||
|
||||
Product({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.price,
|
||||
this.regularPrice,
|
||||
this.salePrice,
|
||||
this.imageUrl,
|
||||
this.images = const [],
|
||||
this.inStock = true,
|
||||
this.stockQuantity = 0,
|
||||
this.sku,
|
||||
this.categories,
|
||||
this.rating,
|
||||
this.ratingCount,
|
||||
});
|
||||
|
||||
factory Product.fromJson(Map<String, dynamic> json) {
|
||||
final images = <String>[];
|
||||
|
||||
if (json['images'] != null && json['images'] is List) {
|
||||
for (var img in json['images']) {
|
||||
if (img['src'] != null) {
|
||||
images.add(img['src']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Product(
|
||||
id: json['id'] ?? 0,
|
||||
name: json['name'] ?? '',
|
||||
description: json['description'] ?? '',
|
||||
price: json['price'] ?? '0',
|
||||
regularPrice: json['regular_price'],
|
||||
salePrice: json['sale_price'],
|
||||
imageUrl: images.isNotEmpty ? images[0] : null,
|
||||
images: images,
|
||||
inStock: json['stock_status'] == 'instock',
|
||||
stockQuantity: json['stock_quantity'] ?? 0,
|
||||
sku: json['sku'],
|
||||
categories: json['categories'] != null
|
||||
? List<Map<String, dynamic>>.from(json['categories'])
|
||||
: null,
|
||||
rating: json['average_rating'] != null
|
||||
? double.tryParse(json['average_rating'].toString())
|
||||
: null,
|
||||
ratingCount: json['rating_count'],
|
||||
);
|
||||
}
|
||||
|
||||
bool get isOnSale => salePrice != null && salePrice!.isNotEmpty;
|
||||
|
||||
double get priceValue {
|
||||
try {
|
||||
return double.parse(price);
|
||||
} catch (e) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'price': price,
|
||||
'regular_price': regularPrice,
|
||||
'sale_price': salePrice,
|
||||
'image_url': imageUrl,
|
||||
'images': images,
|
||||
'in_stock': inStock,
|
||||
'stock_quantity': stockQuantity,
|
||||
'sku': sku,
|
||||
'categories': categories,
|
||||
'average_rating': rating,
|
||||
'rating_count': ratingCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
80
lib/models/review.dart
Normal file
80
lib/models/review.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
class Review {
|
||||
final int id;
|
||||
final int productId;
|
||||
final String reviewer;
|
||||
final String reviewerEmail;
|
||||
final String review;
|
||||
final int rating;
|
||||
final bool verified;
|
||||
final DateTime dateCreated;
|
||||
final String? reviewerAvatarUrl;
|
||||
|
||||
Review({
|
||||
required this.id,
|
||||
required this.productId,
|
||||
required this.reviewer,
|
||||
required this.reviewerEmail,
|
||||
required this.review,
|
||||
required this.rating,
|
||||
this.verified = false,
|
||||
required this.dateCreated,
|
||||
this.reviewerAvatarUrl,
|
||||
});
|
||||
|
||||
factory Review.fromJson(Map<String, dynamic> json) {
|
||||
return Review(
|
||||
id: json['id'] ?? 0,
|
||||
productId: json['product_id'] ?? 0,
|
||||
reviewer: json['reviewer'] ?? '',
|
||||
reviewerEmail: json['reviewer_email'] ?? '',
|
||||
review: json['review'] ?? '',
|
||||
rating: json['rating'] ?? 0,
|
||||
verified: json['verified'] ?? false,
|
||||
dateCreated: json['date_created'] != null
|
||||
? DateTime.parse(json['date_created'])
|
||||
: DateTime.now(),
|
||||
reviewerAvatarUrl: json['reviewer_avatar_urls']?['96'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'product_id': productId,
|
||||
'reviewer': reviewer,
|
||||
'reviewer_email': reviewerEmail,
|
||||
'review': review,
|
||||
'rating': rating,
|
||||
'verified': verified,
|
||||
'date_created': dateCreated.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ProductRating {
|
||||
final double averageRating;
|
||||
final int ratingCount;
|
||||
final Map<int, int> ratingBreakdown; // rating -> count
|
||||
|
||||
ProductRating({
|
||||
required this.averageRating,
|
||||
required this.ratingCount,
|
||||
required this.ratingBreakdown,
|
||||
});
|
||||
|
||||
factory ProductRating.fromJson(Map<String, dynamic> json) {
|
||||
final breakdown = <int, int>{};
|
||||
if (json['rating_breakdown'] != null) {
|
||||
(json['rating_breakdown'] as Map).forEach((key, value) {
|
||||
breakdown[int.parse(key.toString())] = value as int;
|
||||
});
|
||||
}
|
||||
|
||||
return ProductRating(
|
||||
averageRating: (json['average_rating'] ?? 0.0).toDouble(),
|
||||
ratingCount: json['rating_count'] ?? 0,
|
||||
ratingBreakdown: breakdown,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
51
lib/models/user.dart
Normal file
51
lib/models/user.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
class User {
|
||||
final int id;
|
||||
final String email;
|
||||
final String? username;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? displayName;
|
||||
final String? avatarUrl;
|
||||
|
||||
User({
|
||||
required this.id,
|
||||
required this.email,
|
||||
this.username,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.displayName,
|
||||
this.avatarUrl,
|
||||
});
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) {
|
||||
return User(
|
||||
id: json['id'] ?? 0,
|
||||
email: json['email'] ?? '',
|
||||
username: json['username'],
|
||||
firstName: json['first_name'],
|
||||
lastName: json['last_name'],
|
||||
displayName: json['display_name'] ?? json['name'],
|
||||
avatarUrl: json['avatar_url'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'email': email,
|
||||
'username': username,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'display_name': displayName,
|
||||
'avatar_url': avatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
String get fullName {
|
||||
if (firstName != null && lastName != null) {
|
||||
return '$firstName $lastName';
|
||||
}
|
||||
return displayName ?? email;
|
||||
}
|
||||
}
|
||||
|
||||
118
lib/providers/cart_provider.dart
Normal file
118
lib/providers/cart_provider.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'dart:convert';
|
||||
import '../models/product.dart';
|
||||
|
||||
class CartItem {
|
||||
final Product product;
|
||||
int quantity;
|
||||
|
||||
CartItem({
|
||||
required this.product,
|
||||
this.quantity = 1,
|
||||
});
|
||||
|
||||
double get totalPrice => product.priceValue * quantity;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'product_id': product.id,
|
||||
'quantity': quantity,
|
||||
'product': product.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
factory CartItem.fromJson(Map<String, dynamic> json) {
|
||||
return CartItem(
|
||||
product: Product.fromJson(json['product']),
|
||||
quantity: json['quantity'] ?? 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CartProvider extends ChangeNotifier {
|
||||
final List<CartItem> _items = [];
|
||||
static const String _cartKey = 'cart_items';
|
||||
|
||||
List<CartItem> get items => _items;
|
||||
|
||||
int get itemCount => _items.fold(0, (sum, item) => sum + item.quantity);
|
||||
|
||||
double get totalPrice {
|
||||
return _items.fold(0.0, (sum, item) => sum + item.totalPrice);
|
||||
}
|
||||
|
||||
CartProvider() {
|
||||
_loadCart();
|
||||
}
|
||||
|
||||
/// Lädt gespeicherten Warenkorb
|
||||
Future<void> _loadCart() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cartJson = prefs.getString(_cartKey);
|
||||
|
||||
if (cartJson != null) {
|
||||
final List<dynamic> cartData = json.decode(cartJson);
|
||||
_items.clear();
|
||||
_items.addAll(
|
||||
cartData.map((item) => CartItem.fromJson(item)).toList(),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Fehler beim Laden des Warenkorbs: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Speichert Warenkorb
|
||||
Future<void> _saveCart() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cartJson = json.encode(
|
||||
_items.map((item) => item.toJson()).toList(),
|
||||
);
|
||||
await prefs.setString(_cartKey, cartJson);
|
||||
} catch (e) {
|
||||
print('Fehler beim Speichern des Warenkorbs: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void addItem(Product product) {
|
||||
final existingIndex = _items.indexWhere((item) => item.product.id == product.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
_items[existingIndex].quantity++;
|
||||
} else {
|
||||
_items.add(CartItem(product: product));
|
||||
}
|
||||
notifyListeners();
|
||||
_saveCart();
|
||||
}
|
||||
|
||||
void removeItem(Product product) {
|
||||
final existingIndex = _items.indexWhere((item) => item.product.id == product.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
if (_items[existingIndex].quantity > 1) {
|
||||
_items[existingIndex].quantity--;
|
||||
} else {
|
||||
_items.removeAt(existingIndex);
|
||||
}
|
||||
notifyListeners();
|
||||
_saveCart();
|
||||
}
|
||||
}
|
||||
|
||||
void removeItemCompletely(Product product) {
|
||||
_items.removeWhere((item) => item.product.id == product.id);
|
||||
notifyListeners();
|
||||
_saveCart();
|
||||
}
|
||||
|
||||
void clearCart() {
|
||||
_items.clear();
|
||||
notifyListeners();
|
||||
_saveCart();
|
||||
}
|
||||
}
|
||||
63
lib/providers/user_provider.dart
Normal file
63
lib/providers/user_provider.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/user.dart';
|
||||
import '../services/auth_service.dart';
|
||||
|
||||
class UserProvider extends ChangeNotifier {
|
||||
final AuthService _authService = AuthService();
|
||||
User? _user;
|
||||
bool _isLoading = false;
|
||||
|
||||
User? get user => _user;
|
||||
bool get isLoggedIn => _user != null;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
UserProvider() {
|
||||
_loadSavedUser();
|
||||
}
|
||||
|
||||
Future<void> _loadSavedUser() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_user = await _authService.getSavedUser();
|
||||
} catch (e) {
|
||||
print('Fehler beim Laden des Benutzers: $e');
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> login(String username, String password) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final user = await _authService.login(username, password);
|
||||
if (user != null) {
|
||||
_user = user;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Login-Fehler: $e');
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await _authService.logout();
|
||||
_user = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> refreshUser() async {
|
||||
await _loadSavedUser();
|
||||
}
|
||||
}
|
||||
|
||||
372
lib/screens/cart_screen.dart
Normal file
372
lib/screens/cart_screen.dart
Normal 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();
|
||||
}
|
||||
|
||||
168
lib/screens/categories_screen.dart
Normal file
168
lib/screens/categories_screen.dart
Normal 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],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
241
lib/screens/checkout_screen.dart
Normal file
241
lib/screens/checkout_screen.dart
Normal 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...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
427
lib/screens/home_screen.dart
Normal file
427
lib/screens/home_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
253
lib/screens/login_screen.dart
Normal file
253
lib/screens/login_screen.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
162
lib/screens/main_navigation_screen.dart
Normal file
162
lib/screens/main_navigation_screen.dart
Normal 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',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
293
lib/screens/order_detail_screen.dart
Normal file
293
lib/screens/order_detail_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
270
lib/screens/orders_screen.dart
Normal file
270
lib/screens/orders_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
172
lib/screens/password_reset_screen.dart
Normal file
172
lib/screens/password_reset_screen.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
259
lib/screens/product_detail_screen.dart
Normal file
259
lib/screens/product_detail_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
200
lib/screens/products_screen.dart
Normal file
200
lib/screens/products_screen.dart
Normal 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],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
446
lib/screens/profile_screen.dart
Normal file
446
lib/screens/profile_screen.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
353
lib/screens/register_screen.dart
Normal file
353
lib/screens/register_screen.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
201
lib/screens/search_screen.dart
Normal file
201
lib/screens/search_screen.dart
Normal 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],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
132
lib/services/analytics_service.dart
Normal file
132
lib/services/analytics_service.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// Analytics Service für Umami Integration
|
||||
///
|
||||
/// Umami ist eine datenschutzfreundliche Analytics-Lösung.
|
||||
/// Konfiguriere deine Umami-URL und Website-ID in den statischen Variablen.
|
||||
class AnalyticsService {
|
||||
// Ersetze mit deiner Umami-Instanz URL
|
||||
static const String umamiUrl = 'https://analytics.hyggecraftery.com';
|
||||
|
||||
// Ersetze mit deiner Website-ID von Umami
|
||||
static const String websiteId = 'c43cd023-ff64-43c8-a42a-ac23d32366f6';
|
||||
|
||||
// Optionale API-Key für Authentifizierung
|
||||
static const String? apiKey = null;
|
||||
|
||||
/// Sendet ein Event an Umami
|
||||
static Future<void> trackEvent({
|
||||
required String eventName,
|
||||
Map<String, dynamic>? eventData,
|
||||
String? url,
|
||||
String? referrer,
|
||||
}) async {
|
||||
try {
|
||||
final uri = Uri.parse('$umamiUrl/api/send');
|
||||
|
||||
final payload = {
|
||||
'website': websiteId,
|
||||
'hostname': 'hyggecraftery.com',
|
||||
'url': url ?? '/',
|
||||
'referrer': referrer,
|
||||
'name': eventName,
|
||||
'data': eventData ?? {},
|
||||
};
|
||||
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (apiKey != null) {
|
||||
headers['Authorization'] = 'Bearer $apiKey';
|
||||
}
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: headers,
|
||||
body: json.encode(payload),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
print('Umami Tracking Fehler: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
// Fail silently - Analytics sollten die App nicht blockieren
|
||||
print('Analytics Fehler: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Trackt einen Page View
|
||||
static Future<void> trackPageView(String page) async {
|
||||
await trackEvent(
|
||||
eventName: 'pageview',
|
||||
url: page,
|
||||
);
|
||||
}
|
||||
|
||||
/// Trackt eine Produktansicht
|
||||
static Future<void> trackProductView(int productId, String productName) async {
|
||||
await trackEvent(
|
||||
eventName: 'product_view',
|
||||
eventData: {
|
||||
'product_id': productId,
|
||||
'product_name': productName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Trackt einen "Zum Warenkorb hinzufügen" Event
|
||||
static Future<void> trackAddToCart(int productId, String productName, double price) async {
|
||||
await trackEvent(
|
||||
eventName: 'add_to_cart',
|
||||
eventData: {
|
||||
'product_id': productId,
|
||||
'product_name': productName,
|
||||
'price': price,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Trackt einen Checkout-Start
|
||||
static Future<void> trackCheckoutStart(double total) async {
|
||||
await trackEvent(
|
||||
eventName: 'checkout_start',
|
||||
eventData: {
|
||||
'total': total,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Trackt eine erfolgreiche Bestellung
|
||||
static Future<void> trackPurchase(int orderId, double total) async {
|
||||
await trackEvent(
|
||||
eventName: 'purchase',
|
||||
eventData: {
|
||||
'order_id': orderId,
|
||||
'total': total,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Trackt eine Suche
|
||||
static Future<void> trackSearch(String searchQuery) async {
|
||||
await trackEvent(
|
||||
eventName: 'search',
|
||||
eventData: {
|
||||
'query': searchQuery,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Trackt eine Kategorie-Ansicht
|
||||
static Future<void> trackCategoryView(String categoryName) async {
|
||||
await trackEvent(
|
||||
eventName: 'category_view',
|
||||
eventData: {
|
||||
'category': categoryName,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
285
lib/services/auth_service.dart
Normal file
285
lib/services/auth_service.dart
Normal file
@@ -0,0 +1,285 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/user.dart';
|
||||
import 'woocommerce_service.dart';
|
||||
|
||||
class AuthService {
|
||||
static const String _userKey = 'user_data';
|
||||
static const String _tokenKey = 'auth_token';
|
||||
|
||||
/// Meldet einen Benutzer an
|
||||
///
|
||||
/// Verwendet die WordPress REST API für die Authentifizierung
|
||||
Future<User?> login(String username, String password) async {
|
||||
try {
|
||||
final uri = Uri.parse('${WooCommerceService.baseUrl}/wp-json/jwt-auth/v1/token');
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'username': username,
|
||||
'password': password,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
|
||||
if (data['token'] != null) {
|
||||
// Speichere Token
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_tokenKey, data['token']);
|
||||
|
||||
// Hole Benutzerdaten
|
||||
final user = await getUserData(data['token']);
|
||||
|
||||
if (user != null) {
|
||||
await saveUser(user);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
} else if (response.statusCode == 403) {
|
||||
// JWT Auth Plugin nicht installiert, verwende alternative Methode
|
||||
return await _loginAlternative(username, password);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Login-Fehler: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Alternative Login-Methode über WooCommerce REST API
|
||||
Future<User?> _loginAlternative(String username, String password) async {
|
||||
try {
|
||||
// Verwende Basic Auth mit WooCommerce API
|
||||
final credentials = base64Encode(utf8.encode('$username:$password'));
|
||||
final uri = Uri.parse('${WooCommerceService.baseUrl}/wp-json/wc/v3/customers');
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'Authorization': 'Basic $credentials',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Suche nach dem Benutzer mit dieser E-Mail/Username
|
||||
final customers = json.decode(response.body) as List;
|
||||
final customer = customers.firstWhere(
|
||||
(c) => c['email'] == username || c['username'] == username,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (customer != null) {
|
||||
final user = User.fromJson(customer);
|
||||
await saveUser(user);
|
||||
// Speichere Credentials für spätere API-Calls
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('username', username);
|
||||
await prefs.setString('password', password); // In Produktion verschlüsseln!
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Alternative Login-Fehler: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Holt Benutzerdaten mit Token
|
||||
Future<User?> getUserData(String token) async {
|
||||
try {
|
||||
final uri = Uri.parse('${WooCommerceService.baseUrl}/wp-json/wp/v2/users/me');
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
return User.fromJson(data);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Fehler beim Abrufen der Benutzerdaten: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Speichert Benutzerdaten lokal
|
||||
Future<void> saveUser(User user) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_userKey, json.encode(user.toJson()));
|
||||
}
|
||||
|
||||
/// Lädt gespeicherten Benutzer
|
||||
Future<User?> getSavedUser() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userJson = prefs.getString(_userKey);
|
||||
|
||||
if (userJson != null) {
|
||||
final userData = json.decode(userJson);
|
||||
return User.fromJson(userData);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Fehler beim Laden des gespeicherten Benutzers: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Prüft ob Benutzer eingeloggt ist
|
||||
Future<bool> isLoggedIn() async {
|
||||
final user = await getSavedUser();
|
||||
return user != null;
|
||||
}
|
||||
|
||||
/// Meldet den Benutzer ab
|
||||
Future<void> logout() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_userKey);
|
||||
await prefs.remove(_tokenKey);
|
||||
await prefs.remove('username');
|
||||
await prefs.remove('password');
|
||||
}
|
||||
|
||||
/// Holt das gespeicherte Token
|
||||
Future<String?> getToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_tokenKey);
|
||||
}
|
||||
|
||||
/// Registriert einen neuen Benutzer
|
||||
Future<Map<String, dynamic>> register({
|
||||
required String email,
|
||||
required String username,
|
||||
required String password,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
}) async {
|
||||
try {
|
||||
// Versuche WordPress REST API
|
||||
final uri = Uri.parse('${WooCommerceService.baseUrl}/wp-json/wp/v2/users');
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'first_name': firstName ?? '',
|
||||
'last_name': lastName ?? '',
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
final data = json.decode(response.body);
|
||||
return {'success': true, 'user_id': data['id']};
|
||||
} else {
|
||||
final error = json.decode(response.body);
|
||||
return {
|
||||
'success': false,
|
||||
'message': error['message'] ?? 'Registrierung fehlgeschlagen',
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
'success': false,
|
||||
'message': 'Fehler bei der Registrierung: $e',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Sendet Passwort-Reset-E-Mail
|
||||
Future<Map<String, dynamic>> requestPasswordReset(String email) async {
|
||||
try {
|
||||
final uri = Uri.parse('${WooCommerceService.baseUrl}/wp-json/bdpwr/v1/reset-password');
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({'email': email}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return {'success': true, 'message': 'Reset-Link wurde per E-Mail gesendet'};
|
||||
} else {
|
||||
// Fallback: WordPress Standard
|
||||
final uri2 = Uri.parse('${WooCommerceService.baseUrl}/wp-login.php?action=lostpassword');
|
||||
final response2 = await http.post(
|
||||
uri2,
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'user_login=$email',
|
||||
);
|
||||
|
||||
return {
|
||||
'success': response2.statusCode == 200,
|
||||
'message': 'Bitte prüfe deine E-Mails für den Reset-Link',
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
'success': false,
|
||||
'message': 'Fehler beim Anfordern des Passwort-Resets: $e',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Aktualisiert Benutzerprofil
|
||||
Future<User?> updateProfile({
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
String? email,
|
||||
String? displayName,
|
||||
}) async {
|
||||
try {
|
||||
final token = await getToken();
|
||||
if (token == null) return null;
|
||||
|
||||
final uri = Uri.parse('${WooCommerceService.baseUrl}/wp-json/wp/v2/users/me');
|
||||
|
||||
final body = <String, dynamic>{
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
};
|
||||
|
||||
if (email != null) body['email'] = email;
|
||||
if (displayName != null) body['name'] = displayName;
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: json.encode(body),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final user = User.fromJson(data);
|
||||
await saveUser(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Fehler beim Aktualisieren des Profils: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
82
lib/services/coupon_service.dart
Normal file
82
lib/services/coupon_service.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/coupon.dart';
|
||||
import 'woocommerce_service.dart';
|
||||
|
||||
class CouponService {
|
||||
/// Validiert einen Gutschein-Code
|
||||
Future<Coupon> validateCoupon(String code) async {
|
||||
try {
|
||||
final queryParams = {
|
||||
'code': code,
|
||||
'status': 'publish',
|
||||
'consumer_key': WooCommerceService.consumerKey,
|
||||
'consumer_secret': WooCommerceService.consumerSecret,
|
||||
};
|
||||
|
||||
final uri = Uri.parse('${WooCommerceService.baseUrl}/wp-json/wc/v3/coupons').replace(
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
|
||||
if (data.isNotEmpty) {
|
||||
final couponData = data[0];
|
||||
return Coupon(
|
||||
code: couponData['code'] ?? code,
|
||||
description: couponData['description'],
|
||||
discountType: couponData['discount_type'] ?? 'fixed_cart',
|
||||
amount: couponData['amount'] ?? '0',
|
||||
isValid: true,
|
||||
);
|
||||
} else {
|
||||
return Coupon(
|
||||
code: code,
|
||||
discountType: 'fixed_cart',
|
||||
amount: '0',
|
||||
isValid: false,
|
||||
errorMessage: 'Gutschein-Code nicht gefunden',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return Coupon(
|
||||
code: code,
|
||||
discountType: 'fixed_cart',
|
||||
amount: '0',
|
||||
isValid: false,
|
||||
errorMessage: 'Fehler beim Validieren des Gutscheins',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return Coupon(
|
||||
code: code,
|
||||
discountType: 'fixed_cart',
|
||||
amount: '0',
|
||||
isValid: false,
|
||||
errorMessage: 'Fehler: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Berechnet den Rabatt für einen Warenkorb
|
||||
double calculateDiscount(Coupon coupon, double cartTotal) {
|
||||
if (!coupon.isValid) return 0.0;
|
||||
|
||||
final amount = double.tryParse(coupon.amount) ?? 0.0;
|
||||
|
||||
switch (coupon.discountType) {
|
||||
case 'percent':
|
||||
case 'percent_product':
|
||||
return (cartTotal * amount) / 100;
|
||||
case 'fixed_cart':
|
||||
case 'fixed_product':
|
||||
return amount;
|
||||
default:
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
154
lib/services/order_service.dart
Normal file
154
lib/services/order_service.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/order.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import 'woocommerce_service.dart';
|
||||
|
||||
class OrderService {
|
||||
final AuthService _authService = AuthService();
|
||||
|
||||
/// Holt alle Bestellungen des eingeloggten Benutzers
|
||||
Future<List<Order>> getOrders({int perPage = 20, int page = 1}) async {
|
||||
try {
|
||||
// Versuche zuerst mit JWT Token
|
||||
final token = await _authService.getToken();
|
||||
|
||||
if (token != null) {
|
||||
return await _getOrdersWithToken(token, perPage, page);
|
||||
}
|
||||
|
||||
// Fallback: Verwende WooCommerce Customer API
|
||||
return await _getOrdersWithCredentials(perPage, page);
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Abrufen der Bestellungen: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Order>> _getOrdersWithToken(String token, int perPage, int page) async {
|
||||
try {
|
||||
final uri = Uri.parse('${WooCommerceService.baseUrl}/wp-json/wc/v3/orders').replace(
|
||||
queryParameters: {
|
||||
'per_page': perPage.toString(),
|
||||
'page': page.toString(),
|
||||
'customer': 'me', // Holt Bestellungen des aktuellen Benutzers
|
||||
},
|
||||
);
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
return data.map((json) => Order.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Fehler: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback zur Credentials-Methode
|
||||
return await _getOrdersWithCredentials(perPage, page);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Order>> _getOrdersWithCredentials(int perPage, int page) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final username = prefs.getString('username');
|
||||
final password = prefs.getString('password');
|
||||
|
||||
if (username == null || password == null) {
|
||||
throw Exception('Nicht eingeloggt');
|
||||
}
|
||||
|
||||
// Hole Customer ID
|
||||
final customerId = await _getCustomerId(username, password);
|
||||
if (customerId == null) {
|
||||
throw Exception('Kunde nicht gefunden');
|
||||
}
|
||||
|
||||
final credentials = base64Encode(utf8.encode('$username:$password'));
|
||||
final uri = Uri.parse('${WooCommerceService.baseUrl}/wp-json/wc/v3/orders').replace(
|
||||
queryParameters: {
|
||||
'per_page': perPage.toString(),
|
||||
'page': page.toString(),
|
||||
'customer': customerId.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'Authorization': 'Basic $credentials',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
return data.map((json) => Order.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Fehler: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Abrufen der Bestellungen: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<int?> _getCustomerId(String username, String password) async {
|
||||
try {
|
||||
final credentials = base64Encode(utf8.encode('$username:$password'));
|
||||
final uri = Uri.parse('${WooCommerceService.baseUrl}/wp-json/wc/v3/customers');
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'Authorization': 'Basic $credentials',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final customers = json.decode(response.body) as List;
|
||||
final customer = customers.firstWhere(
|
||||
(c) => c['email'] == username || c['username'] == username,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
return customer != null ? customer['id'] : null;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Fehler beim Abrufen der Customer ID: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Holt Details einer einzelnen Bestellung
|
||||
Future<Order> getOrder(int orderId) async {
|
||||
try {
|
||||
final token = await _authService.getToken();
|
||||
|
||||
if (token != null) {
|
||||
final uri = Uri.parse('${WooCommerceService.baseUrl}/wp-json/wc/v3/orders/$orderId');
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
return Order.fromJson(data);
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception('Nicht autorisiert');
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Abrufen der Bestellung: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
149
lib/services/woocommerce_checkout_service.dart
Normal file
149
lib/services/woocommerce_checkout_service.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../providers/cart_provider.dart';
|
||||
import 'woocommerce_service.dart';
|
||||
|
||||
/// Service für WooCommerce Checkout-Funktionalität
|
||||
///
|
||||
/// Dieser Service bietet Methoden für den Checkout-Prozess.
|
||||
/// Für die einfachste Lösung wird die WebView-basierte Checkout-Seite verwendet,
|
||||
/// die alle WooCommerce-Einstellungen automatisch nutzt.
|
||||
class WooCommerceCheckoutService {
|
||||
static const String baseUrl = WooCommerceService.baseUrl;
|
||||
static const String consumerKey = WooCommerceService.consumerKey;
|
||||
static const String consumerSecret = WooCommerceService.consumerSecret;
|
||||
|
||||
/// Erstellt eine Checkout-URL mit Warenkorb-Produkten
|
||||
///
|
||||
/// Diese Methode erstellt eine URL, die direkt zur WooCommerce Checkout-Seite
|
||||
/// führt und die Produkte aus dem Warenkorb automatisch hinzufügt.
|
||||
static String buildCheckoutUrl(CartProvider cart) {
|
||||
if (cart.items.isEmpty) {
|
||||
return '$baseUrl/checkout/';
|
||||
}
|
||||
|
||||
// Erstelle eine URL, die alle Produkte zum Warenkorb hinzufügt
|
||||
// Format: /checkout/?add-to-cart=ID1,ID2,ID3
|
||||
final productIds = cart.items
|
||||
.map((item) => '${item.product.id}:${item.quantity}')
|
||||
.join(',');
|
||||
|
||||
// Alternative: Verwende die WooCommerce Cart-Seite und leite dann weiter
|
||||
// Oder direkt zur Checkout-Seite mit Produkten
|
||||
return '$baseUrl/checkout/?add-to-cart=${cart.items.map((item) => item.product.id).join(',')}';
|
||||
}
|
||||
|
||||
/// Erstellt eine Order über die WooCommerce REST API
|
||||
///
|
||||
/// Diese Methode kann verwendet werden, wenn du einen nativen Checkout
|
||||
/// implementieren möchtest. Sie erfordert Write-Berechtigungen für die API Keys.
|
||||
///
|
||||
/// Hinweis: Für die einfachste Lösung wird die WebView-basierte Checkout-Seite
|
||||
/// empfohlen, da sie alle WooCommerce-Einstellungen (Zahlungen, Versand, etc.)
|
||||
/// automatisch nutzt.
|
||||
static Future<Map<String, dynamic>> createOrder({
|
||||
required CartProvider cart,
|
||||
required Map<String, dynamic> billing,
|
||||
Map<String, dynamic>? shipping,
|
||||
String? paymentMethod,
|
||||
}) async {
|
||||
try {
|
||||
final lineItems = cart.items.map((item) {
|
||||
return {
|
||||
'product_id': item.product.id,
|
||||
'quantity': item.quantity,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
final orderData = {
|
||||
'payment_method': paymentMethod ?? 'bacs',
|
||||
'payment_method_title': paymentMethod ?? 'Direktüberweisung',
|
||||
'set_paid': false,
|
||||
'billing': billing,
|
||||
'shipping': shipping ?? billing,
|
||||
'line_items': lineItems,
|
||||
'shipping_lines': [],
|
||||
};
|
||||
|
||||
final uri = Uri.parse('$baseUrl/wp-json/wc/v3/orders');
|
||||
|
||||
// Basic Auth für API
|
||||
final credentials = base64Encode(utf8.encode('$consumerKey:$consumerSecret'));
|
||||
final headers = {
|
||||
'Authorization': 'Basic $credentials',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: headers,
|
||||
body: json.encode(orderData),
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Fehler beim Erstellen der Bestellung: ${response.statusCode} - ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Checkout: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Ruft verfügbare Zahlungsmethoden ab
|
||||
static Future<List<Map<String, dynamic>>> getPaymentMethods() async {
|
||||
try {
|
||||
final uri = Uri.parse('$baseUrl/wp-json/wc/v3/payment_gateways');
|
||||
|
||||
final credentials = base64Encode(utf8.encode('$consumerKey:$consumerSecret'));
|
||||
final headers = {
|
||||
'Authorization': 'Basic $credentials',
|
||||
};
|
||||
|
||||
final response = await http.get(uri, headers: headers);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
return data
|
||||
.where((gateway) => gateway['enabled'] == true)
|
||||
.map((gateway) => {
|
||||
return {
|
||||
'id': gateway['id'],
|
||||
'title': gateway['title'],
|
||||
'description': gateway['description'],
|
||||
};
|
||||
})
|
||||
.toList()
|
||||
.cast<Map<String, dynamic>>();
|
||||
} else {
|
||||
throw Exception('Fehler beim Laden der Zahlungsmethoden: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Abrufen der Zahlungsmethoden: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Ruft Versandoptionen ab
|
||||
static Future<List<Map<String, dynamic>>> getShippingMethods() async {
|
||||
try {
|
||||
final uri = Uri.parse('$baseUrl/wp-json/wc/v3/shipping/zones');
|
||||
|
||||
final credentials = base64Encode(utf8.encode('$consumerKey:$consumerSecret'));
|
||||
final headers = {
|
||||
'Authorization': 'Basic $credentials',
|
||||
};
|
||||
|
||||
final response = await http.get(uri, headers: headers);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
return data.cast<Map<String, dynamic>>();
|
||||
} else {
|
||||
throw Exception('Fehler beim Laden der Versandoptionen: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Abrufen der Versandoptionen: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
230
lib/services/woocommerce_service.dart
Normal file
230
lib/services/woocommerce_service.dart
Normal file
@@ -0,0 +1,230 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/product.dart';
|
||||
import '../models/category.dart';
|
||||
import '../models/review.dart';
|
||||
|
||||
class WooCommerceService {
|
||||
// Ersetze diese URL mit deiner WooCommerce Shop-URL
|
||||
static const String baseUrl = 'https://hyggecraftery.com';
|
||||
static const String consumerKey = 'ck_ae583aa69ca3f962e14ae03faadd302937b8f3e2'; // Ersetze mit deinem Consumer Key
|
||||
static const String consumerSecret = 'cs_3835315e5dc3c7e87c89a733e6c3e5ec8f76b70e'; // Ersetze mit deinem Consumer Secret
|
||||
|
||||
// Öffentliche Zugriffe für andere Services
|
||||
static String get shopUrl => baseUrl;
|
||||
|
||||
// Für öffentliche Produkte (ohne Authentifizierung)
|
||||
// Du kannst auch die WooCommerce REST API ohne Keys verwenden, wenn die Produkte öffentlich sind
|
||||
static const String apiBaseUrl = '$baseUrl/wp-json/wc/v3';
|
||||
|
||||
/// Baut eine authentifizierte URL für WooCommerce API-Aufrufe
|
||||
Uri _buildAuthenticatedUrl(String endpoint, Map<String, String>? queryParams) {
|
||||
final params = <String, String>{
|
||||
'consumer_key': consumerKey,
|
||||
'consumer_secret': consumerSecret,
|
||||
if (queryParams != null) ...queryParams,
|
||||
};
|
||||
|
||||
return Uri.parse('$apiBaseUrl/$endpoint').replace(
|
||||
queryParameters: params,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Product>> getProducts({
|
||||
int perPage = 20,
|
||||
int page = 1,
|
||||
String? category,
|
||||
String? search,
|
||||
String? orderBy,
|
||||
String? order,
|
||||
double? minPrice,
|
||||
double? maxPrice,
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = {
|
||||
'per_page': perPage.toString(),
|
||||
'page': page.toString(),
|
||||
'status': 'publish',
|
||||
};
|
||||
|
||||
if (category != null) {
|
||||
queryParams['category'] = category;
|
||||
}
|
||||
|
||||
if (search != null && search.isNotEmpty) {
|
||||
queryParams['search'] = search;
|
||||
}
|
||||
|
||||
if (orderBy != null) {
|
||||
queryParams['orderby'] = orderBy;
|
||||
}
|
||||
|
||||
if (order != null) {
|
||||
queryParams['order'] = order;
|
||||
}
|
||||
|
||||
if (minPrice != null) {
|
||||
queryParams['min_price'] = minPrice.toString();
|
||||
}
|
||||
|
||||
if (maxPrice != null) {
|
||||
queryParams['max_price'] = maxPrice.toString();
|
||||
}
|
||||
|
||||
final uri = _buildAuthenticatedUrl('products', queryParams);
|
||||
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
return data.map((json) => Product.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Fehler beim Laden der Produkte: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Abrufen der Produkte: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Product> getProduct(int productId) async {
|
||||
try {
|
||||
final uri = _buildAuthenticatedUrl('products/$productId', null);
|
||||
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
return Product.fromJson(data);
|
||||
} else {
|
||||
throw Exception('Fehler beim Laden des Produkts: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Abrufen des Produkts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Product>> getFeaturedProducts({int limit = 6}) async {
|
||||
try {
|
||||
final uri = _buildAuthenticatedUrl('products', {
|
||||
'featured': 'true',
|
||||
'per_page': limit.toString(),
|
||||
'status': 'publish',
|
||||
});
|
||||
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
return data.map((json) => Product.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Fehler beim Laden der Featured Produkte: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Abrufen der Featured Produkte: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Holt alle Kategorien
|
||||
Future<List<Category>> getCategories({int? parent}) async {
|
||||
try {
|
||||
final queryParams = <String, String>{
|
||||
'per_page': '100',
|
||||
'hide_empty': 'false',
|
||||
};
|
||||
|
||||
if (parent != null) {
|
||||
queryParams['parent'] = parent.toString();
|
||||
}
|
||||
|
||||
final uri = _buildAuthenticatedUrl('products/categories', queryParams);
|
||||
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
return data.map((json) => Category.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Fehler beim Laden der Kategorien: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Abrufen der Kategorien: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Holt Bewertungen für ein Produkt
|
||||
Future<List<Review>> getProductReviews(int productId, {int perPage = 10, int page = 1}) async {
|
||||
try {
|
||||
final uri = _buildAuthenticatedUrl('products/reviews', {
|
||||
'product': productId.toString(),
|
||||
'per_page': perPage.toString(),
|
||||
'page': page.toString(),
|
||||
'status': 'approved',
|
||||
});
|
||||
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
return data.map((json) => Review.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Fehler beim Laden der Bewertungen: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Abrufen der Bewertungen: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Holt Produktbewertungs-Statistiken
|
||||
Future<ProductRating> getProductRating(int productId) async {
|
||||
try {
|
||||
final product = await getProduct(productId);
|
||||
|
||||
// WooCommerce speichert Rating-Daten im Produkt
|
||||
final averageRating = double.tryParse(product.rating?.toString() ?? '0') ?? 0.0;
|
||||
final ratingCount = product.ratingCount ?? 0;
|
||||
|
||||
return ProductRating(
|
||||
averageRating: averageRating,
|
||||
ratingCount: ratingCount,
|
||||
ratingBreakdown: {}, // Kann aus Reviews berechnet werden
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Abrufen der Bewertungsstatistik: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Erstellt eine neue Bewertung
|
||||
Future<Review?> createReview({
|
||||
required int productId,
|
||||
required String reviewer,
|
||||
required String reviewerEmail,
|
||||
required String review,
|
||||
required int rating,
|
||||
}) async {
|
||||
try {
|
||||
final uri = _buildAuthenticatedUrl('products/reviews', null);
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'product_id': productId,
|
||||
'reviewer': reviewer,
|
||||
'reviewer_email': reviewerEmail,
|
||||
'review': review,
|
||||
'rating': rating,
|
||||
'status': 'pending', // Wird vom Admin genehmigt
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
final data = json.decode(response.body);
|
||||
return Review.fromJson(data);
|
||||
} else {
|
||||
throw Exception('Fehler beim Erstellen der Bewertung: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Fehler beim Erstellen der Bewertung: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
81
lib/utils/error_handler.dart
Normal file
81
lib/utils/error_handler.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'dart:io' if (dart.library.html) 'dart:html' as io;
|
||||
|
||||
class ErrorHandler {
|
||||
/// Zeigt eine benutzerfreundliche Fehlermeldung
|
||||
static void showError(BuildContext context, dynamic error) {
|
||||
String message = 'Ein Fehler ist aufgetreten';
|
||||
final errorString = error.toString().toLowerCase();
|
||||
|
||||
// Web-kompatible Fehlerbehandlung (ohne direkte Typ-Prüfungen für dart:io)
|
||||
if (error is String) {
|
||||
message = error;
|
||||
} else if (errorString.contains('failed host lookup') ||
|
||||
errorString.contains('socketexception') ||
|
||||
errorString.contains('network') ||
|
||||
errorString.contains('connection')) {
|
||||
message = 'Keine Internetverbindung. Bitte prüfe deine Verbindung.';
|
||||
} else if (errorString.contains('timeout') ||
|
||||
errorString.contains('timed out')) {
|
||||
message = 'Zeitüberschreitung. Bitte versuche es erneut.';
|
||||
} else if (error is FormatException) {
|
||||
message = 'Datenfehler. Bitte versuche es erneut.';
|
||||
} else if (errorString.contains('404') || errorString.contains('not found')) {
|
||||
message = 'Nicht gefunden.';
|
||||
} else if (errorString.contains('401') ||
|
||||
errorString.contains('403') ||
|
||||
errorString.contains('unauthorized') ||
|
||||
errorString.contains('forbidden')) {
|
||||
message = 'Nicht autorisiert. Bitte melde dich an.';
|
||||
} else if (errorString.contains('500') ||
|
||||
errorString.contains('server error') ||
|
||||
errorString.contains('internal server')) {
|
||||
message = 'Server-Fehler. Bitte versuche es später erneut.';
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
action: SnackBarAction(
|
||||
label: 'OK',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Prüft die Internetverbindung
|
||||
static Future<bool> checkConnectivity() async {
|
||||
try {
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
return connectivityResult != ConnectivityResult.none;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Zeigt einen Retry-Dialog
|
||||
static Future<bool> showRetryDialog(BuildContext context, String message) async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Fehler'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Erneut versuchen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
34
lib/widgets/loading_widget.dart
Normal file
34
lib/widgets/loading_widget.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LoadingWidget extends StatelessWidget {
|
||||
final String? message;
|
||||
|
||||
const LoadingWidget({
|
||||
super.key,
|
||||
this.message,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF8B6F47)),
|
||||
),
|
||||
if (message != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
137
lib/widgets/product_card.dart
Normal file
137
lib/widgets/product_card.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../models/product.dart';
|
||||
|
||||
class ProductCard extends StatelessWidget {
|
||||
final Product product;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const ProductCard({
|
||||
super.key,
|
||||
required this.product,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Produktbild
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (product.imageUrl != null)
|
||||
CachedNetworkImage(
|
||||
imageUrl: product.imageUrl!,
|
||||
width: double.infinity,
|
||||
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.image_not_supported,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
color: Colors.grey[300],
|
||||
child: const Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
|
||||
// Sale Badge
|
||||
if (product.isOnSale)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Produktinfo
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Produktname
|
||||
Text(
|
||||
product.name,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
// Preis
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (product.isOnSale) ...[
|
||||
Text(
|
||||
'${product.regularPrice} €',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
decoration: TextDecoration.lineThrough,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
'${product.price} €',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: const Color(0xFF8B6F47),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
52
lib/widgets/retry_widget.dart
Normal file
52
lib/widgets/retry_widget.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RetryWidget extends StatelessWidget {
|
||||
final String message;
|
||||
final VoidCallback onRetry;
|
||||
final IconData? icon;
|
||||
|
||||
const RetryWidget({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.onRetry,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon ?? Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Erneut versuchen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF8B6F47),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
219
lib/widgets/reviews_widget.dart
Normal file
219
lib/widgets/reviews_widget.dart
Normal file
@@ -0,0 +1,219 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../models/review.dart';
|
||||
import '../services/woocommerce_service.dart';
|
||||
import '../widgets/loading_widget.dart';
|
||||
import '../utils/error_handler.dart';
|
||||
|
||||
class ReviewsWidget extends StatefulWidget {
|
||||
final int productId;
|
||||
|
||||
const ReviewsWidget({
|
||||
super.key,
|
||||
required this.productId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReviewsWidget> createState() => _ReviewsWidgetState();
|
||||
}
|
||||
|
||||
class _ReviewsWidgetState extends State<ReviewsWidget> {
|
||||
final WooCommerceService _wooCommerceService = WooCommerceService();
|
||||
List<Review> _reviews = [];
|
||||
ProductRating? _rating;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadReviews();
|
||||
}
|
||||
|
||||
Future<void> _loadReviews() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final reviews = await _wooCommerceService.getProductReviews(widget.productId);
|
||||
final rating = await _wooCommerceService.getProductRating(widget.productId);
|
||||
|
||||
setState(() {
|
||||
_reviews = reviews;
|
||||
_rating = rating;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const LoadingWidget();
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Fehler beim Laden der Bewertungen'),
|
||||
ElevatedButton(
|
||||
onPressed: _loadReviews,
|
||||
child: const Text('Erneut versuchen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_rating != null) _buildRatingSummary(_rating!),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Bewertungen (${_reviews.length})',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_reviews.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text('Noch keine Bewertungen'),
|
||||
)
|
||||
else
|
||||
..._reviews.map((review) => _buildReviewCard(review)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRatingSummary(ProductRating rating) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
rating.averageRating.toStringAsFixed(1),
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF8B6F47),
|
||||
),
|
||||
),
|
||||
_buildStars(rating.averageRating),
|
||||
Text(
|
||||
'${rating.ratingCount} Bewertungen',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStars(double rating) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(5, (index) {
|
||||
return Icon(
|
||||
index < rating.floor()
|
||||
? Icons.star
|
||||
: index < rating
|
||||
? Icons.star_half
|
||||
: Icons.star_border,
|
||||
color: Colors.amber,
|
||||
size: 20,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReviewCard(Review review) {
|
||||
final dateFormat = DateFormat('dd.MM.yyyy');
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (review.reviewerAvatarUrl != null)
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundImage: CachedNetworkImageProvider(review.reviewerAvatarUrl!),
|
||||
)
|
||||
else
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
child: Text(review.reviewer[0].toUpperCase()),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
review.reviewer,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
dateFormat.format(review.dateCreated),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildStars(review.rating.toDouble()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
review.review,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
if (review.verified)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.verified, size: 16, color: Colors.green),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Verifizierter Kauf',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user