lib/favorites_page.dartDart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'constants.dart';
import 'vendor_card.dart';
import 'VendorDetailPage.dart';
import 'core/api/api_endpoints.dart';
import 'core/api/auth_headers.dart';
import 'core/prefs/pref_keys.dart';
Für Auto-Refresh beim Zurückkommen (RouteAware)
import 'main.dart' show routeObserver;
class FavoritesPage extends StatefulWidget {
const FavoritesPage({super.key});
@override
State<FavoritesPage> createState() => _FavoritesPageState();
}
class _FavoritesPageState extends State<FavoritesPage> with RouteAware {
List<dynamic> _favoriteVendors = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadFavorites();
}
RouteAware: Wenn man von einer anderen Seite zurückkommt -> neu laden
@override
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of(context);
if (route is PageRoute) {
routeObserver.subscribe(this, route);
}
}
@override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
@override
void didPopNext() {
_loadFavorites();
}
Future<void> _loadFavorites() async {
if (!mounted) return;
setState(() => _isLoading = true);
debugPrint("--- [FAVORITES] Starte Ladevorgang via Custom Endpoint ---");
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString(PrefKeys.jwtToken);
Wenn nicht eingeloggt: lokale IDs löschen + leere Liste
if (token == null || token.isEmpty) {
await prefs.remove(PrefKeys.favoriteVendorIds);
if (mounted) {
setState(() {
_favoriteVendors = [];
_isLoading = false;
});
}
return;
}
1) Favoriten-IDs serverseitig holen
List<String> favIds = [];
try {
final favRes = await http.get(
Uri.parse(ApiEndpoints.myFavorites),
headers: AuthHeaders.bearer(token),
).timeout(const Duration(seconds: 8));
if (favRes.statusCode == 200) {
final decoded = json.decode(favRes.body);
if (decoded is List) {
favIds = decoded
.map((e) => e.toString())
.where((s) => s.isNotEmpty && s != "0" && s != "null")
.toList();
// optional Cache (aber Quelle ist Server)
await prefs.setStringList(PrefKeys.favoriteVendorIds, favIds);
}
} else {
debugPrint("[FAVORITES] my-favorites API Fehler: ${favRes.statusCode}");
}
} catch (e) {
debugPrint("[FAVORITES] my-favorites Exception: $e");
}
debugPrint("[FAVORITES] IDs (Server): $favIds");
if (favIds.isEmpty) {
if (mounted) {
setState(() {
_favoriteVendors = [];
_isLoading = false;
});
}
return;
}
2) Details zu diesen IDs holen
try {
final String idsParam = favIds.join(',');
final response = await http.get(
Uri.parse(ApiEndpoints.favoritesDetails(idsParam)),
).timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
final List<dynamic> loadedVendors = json.decode(response.body);
debugPrint("[FAVORITES] Erfolg! ${loadedVendors.length} Stores geladen.");
if (mounted) {
setState(() {
_favoriteVendors = loadedVendors;
_isLoading = false;
});
}
} else {
debugPrint("[FAVORITES] favorites-details API Fehler: ${response.statusCode}");
if (mounted) setState(() => _isLoading = false);
}
} catch (e) {
debugPrint("[FAVORITES] favorites-details Exception: $e");
if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _removeFavorite(int vendorId) async {
// UI sofort entfernen
setState(() {
_favoriteVendors.removeWhere((v) {
final map = _asMap(v);
final id = map['vendor_id'] ?? map['id'];
return id.toString() == vendorId.toString();
});
});
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString(PrefKeys.jwtToken);
// Wenn kein Login: Cache bereinigen und fertig
if (token == null || token.isEmpty) {
List<String> favIds = prefs.getStringList(PrefKeys.favoriteVendorIds) ?? [];
favIds.remove(vendorId.toString());
await prefs.setStringList(PrefKeys.favoriteVendorIds, favIds);
return;
}
Serverseitig togglen (entfernen)
try {
final res = await http.post(
Uri.parse(ApiEndpoints.toggleFavorite(vendorId.toString())),
headers: AuthHeaders.bearer(token),
).timeout(const Duration(seconds: 8));
if (res.statusCode == 200) {
final decoded = json.decode(res.body);
if (decoded is Map && decoded['favorites'] is List) {
final favs = (decoded['favorites'] as List)
.map((e) => e.toString())
.where((s) => s.isNotEmpty && s != "0" && s != "null")
.toList();
await prefs.setStringList(PrefKeys.favoriteVendorIds, favs);
}
return;
} else {
debugPrint("Fehler beim serverseitigen Unfollow: ${res.statusCode}");
}
} catch (e) {
debugPrint("Fehler beim serverseitigen Unfollow: $e");
}
// Falls was schief ging: sauber neu laden, damit UI wieder stimmt
_loadFavorites();
}
Map<String, dynamic> _asMap(dynamic v) {
if (v is Map) return Map<String, dynamic>.from(v);
if (v is List && v.isNotEmpty && v.first is Map) {
return Map<String, dynamic>.from(v.first);
}
return {};
}
Fix: Beim Tap erst favorites-details für genau diesen Vendor holen (liefert bei uns Location-Felder),
/// danach optional WCFM Single-Endpoint als Zusatz, dann zur Detailseite.
Future<void> _openVendorDetail(dynamic vendor) async {
final vendorData = _asMap(vendor);
// vendor_id sicherstellen
final rawId = vendorData['vendor_id'] ?? vendorData['id'] ?? 0;
final int vendorId = int.tryParse(rawId.toString()) ?? 0;
vendorData['vendor_id'] = vendorId;
if (vendorId <= 0) {
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(builder: (context) => VendorDetailPage(vendor: vendorData)),
);
return;
}
1) favorites-details
try {
final url = ApiEndpoints.favoritesDetails(vendorId);
final res = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 8));
if (res.statusCode == 200) {
final decoded = json.decode(res.body);
if (decoded is List && decoded.isNotEmpty && decoded.first is Map) {
vendorData.addAll(Map<String, dynamic>.from(decoded.first));
}
}
} catch (_) {}
2) optional: WCFM Single Vendor
try {
final url = ApiEndpoints.storeVendor(vendorId);
final res = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 8));
if (res.statusCode == 200) {
final decoded = json.decode(res.body);
if (decoded is Map) {
vendorData.addAll(Map<String, dynamic>.from(decoded));
}
}
} catch (_) {}
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => VendorDetailPage(vendor: vendorData),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppConstants.baseWhite,
appBar: AppBar(
title: const Text(
"Meine Favoriten",
style: TextStyle(color: AppConstants.fontGrey, fontWeight: FontWeight.bold),
),
backgroundColor: AppConstants.baseWhite,
elevation: 0,
iconTheme: const IconThemeData(color: AppConstants.fontGrey),
),
body: _isLoading
? const Center(child: CircularProgressIndicator(color: AppConstants.mainColor))
: _favoriteVendors.isEmpty
? _buildEmptyState()
: RefreshIndicator(
onRefresh: _loadFavorites,
color: AppConstants.mainColor,
child: ListView.builder(
padding: const EdgeInsets.only(top: 10, bottom: 20),
itemCount: _favoriteVendors.length,
itemBuilder: (context, index) {
final vendor = _favoriteVendors[index];
final map = _asMap(vendor);
final rawId = map['vendor_id'] ?? map['id'] ?? 0;
final int vId = int.tryParse(rawId.toString()) ?? 0;
return VendorCard(
immer Map übergeben
onTap: () => _openVendorDetail(map),
onUnfollow: () => _removeFavorite(vId),
);
},
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite_border, size: 80, color: Colors.grey[300]),
const SizedBox(height: 20),
const Text(
"Noch keine Favoriten",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppConstants.fontGrey),
),
const SizedBox(height: 10),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: Text(
"Markiere deine Lieblingsläden mit einem Herz, um sie hier schnell wiederzufinden.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
),
],
),
);
}
}