lib/vendor_card.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 'VendorDetailPage.dart';
import 'core/api/api_endpoints.dart';
import 'core/prefs/pref_keys.dart';
class VendorCard extends StatefulWidget {
final dynamic vendor;
final VoidCallback? onTap;
final VoidCallback? onUnfollow;
const VendorCard({
super.key,
required this.vendor,
this.onTap,
this.onUnfollow,
});
@override
State<VendorCard> createState() => _VendorCardState();
}
class _VendorCardState extends State<VendorCard> {
Map<String, dynamic> _extendedData = {};
int? _activeProductCount;
@override
void initState() {
super.initState();
_extendedData = _getSafeMap(widget.vendor);
_fetchVendorDetailsForBanner();
NEU: Location-Felder nachziehen
_fetchActiveProductCount();
}
Map<String, dynamic> _getSafeMap(dynamic data) {
if (data is Map) return Map<String, dynamic>.from(data);
return {};
}
String _getVendorId() {
return (_extendedData['vendor_id'] ?? _extendedData['id'] ?? _extendedData['term_id'] ?? "0").toString();
}
Future<void> _fetchVendorDetailsForBanner() async {
final vendorId = _getVendorId();
if (vendorId == "0" || vendorId == "null") return;
final url = ApiEndpoints.storeVendor(vendorId);
try {
final response = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 8));
if (response.statusCode == 200 && mounted) {
final decoded = json.decode(response.body);
if (decoded is Map) {
setState(() {
FIX: sauber casten
_extendedData.addAll(Map<String, dynamic>.from(decoded as Map));
});
}
}
} catch (_) {}
}
NEU: Store-Location Endpoint (NUR Find Location) holen + mergen
Future<void> _fetchAndMergeStoreLocation() async {
final vendorId = _getVendorId();
if (vendorId == "0" || vendorId == "null") return;
final url = ApiEndpoints.vendorLocation(vendorId);
try {
final response = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 8));
if (response.statusCode == 200 && mounted) {
final decoded = json.decode(response.body);
if (decoded is Map) {
setState(() {
FIX: sauber casten
_extendedData.addAll(Map<String, dynamic>.from(decoded as Map));
});
}
}
} catch (_) {}
}
Future<void> _fetchActiveProductCount() async {
final vendorId = _getVendorId();
if (vendorId == "0" || vendorId == "null") return;
final url = ApiEndpoints.vendorProductCount(vendorId);
try {
final res = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 8));
if (res.statusCode == 200) {
final decoded = json.decode(res.body);
final count = int.tryParse(decoded['count']?.toString() ?? "");
if (count != null && mounted) {
setState(() => _activeProductCount = count);
}
} else {
debugPrint("[COUNT] vendor-product-count Fehler: ${res.statusCode} ${res.body}");
}
} catch (e) {
debugPrint("[COUNT] vendor-product-count Exception: $e");
}
}
Future<void> _syncUnfollowWithWordPress(String vendorId) async {
final prefs = await SharedPreferences.getInstance();
final String? userId = prefs.getString(PrefKeys.userId);
if (userId == null) {
debugPrint("[SYNC] Keine User-ID gefunden, WordPress-Sync nicht möglich.");
return;
}
try {
final response = await http.post(
Uri.parse(ApiEndpoints.adminAjax),
body: {
'action': 'wcfm_follow_store',
'vendor_id': vendorId,
'user_id': userId,
'wcfm_follow_status': 'unfollow',
},
);
if (response.statusCode == 200) {
debugPrint("[SYNC] Erfolgreich in WordPress entfernt für User: $userId");
}
} catch (e) {
debugPrint("[SYNC] Fehler beim WordPress-Abgleich: $e");
}
}
@override
Widget build(BuildContext context) {
final d = _extendedData;
final String storeName = d['store_name'] ?? d['vendor_name'] ?? 'Partner';
final String? logoUrl = d['vendor_list_logo'] ?? d['logo'] ?? d['vendor_shop_logo'];
String? bannerUrl = d['mobile_banner'] ?? d['vendor_banner'] ?? d['banner'];
if (bannerUrl == null && d['settings'] is Map) bannerUrl = d['settings']['banner'];
final String vendorId = (d['vendor_id'] ?? d['id'] ?? d['term_id'] ?? "0").toString();
Rating (ein Stern + Zahl)
final ratingVal = d['store_rating'] ?? d['avg_review_rating'] ?? d['vendor_rating'];
final double rating = double.tryParse(ratingVal?.toString() ?? "0") ?? 0;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
decoration: BoxDecoration(
color: AppConstants.baseWhite,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: InkWell(
onTap: widget.onTap ??
() {
Navigator.push(
context,
MaterialPageRoute(
WICHTIG: immer die gemergten Daten übergeben
builder: (context) => VendorDetailPage(vendor: d),
),
);
},
borderRadius: BorderRadius.circular(20),
child: Column(
children: [
Stack(
clipBehavior: Clip.none,
children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
child: bannerUrl != null && bannerUrl.isNotEmpty && bannerUrl.startsWith('http')
? Image.network(bannerUrl, height: 100, width: double.infinity, fit: BoxFit.cover)
: Container(
height: 100,
color: AppConstants.mainColor.withOpacity(0.1),
child: const Center(child: Icon(Icons.storefront, color: AppConstants.mainColor, size: 30)),
),
),
Logo bleibt exakt wie vorher
Positioned(
bottom: -20,
left: 15,
child: Container(
padding: const EdgeInsets.all(3),
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
child: CircleAvatar(
radius: 30,
backgroundColor: Colors.grey[100],
backgroundImage: (logoUrl != null && logoUrl.startsWith('http')) ? NetworkImage(logoUrl) : null,
child: (logoUrl == null || !logoUrl.startsWith('http'))
? const Icon(Icons.store, color: AppConstants.fontGrey)
: null,
),
),
),
Rating unten links im freien Bereich (ohne Layout-Shift)
if (rating > 0)
Positioned(
left: 18,
bottom: -46,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.92),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, color: Colors.amber, size: 14),
const SizedBox(width: 4),
Text(
rating.toStringAsFixed(1),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: AppConstants.fontGrey,
),
),
],
),
),
),
Positioned(
top: 10,
right: 10,
child: GestureDetector(
onTap: () {
if (widget.onUnfollow != null) widget.onUnfollow!();
_syncUnfollowWithWordPress(vendorId);
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
child: const Icon(Icons.favorite, color: AppConstants.mainColor, size: 20),
),
),
),
],
),
Padding bleibt exakt wie vorher
Padding(
padding: const EdgeInsets.fromLTRB(85, 10, 15, 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
storeName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppConstants.fontGrey),
),
if (_activeProductCount != null) ...[
const SizedBox(height: 4),
Text(
"${_activeProductCount!} aktive Produkte",
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
const SizedBox(height: 4),
const Text(
"Dein Lieblingsladen in Weimar",
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
],
),
),
);
}
}