push notification implemented.
This commit is contained in:
parent
064e753fb9
commit
c6f2b15453
@ -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);
|
||||
}
|
||||
|
||||
@ -18,4 +18,5 @@ class AppRoutePaths {
|
||||
static const products = '/products';
|
||||
static const pricing = '/pricing';
|
||||
static const myAccount = '/myAccount';
|
||||
static const imports = '/imports';
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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"),
|
||||
|
||||
57
lib/data/models/import_product_model.dart
Normal file
57
lib/data/models/import_product_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<String, dynamic> 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,
|
||||
|
||||
@ -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<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 {
|
||||
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<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(
|
||||
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<String, dynamic> 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)';
|
||||
}
|
||||
}
|
||||
|
||||
147
lib/data/repositories/imports_repository_impl.dart
Normal file
147
lib/data/repositories/imports_repository_impl.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
25
lib/domain/entities/import_product.dart
Normal file
25
lib/domain/entities/import_product.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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<MyApp> {
|
||||
Future<void> _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<MyApp> {
|
||||
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,
|
||||
);
|
||||
|
||||
84
lib/presentation/providers/imports_provider.dart
Normal file
84
lib/presentation/providers/imports_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,6 +33,7 @@ class Turn14Notifier extends StateNotifier<AsyncValue<Turn14Entity?>> {
|
||||
|
||||
Turn14Notifier(this.repository) : super(const AsyncValue.data(null));
|
||||
|
||||
|
||||
/// Save Turn14 credentials
|
||||
Future<void> saveCredentials({
|
||||
required String userId,
|
||||
|
||||
@ -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<ApiService>((ref) => ApiService());
|
||||
|
||||
/// ✅ Provide repository that depends on ApiService
|
||||
/// Provide repository that depends on ApiService
|
||||
final userRepositoryProvider = Provider<UserRepositoryImpl>(
|
||||
(ref) => UserRepositoryImpl(ref.read(apiServiceProvider)),
|
||||
);
|
||||
|
||||
/// ✅ ✅ ✅ SINGLE GLOBAL USER PROVIDER (ONLY ONE YOU SHOULD USE)
|
||||
/// SINGLE GLOBAL USER PROVIDER
|
||||
final userProvider =
|
||||
StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
|
||||
final repo = ref.read(userRepositoryProvider);
|
||||
@ -38,7 +37,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
||||
UserNotifier(this.ref, this.repository)
|
||||
: super(const AsyncValue.data(null));
|
||||
|
||||
// ✅ ✅ AUTO LOGIN ON APP START
|
||||
// AUTO LOGIN ON APP START
|
||||
Future<void> loadUserFromStorage() async {
|
||||
final jsonString = await _storage.read(key: _userKey);
|
||||
|
||||
@ -53,7 +52,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
||||
state = AsyncValue.data(user);
|
||||
}
|
||||
|
||||
// ✅ ✅ LOGIN FLOW (FULLY SYNCHRONIZED)
|
||||
// LOGIN FLOW (FULLY SYNCHRONIZED)
|
||||
Future<void> login(String email, String password) async {
|
||||
lastAction = AuthAction.login;
|
||||
|
||||
@ -66,17 +65,17 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
||||
// 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<void> signup(
|
||||
String name,
|
||||
String email,
|
||||
@ -114,7 +113,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
||||
state = const AsyncValue.data(null);
|
||||
}
|
||||
|
||||
// ✅ ✅ PASSWORD RESET
|
||||
// PASSWORD RESET
|
||||
Future<void> sendPasswordResetLink(String email) async {
|
||||
try {
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
@ -186,6 +186,7 @@ class _InfoCardState extends State<InfoCard> {
|
||||
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)),
|
||||
|
||||
245
lib/presentation/screens/imports/imports_header.dart
Normal file
245
lib/presentation/screens/imports/imports_header.dart
Normal 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()),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
315
lib/presentation/screens/imports/imports_screen.dart
Normal file
315
lib/presentation/screens/imports/imports_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -93,7 +93,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
/// ✅ Main Content
|
||||
/// Main Content
|
||||
SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20),
|
||||
|
||||
@ -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<CreateLocationScreen> {
|
||||
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<CreateLocationScreen> {
|
||||
|
||||
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<CreateLocationScreen> {
|
||||
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<CreateLocationScreen> {
|
||||
color: Colors.black12,
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
@ -77,9 +85,17 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
|
||||
|
||||
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<CreateLocationScreen> {
|
||||
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<CreateLocationScreen> {
|
||||
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<CreateLocationScreen> {
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user