From c6f2b15453bd56396219183e36fe177ca7dd9574 Mon Sep 17 00:00:00 2001 From: bala Date: Mon, 8 Dec 2025 00:40:20 +0530 Subject: [PATCH] push notification implemented. --- lib/core/routing/app_router.dart | 4 + lib/core/routing/route_paths.dart | 1 + lib/core/theme/app_theme.dart | 2 +- lib/core/widgets/side_menu.dart | 2 +- lib/data/models/import_product_model.dart | 57 ++++ lib/data/models/turn14_model.dart | 26 +- lib/data/models/user_model.dart | 134 +++++++- .../repositories/imports_repository_impl.dart | 147 ++++++++ lib/domain/entities/import_product.dart | 25 ++ lib/domain/entities/turn14.dart | 2 +- lib/main.dart | 24 +- .../providers/imports_provider.dart | 84 +++++ .../providers/turn14_provider.dart | 1 + lib/presentation/providers/user_provider.dart | 19 +- .../screens/dashboard/dashboard_screen.dart | 2 + .../screens/imports/imports_header.dart | 245 ++++++++++++++ .../screens/imports/imports_screen.dart | 315 ++++++++++++++++++ .../screens/products/products_screen.dart | 2 +- .../screens/store/create_location_screen.dart | 52 ++- pubspec.lock | 8 + pubspec.yaml | 1 + 21 files changed, 1107 insertions(+), 46 deletions(-) create mode 100644 lib/data/models/import_product_model.dart create mode 100644 lib/data/repositories/imports_repository_impl.dart create mode 100644 lib/domain/entities/import_product.dart create mode 100644 lib/presentation/providers/imports_provider.dart create mode 100644 lib/presentation/screens/imports/imports_header.dart create mode 100644 lib/presentation/screens/imports/imports_screen.dart diff --git a/lib/core/routing/app_router.dart b/lib/core/routing/app_router.dart index e95c739..1a8727b 100644 --- a/lib/core/routing/app_router.dart +++ b/lib/core/routing/app_router.dart @@ -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/dashboard/dashboard_screen.dart'; import 'package:autos/presentation/screens/ebay/ebay_screen.dart'; +import 'package:autos/presentation/screens/imports/imports_screen.dart'; import 'package:autos/presentation/screens/my_account/account_screen.dart'; import 'package:autos/presentation/screens/pricing/pricing_screen.dart'; import 'package:autos/presentation/screens/products/products_screen.dart'; @@ -73,6 +74,9 @@ class AppRouter { case AppRoutePaths.myAccount: return slideRoute(AccountScreen()); + case AppRoutePaths.imports: + return slideRoute(ImportsScreen()); + default: return _defaultFallback(settings); } diff --git a/lib/core/routing/route_paths.dart b/lib/core/routing/route_paths.dart index 7a50b52..4244a99 100644 --- a/lib/core/routing/route_paths.dart +++ b/lib/core/routing/route_paths.dart @@ -18,4 +18,5 @@ class AppRoutePaths { static const products = '/products'; static const pricing = '/pricing'; static const myAccount = '/myAccount'; + static const imports = '/imports'; } diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index bc5cb95..9a1a511 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -4,7 +4,7 @@ class AppTheme { // ✅ MAIN BRAND COLORS static const Color primary = Color(0xFF00BFFF); // Main Blue static const Color lightBlue = Color(0xFFE8F7FF); // Sidebar highlight - static const Color background = Color(0xFFF4FBFE); // App background + static const Color background = Color(0xFFFFFFFF); // 0xFFF4FBFE App background static const Color textDark = Color(0xFF1C1C1C); static const Color textGrey = Color(0xFF6F6F6F); static const Color cardBorder = Color(0xFFAEE9FF); diff --git a/lib/core/widgets/side_menu.dart b/lib/core/widgets/side_menu.dart index 5a1df36..85e3cf2 100644 --- a/lib/core/widgets/side_menu.dart +++ b/lib/core/widgets/side_menu.dart @@ -66,7 +66,7 @@ class SideMenu extends ConsumerWidget { _sectionHeader("MANAGE"), _menuItem(context, "🏷️", "Brands", AppRoutePaths.brands), _menuItem(context, "📦", "Products", AppRoutePaths.products), - _menuItem(context, "⬇️", "Imports", 'imports'), + _menuItem(context, "⬇️", "Imports", AppRoutePaths.imports), // --- ACCOUNT --- _sectionHeader("ACCOUNT"), diff --git a/lib/data/models/import_product_model.dart b/lib/data/models/import_product_model.dart new file mode 100644 index 0000000..7cc7640 --- /dev/null +++ b/lib/data/models/import_product_model.dart @@ -0,0 +1,57 @@ +import 'package:autos/domain/entities/import_product.dart'; + +class ImportProductModel { + final String id; + final String sku; + final String image; + final String name; + final String partNumber; + final String category; + final String subcategory; + final double price; + final int inventory; + final String offerStatus; + + ImportProductModel({ + required this.id, + required this.sku, + required this.image, + required this.name, + required this.partNumber, + required this.category, + required this.subcategory, + required this.price, + required this.inventory, + required this.offerStatus, + }); + + factory ImportProductModel.fromJson(Map json) { + return ImportProductModel( + id: json['id']?.toString() ?? '', + sku: json['sku'] ?? '', + image: json['imgSrc'] ?? '', + name: json['name'] ?? '', + partNumber: json['partNumber'] ?? '', + category: json['category'] ?? '', + subcategory: json['subcategory'] ?? '', + price: double.tryParse(json['price'].toString()) ?? 0, + inventory: json['inventory'] ?? 0, + offerStatus: json['offer_status'] ?? '', + ); + } + + ImportProductEntity toEntity() { + return ImportProductEntity( + id: id, + sku: sku, + image: image, + name: name, + partNumber: partNumber, + category: category, + subcategory: subcategory, + price: price, + inventory: inventory, + offerStatus: offerStatus, + ); + } +} diff --git a/lib/data/models/turn14_model.dart b/lib/data/models/turn14_model.dart index 6dc69f6..9a2528e 100644 --- a/lib/data/models/turn14_model.dart +++ b/lib/data/models/turn14_model.dart @@ -63,7 +63,9 @@ class Turn14StatusModel { final String? clientId; final String? clientSecret; final String? accessToken; - final String? expiresIn; + final int? expiresIn; // ✅ FIXED: should be int + final String? code; // ✅ ADDED + final String? message; // ✅ ADDED Turn14StatusModel({ required this.userId, @@ -72,19 +74,31 @@ class Turn14StatusModel { this.clientSecret, this.accessToken, this.expiresIn, + this.code, + this.message, }); + /// ✅ SAFE FROM JSON factory Turn14StatusModel.fromJson(Map json) { + final credentials = json["credentials"]; + final tokenInfo = json["tokenInfo"]; + return Turn14StatusModel( - userId: json["userid"], + userId: json["userid"]?.toString() ?? "", hasCredentials: json["hasCredentials"] ?? false, - clientId: json["credentials"]?["turn14clientid"], - clientSecret: json["credentials"]?["turn14clientsecret"], - accessToken: json["tokenInfo"]?["access_token"], - expiresIn: json["tokenInfo"]?["expires_in"], + + clientId: credentials?["turn14clientid"], + clientSecret: credentials?["turn14clientsecret"], + + accessToken: tokenInfo?["access_token"], + expiresIn: tokenInfo?["expires_in"], + + code: json["code"], + message: json["message"], ); } + /// ✅ MODEL → ENTITY (CLEAN ARCH) Turn14StatusEntity toEntity() { return Turn14StatusEntity( userId: userId, diff --git a/lib/data/models/user_model.dart b/lib/data/models/user_model.dart index 575e7fc..e3e4cca 100644 --- a/lib/data/models/user_model.dart +++ b/lib/data/models/user_model.dart @@ -1,7 +1,115 @@ import 'dart:convert'; import 'package:autos/domain/entities/user.dart'; +/// ✅ STORE MODEL +class StoreModel { + final String name; + final String description; + final String url; + final String urlPath; + final String? lastOpenedTime; + final String? lastOpenedTimeRaw; + final String? logoUrl; + + const StoreModel({ + required this.name, + required this.description, + required this.url, + required this.urlPath, + this.lastOpenedTime, + this.lastOpenedTimeRaw, + this.logoUrl, + }); + + factory StoreModel.fromJson(Map json) { + return StoreModel( + name: json['name'] ?? '', + description: json['description'] ?? '', + url: json['url'] ?? '', + urlPath: json['urlPath'] ?? '', + lastOpenedTime: json['lastOpenedTime'], + lastOpenedTimeRaw: json['lastOpenedTimeRaw'], + logoUrl: json['logo']?['url'], + ); + } + + Map toJson() { + return { + 'name': name, + 'description': description, + 'url': url, + 'urlPath': urlPath, + 'lastOpenedTime': lastOpenedTime, + 'lastOpenedTimeRaw': lastOpenedTimeRaw, + 'logo': { + 'url': logoUrl, + }, + }; + } +} + +/// ✅ PAYMENT MODEL +class PaymentModel { + final int id; + final String email; + final String amount; + final String plan; + final String status; + final String? stripeSessionId; + final String? stripePaymentIntentId; + final String? subscriptionId; + final String? startDate; + final String? endDate; + + const PaymentModel({ + required this.id, + required this.email, + required this.amount, + required this.plan, + required this.status, + this.stripeSessionId, + this.stripePaymentIntentId, + this.subscriptionId, + this.startDate, + this.endDate, + }); + + factory PaymentModel.fromJson(Map json) { + return PaymentModel( + id: json['id'] ?? 0, + email: json['email'] ?? '', + amount: json['amount'] ?? '', + plan: json['plan'] ?? '', + status: json['status'] ?? '', + stripeSessionId: json['stripeSessionId'], + stripePaymentIntentId: json['stripePaymentIntentId'], + subscriptionId: json['subscriptionId'], + startDate: json['startDate'], + endDate: json['endDate'], + ); + } + + Map toJson() { + return { + 'id': id, + 'email': email, + 'amount': amount, + 'plan': plan, + 'status': status, + 'stripeSessionId': stripeSessionId, + 'stripePaymentIntentId': stripePaymentIntentId, + 'subscriptionId': subscriptionId, + 'startDate': startDate, + 'endDate': endDate, + }; + } +} + +/// ✅ MAIN USER MODEL class UserModel extends User { + final StoreModel? store; + final PaymentModel? payment; + const UserModel({ required super.id, required super.name, @@ -12,25 +120,37 @@ class UserModel extends User { super.phoneNumber, super.message, super.code, + this.store, + this.payment, }); /// ✅ FROM API JSON factory UserModel.fromJson(Map json) { - final payment = json['payment'] ?? {}; + final paymentJson = json['payment']; + final storeJson = json['store']; + + final payment = + paymentJson != null ? PaymentModel.fromJson(paymentJson) : null; + + final store = + storeJson != null ? StoreModel.fromJson(storeJson) : null; + return UserModel( id: json['userid'] ?? json['id']?.toString() ?? '', name: json['name'] ?? '', email: json['email'] ?? '', role: json['role'] ?? '', - plan: payment['plan'] ?? json['plan'], - paymentStatus: payment['status'] ?? json['paymentStatus'], + plan: payment?.plan ?? json['plan'], + paymentStatus: payment?.status ?? json['paymentStatus'], phoneNumber: json['phonenumber'] ?? '', message: json['message'] ?? '', code: json['code'] ?? '', + store: store, + payment: payment, ); } - /// ✅ ✅ ✅ THIS WAS MISSING — VERY IMPORTANT + /// ✅ FROM ENTITY (LOCAL CONVERSION) factory UserModel.fromEntity(User user) { return UserModel( id: user.id, @@ -45,7 +165,7 @@ class UserModel extends User { ); } - /// ✅ TO JSON FOR STORAGE + /// ✅ TO JSON (FOR LOCAL STORAGE) Map toJson() { return { 'userid': id, @@ -57,6 +177,8 @@ class UserModel extends User { 'phonenumber': phoneNumber, 'message': message, 'code': code, + 'store': store?.toJson(), + 'payment': payment?.toJson(), }; } @@ -69,6 +191,6 @@ class UserModel extends User { @override String toString() { - return 'UserModel(id: $id, name: $name, email: $email, role: $role, phone: $phoneNumber, plan: $plan, status: $paymentStatus, code: $code)'; + return 'UserModel(id: $id, name: $name, email: $email, role: $role, plan: $plan, status: $paymentStatus)'; } } diff --git a/lib/data/repositories/imports_repository_impl.dart b/lib/data/repositories/imports_repository_impl.dart new file mode 100644 index 0000000..adf31be --- /dev/null +++ b/lib/data/repositories/imports_repository_impl.dart @@ -0,0 +1,147 @@ +import 'dart:convert'; +import 'package:autos/core/constants/api_endpoints.dart'; +import 'package:autos/data/models/import_product_model.dart'; +import 'package:autos/data/sources/remote/api_service.dart'; +import 'package:autos/domain/entities/import_product.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:autos/data/models/user_model.dart'; + +abstract class ImportsRepository { + Future> getUserProducts({ + required int page, + required int pageSize, + String? category, + String? subcategory, + }); +} + +class ImportsRepositoryImpl implements ImportsRepository { + final ApiService _apiService; + final _storage = const FlutterSecureStorage(); + static const _userKey = 'logged_in_user'; + + ImportsRepositoryImpl(this._apiService); + + /// ✅ TOKEN LOGIC (UNCHANGED) + Future getTurn14AccessToken() async { + final jsonString = await _storage.read(key: _userKey); + if (jsonString == null) { + throw Exception("User not found. Please login again."); + } + + final user = UserModel.fromJson(jsonDecode(jsonString)); + + final response = await _apiService.post( + ApiEndpoints.getToken, + {"userid": user.id}, + ); + + final data = response.data; + String token; + + if (data["code"] == "TOKEN_UPDATED") { + token = data["access_token"]; + await _storage.write(key: 'turn14_token', value: token); + } else if (data["code"] == "TOKEN_VALID") { + token = data["access_token"]; + await _storage.write(key: 'turn14_token', value: token); + token = await _storage.read(key: 'turn14_token') ?? ''; + if (token.isEmpty) { + throw Exception("Token missing in storage"); + } + } else { + throw Exception(data["message"]); + } + + debugPrint("✅ Turn14 Token: $token"); + return token; + } + + /// ✅✅✅ FINAL: FETCH USER PRODUCTS WITH PAGINATION + FILTERS + @override + Future> getUserProducts({ + required int page, + required int pageSize, + String? category, + String? subcategory, + }) async { + try { + final token = await getTurn14AccessToken(); + final userId = await _getUserId(); + + /// ✅ CLEAN PARAMS (SKIPS "All") + final Map params = { + "userid": userId, + "page": page, + "pageSize": pageSize, + }; + + if (category != null && category.isNotEmpty && category != "All") { + params["category"] = category; + } + + if (subcategory != null && + subcategory.isNotEmpty && + subcategory != "All") { + params["subcategory"] = subcategory; + } + + debugPrint("📡 FETCH USER PRODUCTS"); + debugPrint("➡️ PARAMS: $params"); + + final response = await _apiService.get( + "/api/user-products", + overrideBaseUrl: "https://ebay.backend.data4autos.com", + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + params: params, + ); + + debugPrint("✅ API STATUS: ${response.statusCode}"); + debugPrint("✅ RAW RESPONSE: ${response.data}"); + + if (response.statusCode != 200) { + throw Exception("Server error: ${response.statusCode}"); + } + + final data = response.data; + + /// ✅ API RETURNS `items` + if (data is! Map || data["items"] == null) { + throw Exception("Invalid API structure: 'items' key missing"); + } + + final List list = data["items"]; + + debugPrint("✅ TOTAL ITEMS LOADED: ${list.length}"); + + return list + .map( + (json) => ImportProductModel.fromJson(json).toEntity(), + ) + .toList(); + } on DioException catch (e) { + debugPrint("❌ NETWORK ERROR: ${e.response?.data ?? e.message}"); + throw Exception("Network error: ${e.response?.data ?? e.message}"); + } catch (e, st) { + debugPrint("❌ PARSING ERROR: $e"); + debugPrintStack(stackTrace: st); + throw Exception("Unexpected error: $e"); + } + } + + /// ✅ GET USER ID FROM STORAGE + Future _getUserId() async { + final jsonString = await _storage.read(key: _userKey); + if (jsonString == null) { + throw Exception("User not found"); + } + + final user = UserModel.fromJson(jsonDecode(jsonString)); + return user.id; + } +} diff --git a/lib/domain/entities/import_product.dart b/lib/domain/entities/import_product.dart new file mode 100644 index 0000000..4983194 --- /dev/null +++ b/lib/domain/entities/import_product.dart @@ -0,0 +1,25 @@ +class ImportProductEntity { + final String id; + final String sku; + final String image; + final String name; + final String partNumber; + final String category; + final String subcategory; + final double price; + final int inventory; + final String offerStatus; + + ImportProductEntity({ + required this.id, + required this.sku, + required this.image, + required this.name, + required this.partNumber, + required this.category, + required this.subcategory, + required this.price, + required this.inventory, + required this.offerStatus, + }); +} diff --git a/lib/domain/entities/turn14.dart b/lib/domain/entities/turn14.dart index 536940b..af0c3a8 100644 --- a/lib/domain/entities/turn14.dart +++ b/lib/domain/entities/turn14.dart @@ -18,7 +18,7 @@ class Turn14StatusEntity { final String? clientId; final String? clientSecret; final String? accessToken; - final String? expiresIn; + final int? expiresIn; Turn14StatusEntity({ required this.userId, diff --git a/lib/main.dart b/lib/main.dart index 4e86c32..0fa2604 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,7 +7,18 @@ import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +// ✅ ADD THIS IMPORT +import 'package:onesignal_flutter/onesignal_flutter.dart'; + void main() { + WidgetsFlutterBinding.ensureInitialized(); + + // ONESIGNAL SETUP (ADDED SAFELY) + OneSignal.Debug.setLogLevel(OSLogLevel.verbose); + OneSignal.initialize("271c3931-07ee-46b6-8629-7f0d63f58085"); + OneSignal.Notifications.requestPermission(false); + + // KEEP YOUR ORIGINAL LOGIC runApp(const ProviderScope(child: MyApp())); } @@ -30,20 +41,22 @@ class _MyAppState extends ConsumerState { Future _restoreSession() async { final userNotifier = ref.read(userProvider.notifier); - // Load user from secure storage + // ✅ Load user from secure storage (UNCHANGED) await userNotifier.loadUserFromStorage(); - // Stop loading after user is restored + // ✅ Stop loading after user is restored (UNCHANGED) setState(() => _isLoading = false); } @override Widget build(BuildContext context) { - // Show loading screen until user session is restored + // ✅ Loading screen (UNCHANGED) if (_isLoading) { return const MaterialApp( debugShowCheckedModeBanner: false, - home: Scaffold(body: Center(child: CircularProgressIndicator())), + home: Scaffold( + body: Center(child: CircularProgressIndicator()), + ), ); } @@ -55,8 +68,9 @@ class _MyAppState extends ConsumerState { title: "Autos", theme: AppTheme.lightTheme, - // Dynamic home based on session + // ✅ Dynamic home based on session (UNCHANGED) home: user != null ? const DashboardScreen() : const LoginScreen(), + navigatorKey: NavigationService.navigatorKey, onGenerateRoute: AppRouter.generateRoute, ); diff --git a/lib/presentation/providers/imports_provider.dart b/lib/presentation/providers/imports_provider.dart new file mode 100644 index 0000000..36ee9d9 --- /dev/null +++ b/lib/presentation/providers/imports_provider.dart @@ -0,0 +1,84 @@ +import 'package:autos/data/repositories/imports_repository_impl.dart'; +import 'package:autos/domain/entities/import_product.dart'; +import 'package:autos/presentation/providers/user_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/legacy.dart'; + +/// ✅ Repository Provider +final importsRepositoryProvider = Provider( + (ref) => ImportsRepositoryImpl(ref.read(apiServiceProvider)), +); + +/// ✅ Main Imports Provider +final importsProvider = StateNotifierProvider< + ImportsNotifier, AsyncValue>>( + (ref) => ImportsNotifier(ref.read(importsRepositoryProvider)), +); +class ImportsNotifier + extends StateNotifier>> { + final ImportsRepository _repo; + + ImportsNotifier(this._repo) : super(const AsyncLoading()); + + /// ✅ STATE + int _page = 1; + final int _pageSize = 48; + + String _category = "All"; + String _subcategory = "All"; + + int get currentPage => _page; + + /// ✅ LOAD PRODUCTS FROM API + Future loadProducts({bool resetPage = false}) async { + try { + if (resetPage) _page = 1; + + state = const AsyncLoading(); + + debugPrint("📡 Fetching products"); + debugPrint("➡ Page: $_page"); + debugPrint("➡ Category: $_category"); + debugPrint("➡ SubCategory: $_subcategory"); + + final products = await _repo.getUserProducts( + page: _page, + pageSize: _pageSize, + category: _category == "All" ? null : _category, + subcategory: _subcategory == "All" ? null : _subcategory, + ); + + state = AsyncData(products); + } catch (e, st) { + state = AsyncError(e, st); + } + } + + /// ✅ CATEGORY CHANGE (RESETS SUBCATEGORY AUTOMATICALLY) + void changeCategory(String value) { + _category = value; + _subcategory = "All"; // ✅ RESET HERE + loadProducts(resetPage: true); + } + + /// ✅ SUBCATEGORY CHANGE + void changeSubcategory(String value) { + _subcategory = value; + loadProducts(resetPage: true); + } + + /// ✅ NEXT PAGE + void nextPage() { + _page++; + loadProducts(); + } + + /// ✅ PREVIOUS PAGE + void prevPage() { + if (_page > 1) { + _page--; + loadProducts(); + } + } +} diff --git a/lib/presentation/providers/turn14_provider.dart b/lib/presentation/providers/turn14_provider.dart index d98567d..3c3e4e7 100644 --- a/lib/presentation/providers/turn14_provider.dart +++ b/lib/presentation/providers/turn14_provider.dart @@ -33,6 +33,7 @@ class Turn14Notifier extends StateNotifier> { Turn14Notifier(this.repository) : super(const AsyncValue.data(null)); + /// Save Turn14 credentials Future saveCredentials({ required String userId, diff --git a/lib/presentation/providers/user_provider.dart b/lib/presentation/providers/user_provider.dart index 50dcce1..108347b 100644 --- a/lib/presentation/providers/user_provider.dart +++ b/lib/presentation/providers/user_provider.dart @@ -4,20 +4,19 @@ import 'package:autos/data/models/user_model.dart'; import 'package:autos/data/repositories/user_repository_impl.dart'; import 'package:autos/data/sources/remote/api_service.dart'; import 'package:autos/domain/entities/user.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/legacy.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -/// ✅ Provide a single ApiService instance across the app +/// Provide a single ApiService instance across the app final apiServiceProvider = Provider((ref) => ApiService()); -/// ✅ Provide repository that depends on ApiService +/// Provide repository that depends on ApiService final userRepositoryProvider = Provider( (ref) => UserRepositoryImpl(ref.read(apiServiceProvider)), ); -/// ✅ ✅ ✅ SINGLE GLOBAL USER PROVIDER (ONLY ONE YOU SHOULD USE) +/// SINGLE GLOBAL USER PROVIDER final userProvider = StateNotifierProvider>((ref) { final repo = ref.read(userRepositoryProvider); @@ -38,7 +37,7 @@ class UserNotifier extends StateNotifier> { UserNotifier(this.ref, this.repository) : super(const AsyncValue.data(null)); - // ✅ ✅ AUTO LOGIN ON APP START + // AUTO LOGIN ON APP START Future loadUserFromStorage() async { final jsonString = await _storage.read(key: _userKey); @@ -53,7 +52,7 @@ class UserNotifier extends StateNotifier> { state = AsyncValue.data(user); } - // ✅ ✅ LOGIN FLOW (FULLY SYNCHRONIZED) + // LOGIN FLOW (FULLY SYNCHRONIZED) Future login(String email, String password) async { lastAction = AuthAction.login; @@ -66,17 +65,17 @@ class UserNotifier extends StateNotifier> { // 2️⃣ Fetch FULL user details final fullUser = await repository.getUserDetails(partialUser.id); - // 3️⃣ Store FULL user in secure storage ✅ + // 3️⃣ Store FULL user in secure storage await _storage.write(key: _userKey, value: fullUser.toRawJson()); - // 4️⃣ Update provider ONCE with FULL data ✅ + // 4️⃣ Update provider ONCE with FULL data state = AsyncValue.data(fullUser); } catch (e, st) { state = AsyncValue.error(e, st); } } - // ✅ ✅ SIGNUP + // SIGNUP Future signup( String name, String email, @@ -114,7 +113,7 @@ class UserNotifier extends StateNotifier> { state = const AsyncValue.data(null); } - // ✅ ✅ PASSWORD RESET + // PASSWORD RESET Future sendPasswordResetLink(String email) async { try { state = const AsyncValue.loading(); diff --git a/lib/presentation/screens/dashboard/dashboard_screen.dart b/lib/presentation/screens/dashboard/dashboard_screen.dart index c56d021..330fed0 100644 --- a/lib/presentation/screens/dashboard/dashboard_screen.dart +++ b/lib/presentation/screens/dashboard/dashboard_screen.dart @@ -186,6 +186,7 @@ class _InfoCardState extends State { Widget build(BuildContext context) { return Card( elevation: 2, + shadowColor: Colors.black.withValues(alpha: 0.5), margin: const EdgeInsets.only(bottom: 18), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( @@ -281,6 +282,7 @@ class UserDetailsCard extends StatelessWidget { Widget build(BuildContext context) { return Card( color: Colors.white, + shadowColor: Colors.black.withValues(alpha: 0.5), elevation: 2, margin: const EdgeInsets.only(bottom: 18), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), diff --git a/lib/presentation/screens/imports/imports_header.dart b/lib/presentation/screens/imports/imports_header.dart new file mode 100644 index 0000000..11609bc --- /dev/null +++ b/lib/presentation/screens/imports/imports_header.dart @@ -0,0 +1,245 @@ +import 'package:autos/core/theme/app_typography.dart'; +import 'package:flutter/material.dart'; + +class ImportsHeader extends StatefulWidget { + final int visibleCount; + final int pageSize; + final int totalCount; + final int selectedCount; + + final Function(String) onSearch; + final Function(bool) onStockToggle; + final Function(String) onCategoryChange; + final Function(String) onSubcategoryChange; + final Function(String) onOfferStatusChange; + final Function(double) onPriceChange; + + const ImportsHeader({ + super.key, + required this.visibleCount, + required this.pageSize, + required this.totalCount, + required this.selectedCount, + required this.onSearch, + required this.onStockToggle, + required this.onCategoryChange, + required this.onSubcategoryChange, + required this.onOfferStatusChange, + required this.onPriceChange, + }); + + @override + State createState() => _ImportsHeaderState(); +} + +class _ImportsHeaderState extends State { + String category = "All"; + String subcategory = "All"; + String offerStatus = "All"; + bool inStockOnly = false; + double priceFloor = 0; + + @override + Widget build(BuildContext context) { + final bool isMobile = MediaQuery.of(context).size.width < 900; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// ✅ TOP SECTION (OVERFLOW SAFE) + isMobile ? _mobileTopBar() : _desktopTopBar(), + + const SizedBox(height: 20), + + /// ✅ FILTER ROW + Wrap( + spacing: 16, + runSpacing: 12, + children: [ + /// ✅ CATEGORY (RESETS SUBCATEGORY) + _dropdown( + label: "Category", + value: category, + items: const ["All", "Floor Mats", "Wheels"], + onChanged: (v) { + final value = v ?? "All"; + setState(() { + category = value; + subcategory = "All"; // ✅ RESET + }); + + widget.onCategoryChange(value); + widget.onSubcategoryChange("All"); // ✅ FORCE API RESET + }, + ), + + /// ✅ SUBCATEGORY + _dropdown( + label: "Subcategory", + value: subcategory, + items: const ["All", "Wheels - Cast", "Wheels - Forged"], + onChanged: (v) { + final value = v ?? "All"; + setState(() => subcategory = value); + widget.onSubcategoryChange(value); + }, + ), + + /// ✅ OFFER STATUS + _dropdown( + label: "Offer Status", + value: offerStatus, + items: const ["All", "Published", "Draft"], + onChanged: (v) { + final value = v ?? "All"; + setState(() => offerStatus = value); + widget.onOfferStatusChange(value); + }, + ), + + /// ✅ PRICE SLIDER + SizedBox( + width: 260, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Price Floor"), + Slider( + min: 0, + max: 1000, + value: priceFloor, + onChanged: (value) { + setState(() => priceFloor = value); + widget.onPriceChange(value); + }, + ), + Text("Price ≥ \$${priceFloor.toInt()}"), + ], + ), + ), + ], + ), + ], + ); + } + + /// ✅ DESKTOP TOP BAR + Widget _desktopTopBar() { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child: _titleSection()), + const SizedBox(width: 12), + Flexible(child: _searchBox()), + const SizedBox(width: 12), + Wrap( + spacing: 12, + crossAxisAlignment: WrapCrossAlignment.center, + children: [_stockToggle(), _actionButton()], + ), + ], + ); + } + + /// ✅ MOBILE TOP BAR + Widget _mobileTopBar() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _titleSection(), + const SizedBox(height: 12), + _searchBox(), + const SizedBox(height: 12), + Wrap(spacing: 12, children: [_stockToggle(), _actionButton()]), + ], + ); + } + + /// ✅ TITLE + Widget _titleSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Imports", style: AppTypo.h3), + Text( + "${widget.visibleCount} of ${widget.totalCount} products", + style: AppTypo.bodySmall, + ), + ], + ); + } + + /// ✅ SEARCH BOX + Widget _searchBox() { + return SizedBox( + height: 44, + child: TextField( + onChanged: widget.onSearch, + decoration: const InputDecoration( + hintText: "Search imports...", + prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), + ), + ), + ); + } + + /// ✅ STOCK TOGGLE + Widget _stockToggle() { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Switch( + value: inStockOnly, + onChanged: (v) { + setState(() => inStockOnly = v); + widget.onStockToggle(v); + }, + ), + const SizedBox(width: 6), + const Text("In Stock Only"), + ], + ); + } + + /// ✅ ACTION BUTTON + Widget _actionButton() { + return ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24.0), + ), + ), + onPressed: () {}, + child: const Text("Bulk Action"), + ); + } + + /// ✅ REUSABLE DROPDOWN + Widget _dropdown({ + required String label, + required String value, + required List items, + required Function(String?) onChanged, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label), + const SizedBox(height: 6), + SizedBox( + width: 180, + child: DropdownButtonFormField( + value: value, + onChanged: onChanged, + items: items + .map((e) => DropdownMenuItem(value: e, child: Text(e))) + .toList(), + decoration: const InputDecoration(border: OutlineInputBorder()), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/screens/imports/imports_screen.dart b/lib/presentation/screens/imports/imports_screen.dart new file mode 100644 index 0000000..7ad4719 --- /dev/null +++ b/lib/presentation/screens/imports/imports_screen.dart @@ -0,0 +1,315 @@ +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/domain/entities/import_product.dart'; +import 'package:autos/presentation/providers/imports_provider.dart'; +import 'package:autos/presentation/screens/imports/imports_header.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ImportsScreen extends ConsumerStatefulWidget { + const ImportsScreen({super.key}); + + @override + ConsumerState createState() => _ImportsScreenState(); +} + +class _ImportsScreenState extends ConsumerState { + final GlobalKey _scaffoldKey = GlobalKey(); + String selected = 'imports'; + + /// ✅ Selection & Filters + Set selectedProducts = {}; + bool inStockOnly = false; + String searchQuery = ''; + + @override + void initState() { + super.initState(); + + /// ✅ Load API data on screen open + Future.microtask(() { + ref.read(importsProvider.notifier).loadProducts(); + }); + } + + /// ✅ FILTER LOGIC + List filterProducts(List products) { + return products.where((product) { + final matchesSearch = product.name.toLowerCase().contains( + searchQuery.toLowerCase(), + ); + + final matchesStock = + !inStockOnly || product.inventory > 0; // ✅ REAL STOCK CHECK + + return matchesSearch && matchesStock; + }).toList(); + } + + @override + Widget build(BuildContext context) { + final double topPadding = MediaQuery.of(context).padding.top + 16; + final productsAsync = ref.watch(importsProvider); + + return Scaffold( + key: _scaffoldKey, + drawer: SideMenu( + selected: selected, + onItemSelected: (key) { + setState(() => selected = key); + }, + ), + backgroundColor: const Color(0xFFF6FDFF), + body: Stack( + children: [ + /// ✅ TITLE + Positioned( + top: topPadding, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Imports", + style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700), + ), + ], + ), + ), + + /// ✅ MAIN CONTENT + SingleChildScrollView( + padding: EdgeInsets.fromLTRB(16, topPadding + 70, 16, 20), + child: Column( + children: [ + ImportsHeader( + visibleCount: 47, + pageSize: 48, + totalCount: 373, + selectedCount: selectedProducts.length, + + onSearch: (value) { + debugPrint("Search: $value"); + }, + + onStockToggle: (value) { + debugPrint("Stock Only: $value"); + }, + + onCategoryChange: (value) { + debugPrint("Category: $value"); + }, + + onSubcategoryChange: (value) { + debugPrint("Subcategory: $value"); + }, + + onOfferStatusChange: (value) { + debugPrint("Offer Status: $value"); + }, + + onPriceChange: (value) { + debugPrint("Price Floor: $value"); + }, + ), + + const SizedBox(height: 10), + + /// ✅ PRODUCT LIST FROM API + productsAsync.when( + loading: () => const Padding( + padding: EdgeInsets.only(top: 40), + child: CircularProgressIndicator(), + ), + error: (e, _) => Padding( + padding: const EdgeInsets.all(20), + child: Text("❌ Error: $e"), + ), + data: (products) { + final filtered = filterProducts(products); + + return Column( + children: [ + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: filtered.length, + itemBuilder: (context, index) { + final product = filtered[index]; + final isSelected = selectedProducts.contains( + product.id, + ); + + return ImportProductCard( + product: product, + selected: isSelected, + onTap: () { + setState(() { + if (isSelected) { + selectedProducts.remove(product.id); + } else { + selectedProducts.add(product.id); + } + }); + }, + ); + }, + ), + + const SizedBox(height: 20), + + /// ✅ SUBMIT BUTTON + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: selectedProducts.isEmpty + ? null + : () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Submitted ${selectedProducts.length} products', + ), + ), + ); + }, + child: Text( + 'Submit (${selectedProducts.length}) selected items', + ), + ), + ), + ], + ); + }, + ), + ], + ), + ), + + /// ✅ HAMBURGER BUTTON + HamburgerButton(scaffoldKey: _scaffoldKey), + ], + ), + ); + } +} + +class ImportProductCard extends StatelessWidget { + final ImportProductEntity product; + final bool selected; + final VoidCallback onTap; + + const ImportProductCard({ + super.key, + required this.product, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// ✅ TOP ROW + Row( + children: [ + Checkbox(value: selected, onChanged: (_) => onTap()), + const Text("Select"), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text("Will list: ${product.inventory}"), + ), + ], + ), + + /// ✅ IMAGE + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + product.image, + height: 160, + width: double.infinity, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => + const Icon(Icons.image_not_supported, size: 80), + ), + ), + + const SizedBox(height: 10), + + /// ✅ TITLE + Text( + product.name, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), + ), + + const SizedBox(height: 6), + + /// ✅ META INFO + Text("Part #: ${product.partNumber}"), + Text("${product.category} > ${product.subcategory}"), + + const SizedBox(height: 6), + + /// ✅ PRICE + INVENTORY + Text( + "Price: \$${product.price}", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text("Inventory: ${product.inventory}"), + + const SizedBox(height: 10), + + /// ✅ DESCRIPTION PREVIEW + Text(product.name, maxLines: 2, overflow: TextOverflow.ellipsis), + + const SizedBox(height: 8), + + /// ✅ STATUS TAGS + Row( + children: [ + _statusChip("published", product.offerStatus == "published"), + const SizedBox(width: 8), + _statusChip("Listing", true), + ], + ), + ], + ), + ), + ); + } + + Widget _statusChip(String label, bool active) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: active ? Colors.green.withOpacity(.15) : Colors.grey[200], + borderRadius: BorderRadius.circular(20), + ), + child: Text( + label, + style: TextStyle( + color: active ? Colors.green : Colors.grey[600], + fontWeight: FontWeight.w600, + ), + ), + ); + } +} diff --git a/lib/presentation/screens/products/products_screen.dart b/lib/presentation/screens/products/products_screen.dart index 323196e..8d0b86b 100644 --- a/lib/presentation/screens/products/products_screen.dart +++ b/lib/presentation/screens/products/products_screen.dart @@ -93,7 +93,7 @@ class _ProductsScreenState extends ConsumerState { ), ), - /// ✅ Main Content + /// Main Content SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20), diff --git a/lib/presentation/screens/store/create_location_screen.dart b/lib/presentation/screens/store/create_location_screen.dart index 19c6232..4550163 100644 --- a/lib/presentation/screens/store/create_location_screen.dart +++ b/lib/presentation/screens/store/create_location_screen.dart @@ -1,3 +1,4 @@ +import 'package:autos/core/theme/app_theme.dart'; import 'package:flutter/material.dart'; class CreateLocationScreen extends StatefulWidget { @@ -16,7 +17,9 @@ class _CreateLocationScreenState extends State { final TextEditingController stateCtrl = TextEditingController(); final TextEditingController postalCode = TextEditingController(); final TextEditingController country = TextEditingController(); - final TextEditingController timeZone = TextEditingController(text: "America/New_York"); + final TextEditingController timeZone = TextEditingController( + text: "America/New_York", + ); TimeOfDay openTime = const TimeOfDay(hour: 9, minute: 0); TimeOfDay closeTime = const TimeOfDay(hour: 18, minute: 0); @@ -28,8 +31,10 @@ class _CreateLocationScreenState extends State { if (picked != null) { setState(() { - if (isOpen) openTime = picked; - else closeTime = picked; + if (isOpen) + openTime = picked; + else + closeTime = picked; }); } } @@ -39,8 +44,11 @@ class _CreateLocationScreenState extends State { return Scaffold( backgroundColor: const Color(0xFFEFFAFF), appBar: AppBar( - title: const Text("Create New Location"), - backgroundColor: Colors.white, + title: const Text( + "Create New Location", + style: TextStyle(color: Colors.white), + ), + backgroundColor: AppTheme.primary, elevation: 0, foregroundColor: Colors.black, ), @@ -57,7 +65,7 @@ class _CreateLocationScreenState extends State { color: Colors.black12, blurRadius: 20, offset: const Offset(0, 10), - ) + ), ], ), child: Column( @@ -77,9 +85,17 @@ class _CreateLocationScreenState extends State { Row( children: [ - Expanded(child: _input("Postal Code *", "Postal Code", postalCode)), + Expanded( + child: _input("Postal Code *", "Postal Code", postalCode), + ), const SizedBox(width: 12), - Expanded(child: _input("Country *", "Country Code (e.g. US)", country)), + Expanded( + child: _input( + "Country *", + "Country Code (e.g. US)", + country, + ), + ), ], ), @@ -159,8 +175,10 @@ class _CreateLocationScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, - style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), + Text( + label, + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14), + ), const SizedBox(height: 8), TextField( controller: controller, @@ -190,8 +208,10 @@ class _CreateLocationScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, - style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), + Text( + label, + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14), + ), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 18), @@ -201,13 +221,15 @@ class _CreateLocationScreenState extends State { ), child: Row( children: [ - Text(time.format(context), - style: const TextStyle(fontSize: 16)), + Text( + time.format(context), + style: const TextStyle(fontSize: 16), + ), const Spacer(), const Icon(Icons.access_time, size: 20), ], ), - ) + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 0376281..2797eac 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -472,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + onesignal_flutter: + dependency: "direct main" + description: + name: onesignal_flutter + sha256: b5bb43bf496ddb5e3975ba54c6477cc2d1fcd18fb3698f195d2e0bfd376ddafd + url: "https://pub.dev" + source: hosted + version: "5.3.4" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fbd93a1..57096ea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: flutter_secure_storage: ^9.2.4 youtube_player_flutter: ^9.1.3 flutter_launcher_icons: ^0.14.4 + onesignal_flutter: ^5.3.4 dev_dependencies: flutter_test: