push notification implemented.

This commit is contained in:
bala 2025-12-08 00:40:20 +05:30
parent 064e753fb9
commit c6f2b15453
21 changed files with 1107 additions and 46 deletions

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/imports/imports_screen.dart';
import 'package:autos/presentation/screens/my_account/account_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/pricing/pricing_screen.dart';
import 'package:autos/presentation/screens/products/products_screen.dart'; import 'package:autos/presentation/screens/products/products_screen.dart';
@ -73,6 +74,9 @@ class AppRouter {
case AppRoutePaths.myAccount: case AppRoutePaths.myAccount:
return slideRoute(AccountScreen()); return slideRoute(AccountScreen());
case AppRoutePaths.imports:
return slideRoute(ImportsScreen());
default: default:
return _defaultFallback(settings); return _defaultFallback(settings);
} }

View File

@ -18,4 +18,5 @@ class AppRoutePaths {
static const products = '/products'; static const products = '/products';
static const pricing = '/pricing'; static const pricing = '/pricing';
static const myAccount = '/myAccount'; static const myAccount = '/myAccount';
static const imports = '/imports';
} }

View File

@ -4,7 +4,7 @@ class AppTheme {
// MAIN BRAND COLORS // MAIN BRAND COLORS
static const Color primary = Color(0xFF00BFFF); // Main Blue static const Color primary = Color(0xFF00BFFF); // Main Blue
static const Color lightBlue = Color(0xFFE8F7FF); // Sidebar highlight 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 textDark = Color(0xFF1C1C1C);
static const Color textGrey = Color(0xFF6F6F6F); static const Color textGrey = Color(0xFF6F6F6F);
static const Color cardBorder = Color(0xFFAEE9FF); static const Color cardBorder = Color(0xFFAEE9FF);

View File

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

View File

@ -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<String, dynamic> 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,
);
}
}

View File

@ -63,7 +63,9 @@ class Turn14StatusModel {
final String? clientId; final String? clientId;
final String? clientSecret; final String? clientSecret;
final String? accessToken; final String? accessToken;
final String? expiresIn; final int? expiresIn; // FIXED: should be int
final String? code; // ADDED
final String? message; // ADDED
Turn14StatusModel({ Turn14StatusModel({
required this.userId, required this.userId,
@ -72,19 +74,31 @@ class Turn14StatusModel {
this.clientSecret, this.clientSecret,
this.accessToken, this.accessToken,
this.expiresIn, this.expiresIn,
this.code,
this.message,
}); });
/// SAFE FROM JSON
factory Turn14StatusModel.fromJson(Map<String, dynamic> json) { factory Turn14StatusModel.fromJson(Map<String, dynamic> json) {
final credentials = json["credentials"];
final tokenInfo = json["tokenInfo"];
return Turn14StatusModel( return Turn14StatusModel(
userId: json["userid"], userId: json["userid"]?.toString() ?? "",
hasCredentials: json["hasCredentials"] ?? false, hasCredentials: json["hasCredentials"] ?? false,
clientId: json["credentials"]?["turn14clientid"],
clientSecret: json["credentials"]?["turn14clientsecret"], clientId: credentials?["turn14clientid"],
accessToken: json["tokenInfo"]?["access_token"], clientSecret: credentials?["turn14clientsecret"],
expiresIn: json["tokenInfo"]?["expires_in"],
accessToken: tokenInfo?["access_token"],
expiresIn: tokenInfo?["expires_in"],
code: json["code"],
message: json["message"],
); );
} }
/// MODEL ENTITY (CLEAN ARCH)
Turn14StatusEntity toEntity() { Turn14StatusEntity toEntity() {
return Turn14StatusEntity( return Turn14StatusEntity(
userId: userId, userId: userId,

View File

@ -1,7 +1,115 @@
import 'dart:convert'; import 'dart:convert';
import 'package:autos/domain/entities/user.dart'; 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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 { class UserModel extends User {
final StoreModel? store;
final PaymentModel? payment;
const UserModel({ const UserModel({
required super.id, required super.id,
required super.name, required super.name,
@ -12,25 +120,37 @@ class UserModel extends User {
super.phoneNumber, super.phoneNumber,
super.message, super.message,
super.code, super.code,
this.store,
this.payment,
}); });
/// FROM API JSON /// FROM API JSON
factory UserModel.fromJson(Map<String, dynamic> json) { factory UserModel.fromJson(Map<String, dynamic> 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( return UserModel(
id: json['userid'] ?? json['id']?.toString() ?? '', id: json['userid'] ?? json['id']?.toString() ?? '',
name: json['name'] ?? '', name: json['name'] ?? '',
email: json['email'] ?? '', email: json['email'] ?? '',
role: json['role'] ?? '', role: json['role'] ?? '',
plan: payment['plan'] ?? json['plan'], plan: payment?.plan ?? json['plan'],
paymentStatus: payment['status'] ?? json['paymentStatus'], paymentStatus: payment?.status ?? json['paymentStatus'],
phoneNumber: json['phonenumber'] ?? '', phoneNumber: json['phonenumber'] ?? '',
message: json['message'] ?? '', message: json['message'] ?? '',
code: json['code'] ?? '', code: json['code'] ?? '',
store: store,
payment: payment,
); );
} }
/// THIS WAS MISSING VERY IMPORTANT /// FROM ENTITY (LOCAL CONVERSION)
factory UserModel.fromEntity(User user) { factory UserModel.fromEntity(User user) {
return UserModel( return UserModel(
id: user.id, id: user.id,
@ -45,7 +165,7 @@ class UserModel extends User {
); );
} }
/// TO JSON FOR STORAGE /// TO JSON (FOR LOCAL STORAGE)
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'userid': id, 'userid': id,
@ -57,6 +177,8 @@ class UserModel extends User {
'phonenumber': phoneNumber, 'phonenumber': phoneNumber,
'message': message, 'message': message,
'code': code, 'code': code,
'store': store?.toJson(),
'payment': payment?.toJson(),
}; };
} }
@ -69,6 +191,6 @@ class UserModel extends User {
@override @override
String toString() { 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)';
} }
} }

View File

@ -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<List<ImportProductEntity>> 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<String> 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<List<ImportProductEntity>> 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<String, dynamic> 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<String> _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;
}
}

View File

@ -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,
});
}

View File

@ -18,7 +18,7 @@ class Turn14StatusEntity {
final String? clientId; final String? clientId;
final String? clientSecret; final String? clientSecret;
final String? accessToken; final String? accessToken;
final String? expiresIn; final int? expiresIn;
Turn14StatusEntity({ Turn14StatusEntity({
required this.userId, required this.userId,

View File

@ -7,7 +7,18 @@ import 'package:autos/presentation/screens/dashboard/dashboard_screen.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';
// ADD THIS IMPORT
import 'package:onesignal_flutter/onesignal_flutter.dart';
void main() { 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())); runApp(const ProviderScope(child: MyApp()));
} }
@ -30,20 +41,22 @@ class _MyAppState extends ConsumerState<MyApp> {
Future<void> _restoreSession() async { Future<void> _restoreSession() async {
final userNotifier = ref.read(userProvider.notifier); final userNotifier = ref.read(userProvider.notifier);
// Load user from secure storage // Load user from secure storage (UNCHANGED)
await userNotifier.loadUserFromStorage(); await userNotifier.loadUserFromStorage();
// Stop loading after user is restored // Stop loading after user is restored (UNCHANGED)
setState(() => _isLoading = false); setState(() => _isLoading = false);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Show loading screen until user session is restored // Loading screen (UNCHANGED)
if (_isLoading) { if (_isLoading) {
return const MaterialApp( return const MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
home: Scaffold(body: Center(child: CircularProgressIndicator())), home: Scaffold(
body: Center(child: CircularProgressIndicator()),
),
); );
} }
@ -55,8 +68,9 @@ class _MyAppState extends ConsumerState<MyApp> {
title: "Autos", title: "Autos",
theme: AppTheme.lightTheme, theme: AppTheme.lightTheme,
// Dynamic home based on session // Dynamic home based on session (UNCHANGED)
home: user != null ? const DashboardScreen() : const LoginScreen(), home: user != null ? const DashboardScreen() : const LoginScreen(),
navigatorKey: NavigationService.navigatorKey, navigatorKey: NavigationService.navigatorKey,
onGenerateRoute: AppRouter.generateRoute, onGenerateRoute: AppRouter.generateRoute,
); );

View File

@ -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<ImportsRepository>(
(ref) => ImportsRepositoryImpl(ref.read(apiServiceProvider)),
);
/// Main Imports Provider
final importsProvider = StateNotifierProvider<
ImportsNotifier, AsyncValue<List<ImportProductEntity>>>(
(ref) => ImportsNotifier(ref.read(importsRepositoryProvider)),
);
class ImportsNotifier
extends StateNotifier<AsyncValue<List<ImportProductEntity>>> {
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<void> 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();
}
}
}

View File

@ -33,6 +33,7 @@ class Turn14Notifier extends StateNotifier<AsyncValue<Turn14Entity?>> {
Turn14Notifier(this.repository) : super(const AsyncValue.data(null)); Turn14Notifier(this.repository) : super(const AsyncValue.data(null));
/// Save Turn14 credentials /// Save Turn14 credentials
Future<void> saveCredentials({ Future<void> saveCredentials({
required String userId, required String userId,

View File

@ -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/repositories/user_repository_impl.dart';
import 'package:autos/data/sources/remote/api_service.dart'; import 'package:autos/data/sources/remote/api_service.dart';
import 'package:autos/domain/entities/user.dart'; import 'package:autos/domain/entities/user.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart'; import 'package:flutter_riverpod/legacy.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.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<ApiService>((ref) => ApiService()); final apiServiceProvider = Provider<ApiService>((ref) => ApiService());
/// Provide repository that depends on ApiService /// Provide repository that depends on ApiService
final userRepositoryProvider = Provider<UserRepositoryImpl>( final userRepositoryProvider = Provider<UserRepositoryImpl>(
(ref) => UserRepositoryImpl(ref.read(apiServiceProvider)), (ref) => UserRepositoryImpl(ref.read(apiServiceProvider)),
); );
/// SINGLE GLOBAL USER PROVIDER (ONLY ONE YOU SHOULD USE) /// SINGLE GLOBAL USER PROVIDER
final userProvider = final userProvider =
StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) { StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
final repo = ref.read(userRepositoryProvider); final repo = ref.read(userRepositoryProvider);
@ -38,7 +37,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
UserNotifier(this.ref, this.repository) UserNotifier(this.ref, this.repository)
: super(const AsyncValue.data(null)); : super(const AsyncValue.data(null));
// AUTO LOGIN ON APP START // AUTO LOGIN ON APP START
Future<void> loadUserFromStorage() async { Future<void> loadUserFromStorage() async {
final jsonString = await _storage.read(key: _userKey); final jsonString = await _storage.read(key: _userKey);
@ -53,7 +52,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
state = AsyncValue.data(user); state = AsyncValue.data(user);
} }
// LOGIN FLOW (FULLY SYNCHRONIZED) // LOGIN FLOW (FULLY SYNCHRONIZED)
Future<void> login(String email, String password) async { Future<void> login(String email, String password) async {
lastAction = AuthAction.login; lastAction = AuthAction.login;
@ -66,17 +65,17 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
// 2 Fetch FULL user details // 2 Fetch FULL user details
final fullUser = await repository.getUserDetails(partialUser.id); 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()); 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); state = AsyncValue.data(fullUser);
} catch (e, st) { } catch (e, st) {
state = AsyncValue.error(e, st); state = AsyncValue.error(e, st);
} }
} }
// SIGNUP // SIGNUP
Future<void> signup( Future<void> signup(
String name, String name,
String email, String email,
@ -114,7 +113,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
state = const AsyncValue.data(null); state = const AsyncValue.data(null);
} }
// PASSWORD RESET // PASSWORD RESET
Future<void> sendPasswordResetLink(String email) async { Future<void> sendPasswordResetLink(String email) async {
try { try {
state = const AsyncValue.loading(); state = const AsyncValue.loading();

View File

@ -186,6 +186,7 @@ class _InfoCardState extends State<InfoCard> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
elevation: 2, elevation: 2,
shadowColor: Colors.black.withValues(alpha: 0.5),
margin: const EdgeInsets.only(bottom: 18), margin: const EdgeInsets.only(bottom: 18),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding( child: Padding(
@ -281,6 +282,7 @@ class UserDetailsCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
color: Colors.white, color: Colors.white,
shadowColor: Colors.black.withValues(alpha: 0.5),
elevation: 2, elevation: 2,
margin: const EdgeInsets.only(bottom: 18), margin: const EdgeInsets.only(bottom: 18),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),

View File

@ -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<ImportsHeader> createState() => _ImportsHeaderState();
}
class _ImportsHeaderState extends State<ImportsHeader> {
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<String> items,
required Function(String?) onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label),
const SizedBox(height: 6),
SizedBox(
width: 180,
child: DropdownButtonFormField<String>(
value: value,
onChanged: onChanged,
items: items
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
.toList(),
decoration: const InputDecoration(border: OutlineInputBorder()),
),
),
],
);
}
}

View File

@ -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<ImportsScreen> createState() => _ImportsScreenState();
}
class _ImportsScreenState extends ConsumerState<ImportsScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String selected = 'imports';
/// Selection & Filters
Set<String> 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<ImportProductEntity> filterProducts(List<ImportProductEntity> 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,
),
),
);
}
}

View File

@ -93,7 +93,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
), ),
), ),
/// Main Content /// Main Content
SingleChildScrollView( SingleChildScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20), padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20),

View File

@ -1,3 +1,4 @@
import 'package:autos/core/theme/app_theme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CreateLocationScreen extends StatefulWidget { class CreateLocationScreen extends StatefulWidget {
@ -16,7 +17,9 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
final TextEditingController stateCtrl = TextEditingController(); final TextEditingController stateCtrl = TextEditingController();
final TextEditingController postalCode = TextEditingController(); final TextEditingController postalCode = TextEditingController();
final TextEditingController country = 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 openTime = const TimeOfDay(hour: 9, minute: 0);
TimeOfDay closeTime = const TimeOfDay(hour: 18, minute: 0); TimeOfDay closeTime = const TimeOfDay(hour: 18, minute: 0);
@ -28,8 +31,10 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
if (picked != null) { if (picked != null) {
setState(() { setState(() {
if (isOpen) openTime = picked; if (isOpen)
else closeTime = picked; openTime = picked;
else
closeTime = picked;
}); });
} }
} }
@ -39,8 +44,11 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFEFFAFF), backgroundColor: const Color(0xFFEFFAFF),
appBar: AppBar( appBar: AppBar(
title: const Text("Create New Location"), title: const Text(
backgroundColor: Colors.white, "Create New Location",
style: TextStyle(color: Colors.white),
),
backgroundColor: AppTheme.primary,
elevation: 0, elevation: 0,
foregroundColor: Colors.black, foregroundColor: Colors.black,
), ),
@ -57,7 +65,7 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
color: Colors.black12, color: Colors.black12,
blurRadius: 20, blurRadius: 20,
offset: const Offset(0, 10), offset: const Offset(0, 10),
) ),
], ],
), ),
child: Column( child: Column(
@ -77,9 +85,17 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
Row( Row(
children: [ children: [
Expanded(child: _input("Postal Code *", "Postal Code", postalCode)), Expanded(
child: _input("Postal Code *", "Postal Code", postalCode),
),
const SizedBox(width: 12), 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<CreateLocationScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label, Text(
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), label,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
const SizedBox(height: 8), const SizedBox(height: 8),
TextField( TextField(
controller: controller, controller: controller,
@ -190,8 +208,10 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label, Text(
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), label,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
const SizedBox(height: 8), const SizedBox(height: 8),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 18), padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 18),
@ -201,13 +221,15 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
), ),
child: Row( child: Row(
children: [ children: [
Text(time.format(context), Text(
style: const TextStyle(fontSize: 16)), time.format(context),
style: const TextStyle(fontSize: 16),
),
const Spacer(), const Spacer(),
const Icon(Icons.access_time, size: 20), const Icon(Icons.access_time, size: 20),
], ],
), ),
) ),
], ],
), ),
); );

View File

@ -472,6 +472,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" 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: package_config:
dependency: transitive dependency: transitive
description: description:

View File

@ -21,6 +21,7 @@ dependencies:
flutter_secure_storage: ^9.2.4 flutter_secure_storage: ^9.2.4
youtube_player_flutter: ^9.1.3 youtube_player_flutter: ^9.1.3
flutter_launcher_icons: ^0.14.4 flutter_launcher_icons: ^0.14.4
onesignal_flutter: ^5.3.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: