Products page connected to the backend.

This commit is contained in:
bala 2025-11-29 15:04:40 +05:30
parent 595554d541
commit a978699dfc
13 changed files with 878 additions and 128 deletions

View File

@ -23,4 +23,7 @@ class ApiEndpoints {
///Brands ///Brands
static const String brands = "/v1/brands"; static const String brands = "/v1/brands";
///Products
static const product = '/api/brands/';
} }

View File

@ -5,6 +5,7 @@ import 'package:autos/presentation/screens/auth/sign_up_screen.dart';
import 'package:autos/presentation/screens/brands/brands_screen.dart'; import 'package:autos/presentation/screens/brands/brands_screen.dart';
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart'; import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
import 'package:autos/presentation/screens/ebay/ebay_screen.dart'; import 'package:autos/presentation/screens/ebay/ebay_screen.dart';
import 'package:autos/presentation/screens/products/products_screen.dart';
import 'package:autos/presentation/screens/store/create_location_screen.dart'; import 'package:autos/presentation/screens/store/create_location_screen.dart';
import 'package:autos/presentation/screens/store/store.dart'; import 'package:autos/presentation/screens/store/store.dart';
import 'package:autos/presentation/screens/turn14_screen/turn14_screen.dart'; import 'package:autos/presentation/screens/turn14_screen/turn14_screen.dart';
@ -61,6 +62,9 @@ class AppRouter {
case AppRoutePaths.brands: case AppRoutePaths.brands:
return slideRoute(BrandsScreen()); return slideRoute(BrandsScreen());
case AppRoutePaths.products:
return slideRoute(ProductsScreen());
default: default:
return _defaultFallback(settings); return _defaultFallback(settings);
} }

View File

@ -1,11 +1,15 @@
class AppRoutePaths { class AppRoutePaths {
///Auth
static const auth = '/auth'; static const auth = '/auth';
static const signup = '/signup'; static const signup = '/signup';
static const forgotPassword = '/forgotPassword'; static const forgotPassword = '/forgotPassword';
///Screens
static const dashboard = '/dashboard'; static const dashboard = '/dashboard';
static const turn14 = '/turn14'; static const turn14 = '/turn14';
static const ebay = '/ebay'; static const ebay = '/ebay';
static const store = '/store'; static const store = '/store';
static const createStoreLocation = '/createStoreLocation'; static const createStoreLocation = '/createStoreLocation';
static const brands = '/brands'; static const brands = '/brands';
static const products = '/products';
} }

View File

@ -65,7 +65,7 @@ class SideMenu extends ConsumerWidget {
// --- MANAGE --- // --- MANAGE ---
_sectionHeader("MANAGE"), _sectionHeader("MANAGE"),
_menuItem(context, "🏷️", "Brands", AppRoutePaths.brands), _menuItem(context, "🏷️", "Brands", AppRoutePaths.brands),
_menuItem(context, "📦", "Products", 'products'), _menuItem(context, "📦", "Products", AppRoutePaths.products),
_menuItem(context, "⬇️", "Imports", 'imports'), _menuItem(context, "⬇️", "Imports", 'imports'),
// --- ACCOUNT --- // --- ACCOUNT ---

View File

@ -0,0 +1,21 @@
import 'package:autos/domain/entities/product.dart';
class ProductModel extends ProductEntity {
ProductModel({
required super.id,
required super.name,
required super.image,
required super.price,
});
factory ProductModel.fromJson(Map<String, dynamic> json) {
return ProductModel(
id: json['id'],
name: json['name'],
image: json['image'],
price: (json['price'] as num).toDouble(),
);
}
}

View File

@ -20,7 +20,7 @@ class BrandsRepositoryImpl implements BrandsRepository {
BrandsRepositoryImpl(this._apiService); BrandsRepositoryImpl(this._apiService);
/// ALWAYS CALL TOKEN API (FORCE REFRESH) /// ALWAYS CALL TOKEN API (FORCE REFRESH)
Future<String> getTurn14AccessToken() async { Future<String> getTurn14AccessToken() async {
final jsonString = await _storage.read(key: _userKey); final jsonString = await _storage.read(key: _userKey);
if (jsonString == null) { if (jsonString == null) {
throw Exception("User not found. Please login again."); throw Exception("User not found. Please login again.");
@ -52,7 +52,7 @@ Future<String> getTurn14AccessToken() async {
debugPrint("Turn14 Token: $token"); debugPrint("Turn14 Token: $token");
return token; return token;
} }
/// FETCH BRANDS USING FRESH TOKEN EVERY TIME /// FETCH BRANDS USING FRESH TOKEN EVERY TIME
@override @override
@ -88,4 +88,38 @@ Future<String> getTurn14AccessToken() async {
throw Exception("Unexpected error: $e"); throw Exception("Unexpected error: $e");
} }
} }
/// BULK INSERT SELECTED BRANDS
Future<Map<String, dynamic>> saveSelectedBrands({
required String userId,
required List<BrandEntity> brands,
}) async {
try {
final payload = {
"userid": userId,
"brands": brands
.map(
(brand) => {
"id": brand.id.toString(),
"name": brand.name,
"logo": brand.logo,
"dropship": brand.dropship,
},
)
.toList(),
};
final response = await _apiService.postWithOptions(
"/api/brands/bulk-insert",
overrideBaseUrl: "https://ebay.backend.data4autos.com",
data: payload,
);
return response.data;
} on DioException catch (e) {
throw Exception("Save failed: ${e.response?.data ?? e.message}");
} catch (e) {
throw Exception("Unexpected error: $e");
}
}
} }

View File

@ -0,0 +1,65 @@
import 'package:autos/core/constants/api_endpoints.dart';
import 'package:autos/data/models/product_model.dart';
import 'package:autos/data/sources/remote/api_service.dart';
import 'package:autos/domain/entities/product.dart';
import 'package:flutter/material.dart';
abstract class ProductRepository {
Future<List<ProductEntity>> getProducts(String userId);
}
class ProductRepositoryImpl implements ProductRepository {
final ApiService apiService;
ProductRepositoryImpl(this.apiService);
@override
Future<List<ProductEntity>> getProducts(String userId) async {
try {
final url = '${ApiEndpoints.product}$userId';
/// DEBUG PRINTS
debugPrint("🌍 PRODUCT API HIT");
debugPrint("➡️ URL: $url");
final response = await apiService.get(url);
debugPrint("✅ RAW PRODUCT API RESPONSE:");
debugPrint(response.data.toString());
/// SAFETY: If backend returns null return empty list
if (response.data == null) {
debugPrint("⚠️ PRODUCT API RETURNED NULL → using empty list");
return [];
}
/// SAFETY: Ensure response is really a List
if (response.data is! List) {
debugPrint("⚠️ PRODUCT API RETURNED NON-LIST DATA");
return [];
}
final List data = response.data as List;
debugPrint("✅ PARSED PRODUCT COUNT: ${data.length}");
/// EMPTY LIST IS PERFECTLY VALID
if (data.isEmpty) {
debugPrint(" NO PRODUCTS FOUND (EMPTY LIST)");
return [];
}
/// SAFE MODEL PARSING
return data
.map((e) => ProductModel.fromJson(e))
.toList();
} catch (e, st) {
debugPrint("❌ PRODUCT API REAL ERROR:");
debugPrint(e.toString());
debugPrint(st.toString());
/// ONLY THROW FOR REAL FAILURES
throw Exception("Products fetch error: $e");
}
}
}

View File

@ -1,4 +1,3 @@
// lib/data/sources/remote/api_service.dart
import 'package:autos/core/constants/api_endpoints.dart'; import 'package:autos/core/constants/api_endpoints.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -15,7 +14,6 @@ class ApiService {
headers: ApiEndpoints.defaultHeaders, headers: ApiEndpoints.defaultHeaders,
), ),
) { ) {
// Add logging interceptor in debug mode
if (kDebugMode) { if (kDebugMode) {
_dio.interceptors.add( _dio.interceptors.add(
LogInterceptor(requestBody: true, responseBody: true), LogInterceptor(requestBody: true, responseBody: true),
@ -23,7 +21,7 @@ class ApiService {
} }
} }
/// POST request // KEEP OLD POST (so existing files DO NOT BREAK)
Future<Response> post( Future<Response> post(
String endpoint, String endpoint,
Map<String, dynamic> data, { Map<String, dynamic> data, {
@ -32,13 +30,32 @@ class ApiService {
return await _dio.post(endpoint, data: data, options: options); return await _dio.post(endpoint, data: data, options: options);
} }
/// GET request with optional overrideBaseUrl and headers // NEW POST (for overrideBaseUrl + headers + params)
Future<Response> get( Future<Response> postWithOptions(
String endpoint, {
dynamic data,
Map<String, dynamic>? params,
Map<String, dynamic>? headers,
String? overrideBaseUrl,
}) async {
final String requestUrl =
overrideBaseUrl != null ? overrideBaseUrl + endpoint : endpoint;
return await _dio.post(
requestUrl,
data: data,
queryParameters: params,
options: headers != null ? Options(headers: headers) : null,
);
}
// GET (unchanged)
Future<Response> get(
String endpoint, { String endpoint, {
Map<String, dynamic>? params, Map<String, dynamic>? params,
Map<String, dynamic>? headers, // NOW SUPPORTED Map<String, dynamic>? headers,
String? overrideBaseUrl, String? overrideBaseUrl,
}) async { }) async {
final String requestUrl = final String requestUrl =
overrideBaseUrl != null ? overrideBaseUrl + endpoint : endpoint; overrideBaseUrl != null ? overrideBaseUrl + endpoint : endpoint;
@ -47,6 +64,5 @@ Future<Response> get(
queryParameters: params, queryParameters: params,
options: headers != null ? Options(headers: headers) : null, options: headers != null ? Options(headers: headers) : null,
); );
} }
} }

View File

@ -0,0 +1,13 @@
class ProductEntity {
final int id;
final String name;
final String image;
final double price;
ProductEntity({
required this.id,
required this.name,
required this.image,
required this.price,
});
}

View File

@ -25,16 +25,11 @@ class BrandsNotifier extends StateNotifier<AsyncValue<List<BrandEntity>>> {
Future<void> fetchBrands() async { Future<void> fetchBrands() async {
state = const AsyncValue.loading(); state = const AsyncValue.loading();
try { try {
// 1 Get user safely
final userAsync = ref.read(userDetailsProvider); final userAsync = ref.read(userDetailsProvider);
final user = userAsync.value; final user = userAsync.value;
if (user == null) throw Exception("User not logged in"); if (user == null) throw Exception("User not logged in");
// 2 Always fetch fresh token
final token = await repository.getTurn14AccessToken();
// 3 Fetch brands
final brands = await repository.getBrands(); final brands = await repository.getBrands();
state = AsyncValue.data(brands); state = AsyncValue.data(brands);
@ -43,9 +38,30 @@ class BrandsNotifier extends StateNotifier<AsyncValue<List<BrandEntity>>> {
debugPrint("Brands fetch error: $e"); debugPrint("Brands fetch error: $e");
} }
} }
/// SAVE SELECTED BRANDS TO SERVER (Bulk Insert)
Future<void> saveSelectedBrands(List<BrandEntity> selectedBrands) async {
try {
final userAsync = ref.read(userDetailsProvider);
final user = userAsync.value;
if (user == null) throw Exception("User not logged in");
final response = await repository.saveSelectedBrands(
userId: user.id,
brands: selectedBrands,
);
debugPrint("✅ Save Success: ${response["message"]}");
} catch (e) {
debugPrint("❌ Save Error: $e");
rethrow;
}
} }
/// Brands State Provider }
/// Brands State Provider
final brandsProvider = final brandsProvider =
StateNotifierProvider<BrandsNotifier, AsyncValue<List<BrandEntity>>>( StateNotifierProvider<BrandsNotifier, AsyncValue<List<BrandEntity>>>(
(ref) { (ref) {

View File

@ -0,0 +1,55 @@
import 'package:autos/data/repositories/product_repository.dart';
import 'package:autos/data/sources/remote/api_service.dart';
import 'package:autos/domain/entities/product.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';
/// API SERVICE PROVIDER
final productApiServiceProvider =
Provider<ApiService>((ref) => ApiService());
/// REPOSITORY PROVIDER
final productRepositoryProvider = Provider<ProductRepositoryImpl>((ref) {
return ProductRepositoryImpl(ref.read(productApiServiceProvider));
});
/// PRODUCT STATE PROVIDER
final productProvider = StateNotifierProvider<ProductNotifier,
AsyncValue<List<ProductEntity>>>((ref) {
final repository = ref.read(productRepositoryProvider);
return ProductNotifier(repository);
});
/// PRODUCT NOTIFIER
class ProductNotifier
extends StateNotifier<AsyncValue<List<ProductEntity>>> {
final ProductRepositoryImpl repository;
ProductNotifier(this.repository)
: super(const AsyncValue.loading());
/// FETCH PRODUCTS (EMPTY LIST IS NOT AN ERROR!)
Future<void> getProducts(String userId) async {
try {
state = const AsyncValue.loading();
debugPrint("🚀 FETCHING PRODUCTS FOR USER ID = $userId");
final products = await repository.getProducts(userId);
debugPrint("✅ PRODUCTS RECEIVED = ${products.length}");
// EVEN IF LIST IS EMPTY SUCCESS
state = AsyncValue.data(products);
} catch (e, st) {
debugPrint("❌ PRODUCT API REAL ERROR = $e");
state = AsyncValue.error(e, st);
}
}
/// CLEAR PRODUCTS SAFELY
void clear() {
state = const AsyncValue.data([]);
}
}

View File

@ -1,7 +1,6 @@
import 'package:autos/core/theme/app_typography.dart'; import 'package:autos/core/theme/app_typography.dart';
import 'package:autos/core/widgets/hamburger_button.dart'; import 'package:autos/core/widgets/hamburger_button.dart';
import 'package:autos/core/widgets/side_menu.dart'; import 'package:autos/core/widgets/side_menu.dart';
import 'package:autos/domain/entities/brands.dart';
import 'package:autos/presentation/providers/brand_provider.dart'; import 'package:autos/presentation/providers/brand_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -15,13 +14,19 @@ class BrandsScreen extends ConsumerStatefulWidget {
class _BrandsScreenState extends ConsumerState<BrandsScreen> { class _BrandsScreenState extends ConsumerState<BrandsScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String selected = 'brands'; String selected = 'brands';
String searchQuery = '';
bool dropshipOnly = false;
/// NEW: Selected Brand IDs
final Set<String> selectedBrandIds = {};
bool isSaving = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
/// Fetch brands every time page opens
Future.microtask(() { Future.microtask(() {
ref.read(brandsProvider.notifier).fetchBrands(); ref.read(brandsProvider.notifier).fetchBrands();
}); });
@ -40,34 +45,186 @@ class _BrandsScreenState extends ConsumerState<BrandsScreen> {
), ),
body: Stack( body: Stack(
children: [ children: [
/// TITLE (UNCHANGED)
Positioned( Positioned(
top: topPadding, top: topPadding,
left: 0, left: 0,
right: 0, right: 0,
child: Center( child: Center(
child: Text( child: Text(
"Brands", "Turn14 Brands",
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700), style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
), ),
), ),
), ),
/// HAMBURGER (UNCHANGED)
HamburgerButton(scaffoldKey: _scaffoldKey), HamburgerButton(scaffoldKey: _scaffoldKey),
Positioned.fill( Positioned.fill(
top: topPadding + 60, top: topPadding + 50,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
/// SEARCH (UNCHANGED)
TextField(
onChanged: (value) {
setState(() => searchQuery = value.toLowerCase());
},
decoration: InputDecoration(
hintText: "Search brands...",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 8),
/// FILTER + SELECT ALL (UI SAME)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Text("Dropship Only"),
Switch(
value: dropshipOnly,
onChanged: (val) {
setState(() => dropshipOnly = val);
},
activeColor: Colors.green,
),
],
),
/// SELECT ALL (LOGIC ONLY)
Row(
children: [
const Text("Select All"),
Checkbox(
value: brandsState.maybeWhen(
data: (brands) =>
brands.isNotEmpty &&
selectedBrandIds.length == brands.length,
orElse: () => false,
),
onChanged: (val) {
brandsState.whenData((brands) {
setState(() {
if (val == true) {
selectedBrandIds.addAll(
brands.map((e) => e.id),
);
} else {
selectedBrandIds.clear();
}
});
});
},
),
],
),
],
),
/// SAVE BUTTON (NEW FEATURE ONLY)
if (selectedBrandIds.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ElevatedButton(
onPressed: isSaving
? null
: () async {
try {
setState(() => isSaving = true);
final allBrands = brandsState.value!;
final selectedBrands = allBrands
.where(
(b) => selectedBrandIds.contains(b.id),
)
.toList();
await ref
.read(brandsProvider.notifier)
.saveSelectedBrands(selectedBrands);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
"✅ Collections Saved Successfully",
),
),
);
setState(() {
selectedBrandIds.clear();
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("❌ Save Failed: $e"),
),
);
} finally {
setState(() => isSaving = false);
}
},
child: isSaving
? const CircularProgressIndicator()
: Text(
"Save Collections (${selectedBrandIds.length})",
),
),
),
/// GRID (UI UNCHANGED)
Expanded(
child: brandsState.when( child: brandsState.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () =>
error: (err, st) => Center( const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(
child: Text( child: Text(
err.toString(), (() {
final raw = err.toString();
// Match 'message: ...' from string
final match = RegExp(
r'message[: ]+([^}]+)',
).firstMatch(raw);
if (match != null) {
return match
.group(1)!
.trim(); // just the message
}
// Fallback: remove 'Exception: ' prefix
return raw.replaceFirst(
RegExp(r'^Exception:\s*'),
'',
);
})(),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
), ),
), ),
data: (brands) { data: (brands) {
if (brands.isEmpty) { final filteredBrands = brands.where((brand) {
return const Center(child: Text("No brands available.")); final nameMatch = brand.name.toLowerCase().contains(
} searchQuery,
);
final idMatch = brand.id.toString().contains(
searchQuery,
);
final dropshipMatch = dropshipOnly
? brand.dropship
: true;
return (nameMatch || idMatch) && dropshipMatch;
}).toList();
return GridView.builder( return GridView.builder(
padding: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.only(bottom: 20),
@ -76,13 +233,16 @@ class _BrandsScreenState extends ConsumerState<BrandsScreen> {
crossAxisCount: 2, crossAxisCount: 2,
crossAxisSpacing: 12, crossAxisSpacing: 12,
mainAxisSpacing: 12, mainAxisSpacing: 12,
childAspectRatio: 1, childAspectRatio: 0.95,
), ),
itemCount: brands.length, itemCount: filteredBrands.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final BrandEntity brand = brands[index]; final brand = filteredBrands[index];
final isSelected = selectedBrandIds.contains(
brand.id,
);
return Container( return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -94,29 +254,99 @@ class _BrandsScreenState extends ConsumerState<BrandsScreen> {
), ),
], ],
), ),
child: Column( child: Stack(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
ClipRRect( Padding(
borderRadius: BorderRadius.circular(8), padding: const EdgeInsets.all(12),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 100,
width: 100,
child: ClipRRect(
borderRadius:
BorderRadius.circular(8),
child: Image.network( child: Image.network(
brand.logo, brand.logo,
height: 60, fit: BoxFit.contain,
width: 60,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => errorBuilder: (_, __, ___) =>
const Icon(Icons.image, size: 40), const Icon(Icons.image),
),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text("ID: ${brand.id}"),
const SizedBox(height: 4),
Flexible(
child: Text(
brand.name, brand.name,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: AppTypo.h3.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
/// DROPSHIP TAG (UNCHANGED)
if (brand.dropship)
Align(
alignment: Alignment.topLeft,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: const BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
bottomRight: Radius.circular(12),
),
),
child: const Text(
'Dropship',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
),
/// CHECKBOX ICON (UNCHANGED POSITION)
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8),
child: GestureDetector(
onTap: () {
setState(() {
if (isSelected) {
selectedBrandIds.remove(brand.id);
} else {
selectedBrandIds.add(brand.id);
}
});
},
child: Icon(
isSelected
? Icons.check_box
: Icons.square_outlined,
color: isSelected
? Colors.green
: Colors.grey,
),
),
),
), ),
], ],
), ),
@ -126,6 +356,9 @@ class _BrandsScreenState extends ConsumerState<BrandsScreen> {
}, },
), ),
), ),
],
),
),
), ),
], ],
), ),

View File

@ -0,0 +1,286 @@
import 'package:autos/core/theme/app_typography.dart';
import 'package:autos/core/widgets/hamburger_button.dart';
import 'package:autos/core/widgets/side_menu.dart';
import 'package:autos/presentation/providers/products_provider.dart';
import 'package:autos/presentation/providers/user_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ProductsScreen extends ConsumerStatefulWidget {
const ProductsScreen({super.key});
@override
ConsumerState<ProductsScreen> createState() => _ProductsScreenState();
}
class _ProductsScreenState extends ConsumerState<ProductsScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String selected = 'products';
bool _hasFetched = false; // prevents multiple API calls
// Filter state
String _searchText = '';
bool _inStockOnly = false;
@override
void initState() {
super.initState();
/// Wait for widget + provider to be fully ready
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadUserFromProvider();
});
}
/// LOAD USER FROM RIVERPOD (NOT SECURE STORAGE)
void _loadUserFromProvider() {
final userState = ref.read(userProvider);
final user = userState.value;
if (user == null || user.id == null || user.id!.isEmpty) {
debugPrint("❌ USER NOT FOUND IN PROVIDER");
return;
}
if (_hasFetched) return; // prevent duplicate API calls
_hasFetched = true;
debugPrint(" USER FROM PROVIDER ID: ${user.id}");
ref.read(productProvider.notifier).getProducts(user.id!);
}
/// Filter products based on search text and stock
List productsFilter(List products) {
return products.where((product) {
final matchesSearch = _searchText.isEmpty ||
product.name.toLowerCase().contains(_searchText.toLowerCase());
final matchesStock = !_inStockOnly || (product.inStock ?? true);
return matchesSearch && matchesStock;
}).toList();
}
@override
Widget build(BuildContext context) {
final productsState = ref.watch(productProvider);
final double topPadding = MediaQuery.of(context).padding.top + 16;
return Scaffold(
key: _scaffoldKey,
drawer: SideMenu(
selected: selected,
onItemSelected: (key) {
setState(() => selected = key);
},
),
body: Stack(
children: [
/// Page Title
Positioned(
top: topPadding,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Brand Products",
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
),
],
),
),
/// Main Content
SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20),
child: productsState.when(
/// LOADING
loading: () => const Center(
child: Padding(
padding: EdgeInsets.only(top: 120),
child: CircularProgressIndicator(),
),
),
/// ERROR
error: (e, _) => Center(
child: Padding(
padding: const EdgeInsets.only(top: 120),
child: Text(
(() {
final raw = e.toString();
// Extract backend message if exists
final match = RegExp(r'message[: ]+([^}]+)').firstMatch(raw);
if (match != null) return match.group(1)!.trim();
return raw.replaceFirst(RegExp(r'^Exception:\s*'), '');
})(),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
),
),
/// SUCCESS
data: (products) {
if (products.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.only(top: 120),
child: Text("No products found"),
),
);
}
// Apply search / stock filter
final filteredProducts = productsFilter(products);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Search + Filter + Count Row
Row(
children: [
// Search bar
Expanded(
child: TextField(
onChanged: (value) {
setState(() => _searchText = value);
},
decoration: InputDecoration(
hintText: "Search products",
prefixIcon: const Icon(Icons.search),
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.grey),
),
filled: true,
fillColor: Colors.white,
),
),
),
const SizedBox(width: 12),
// In Stock Only button
ElevatedButton(
onPressed: () {
setState(() => _inStockOnly = !_inStockOnly);
},
style: ElevatedButton.styleFrom(
backgroundColor: _inStockOnly ? Colors.green : Colors.grey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
"In Stock",
style: const TextStyle(fontSize: 12),
),
),
const SizedBox(width: 12),
// Count button
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
"${filteredProducts.length} products",
style: const TextStyle(fontSize: 12),
),
),
],
),
const SizedBox(height: 16),
/// Products Grid
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredProducts.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.68,
),
itemBuilder: (context, index) {
final product = filteredProducts[index];
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
blurRadius: 10,
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Product Image
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
product.image,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
const Icon(Icons.image_not_supported),
),
),
),
const SizedBox(height: 10),
/// Product Name
Text(
product.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
/// Price
Text(
"${product.price}",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
);
},
),
],
);
},
),
),
/// Hamburger Button
HamburgerButton(scaffoldKey: _scaffoldKey),
],
),
);
}
}