Compare commits

...

9 Commits
main ... dev

Author SHA1 Message Date
c6f2b15453 push notification implemented. 2025-12-08 00:40:20 +05:30
064e753fb9 UI changes in sidemenu. 2025-11-30 02:25:45 +05:30
fe49bdf9af improved app state management and app theme updated. 2025-11-30 01:34:51 +05:30
6e9ffdb9dd Pricing page implemented. 2025-11-29 22:28:02 +05:30
a978699dfc Products page connected to the backend. 2025-11-29 15:04:40 +05:30
595554d541 BackendCall Implemented for GET Token and Brands data. 2025-11-27 00:35:58 +05:30
41970033ad Added launcher icon and fixed navigation 2025-11-25 09:25:07 +05:30
dcc8a8ed92 Turn14 connected to backend.
eBay and Store UI implemented.
2025-11-25 00:44:07 +05:30
33c15b1a5f s 2025-11-25 00:36:35 +05:30
83 changed files with 4440 additions and 488 deletions

View File

@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<application <application
android:label="autos" android:label="Autos"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -427,7 +427,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@ -484,7 +484,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";

View File

@ -1,122 +1 @@
{ {"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,5 +1,5 @@
class ApiEndpoints { class ApiEndpoints {
static const String baseUrl = 'https://ebay.backend.data4autos.com/api'; static const String baseUrl = 'https://ebay.backend.data4autos.com';
static const Duration connectTimeout = Duration(seconds: 10); static const Duration connectTimeout = Duration(seconds: 10);
static const Duration receiveTimeout = Duration(seconds: 10); static const Duration receiveTimeout = Duration(seconds: 10);
@ -9,8 +9,21 @@ class ApiEndpoints {
}; };
///Authentication ///Authentication
static const login = '/auth/login'; static const login = '/api/auth/login';
static const signup = '/auth/signup'; static const signup = '/api/auth/signup';
static const forgotPassword = '/auth/forgot-password'; static const forgotPassword = '/api/auth/forgot-password';
static const userDetails = '/auth/users/'; static const userDetails = '/api/auth/users/';
///GET Token
static const getToken = '/api/auth/turn14/get-access-token';
///Turn14
static const turn14Save = '/api/auth/turn14/save';
static const turn14Status = '/api/auth/turn14/status';
///Brands
static const String brands = "/v1/brands";
///Products
static const product = '/api/brands/';
} }

View File

@ -2,7 +2,15 @@ import 'package:autos/core/routing/route_paths.dart';
import 'package:autos/presentation/screens/auth/forgot_password_screen.dart'; import 'package:autos/presentation/screens/auth/forgot_password_screen.dart';
import 'package:autos/presentation/screens/auth/login_screen.dart'; import 'package:autos/presentation/screens/auth/login_screen.dart';
import 'package:autos/presentation/screens/auth/sign_up_screen.dart'; 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/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';
import 'package:autos/presentation/screens/store/create_location_screen.dart';
import 'package:autos/presentation/screens/store/store.dart';
import 'package:autos/presentation/screens/turn14_screen/turn14_screen.dart'; import 'package:autos/presentation/screens/turn14_screen/turn14_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -45,6 +53,30 @@ class AppRouter {
case AppRoutePaths.turn14: case AppRoutePaths.turn14:
return slideRoute(const Turn14Screen()); return slideRoute(const Turn14Screen());
case AppRoutePaths.ebay:
return slideRoute(EbayScreen());
case AppRoutePaths.store:
return slideRoute(StoreScreen());
case AppRoutePaths.createStoreLocation:
return slideRoute(CreateLocationScreen());
case AppRoutePaths.brands:
return slideRoute(BrandsScreen());
case AppRoutePaths.products:
return slideRoute(ProductsScreen());
case AppRoutePaths.pricing:
return slideRoute(PricingScreen());
case AppRoutePaths.myAccount:
return slideRoute(AccountScreen());
case AppRoutePaths.imports:
return slideRoute(ImportsScreen());
default: default:
return _defaultFallback(settings); return _defaultFallback(settings);
} }

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
class NavigationService {
static final GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
static Future<dynamic> push(String route, {Object? arguments}) {
return navigatorKey.currentState!
.pushNamed(route, arguments: arguments);
}
static Future<dynamic> pushReplace(String route, {Object? arguments}) {
return navigatorKey.currentState!
.pushReplacementNamed(route, arguments: arguments);
}
static Future<dynamic> pushRemove(String route, {Object? arguments}) {
return navigatorKey.currentState!.pushNamedAndRemoveUntil(
route,
(route) => false,
arguments: arguments,
);
}
static void pop() {
return navigatorKey.currentState!.pop();
}
}

View File

@ -1,7 +1,22 @@
import 'package:flutter/material.dart';
ValueNotifier<String> selectedRoute = ValueNotifier<String>("/dashboard");
class AppRoutePaths { class AppRoutePaths {
///Auth
static const auth = '/auth'; static const auth = '/auth';
static const signup = '/signup'; static const signup = '/signup';
static const forgotPassword = '/forgotPassword'; static const forgotPassword = '/forgotPassword';
///Screens
static const dashboard = '/dashboard'; static const dashboard = '/dashboard';
static const turn14 = '/turn14'; static const turn14 = '/turn14';
static const ebay = '/ebay';
static const store = '/store';
static const createStoreLocation = '/createStoreLocation';
static const brands = '/brands';
static const products = '/products';
static const pricing = '/pricing';
static const myAccount = '/myAccount';
static const imports = '/imports';
} }

View File

@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
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(0xFFFFFFFF); // 0xFFF4FBFE App background
static const Color textDark = Color(0xFF1C1C1C);
static const Color textGrey = Color(0xFF6F6F6F);
static const Color cardBorder = Color(0xFFAEE9FF);
// GLOBAL APP THEME
static ThemeData lightTheme = ThemeData(
useMaterial3: true,
scaffoldBackgroundColor: background,
primaryColor: primary,
colorScheme: ColorScheme.fromSeed(
seedColor: primary,
brightness: Brightness.light,
),
fontFamily: 'Poppins',
// TEXT STYLES
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontSize: 34,
fontWeight: FontWeight.bold,
color: textDark,
),
headlineMedium: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: textDark,
),
headlineSmall: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: textDark,
),
bodyLarge: TextStyle(fontSize: 16, color: textDark),
bodyMedium: TextStyle(fontSize: 14, color: textGrey),
),
// ELEVATED BUTTON THEME (SUBSCRIBE BUTTONS)
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
elevation: 6,
shadowColor: Colors.black.withOpacity(0.2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 14),
textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
),
// OUTLINED BUTTON THEME (UNSELECTED BUTTONS)
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: primary,
side: const BorderSide(color: primary),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
// INPUT FIELD THEME
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: cardBorder),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: cardBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: primary, width: 2),
),
),
// CARD THEME (PRICING BOXES)
cardTheme: CardThemeData(
color: Colors.white,
elevation: 12,
shadowColor: Colors.black.withValues(alpha: 0.06),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(26)),
),
// APP BAR THEME
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
iconTheme: IconThemeData(color: Colors.white),
titleTextStyle: TextStyle(
color: textDark,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
// DIVIDER THEME
dividerTheme: const DividerThemeData(color: cardBorder, thickness: 1),
// ICON THEME
iconTheme: const IconThemeData(color: primary),
);
}

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 AppTypo { class AppTypo {
@ -13,15 +14,16 @@ class AppTypo {
); );
static const TextStyle h2 = TextStyle( static const TextStyle h2 = TextStyle(
color: Color(0xFF3B81F9), color: Color(0xFF00BFFF),
fontFamily: fontFamily, fontFamily: fontFamily,
fontSize: 28, fontSize: 28,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
); );
static const TextStyle h3 = TextStyle( static const TextStyle h3 = TextStyle(
color: Colors.white,
fontFamily: fontFamily, fontFamily: fontFamily,
fontSize: 24, fontSize: 22,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
@ -49,7 +51,7 @@ class AppTypo {
fontFamily: fontFamily, fontFamily: fontFamily,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: Color(0xFF2272f6), color: AppTheme.primary,
); );
static const TextStyle menu = TextStyle( static const TextStyle menu = TextStyle(

View File

@ -9,7 +9,7 @@ class HamburgerButton extends StatefulWidget {
super.key, super.key,
required this.scaffoldKey, required this.scaffoldKey,
this.backgroundColor = const Color(0xFFEAF1FF), // light background this.backgroundColor = const Color(0xFFEAF1FF), // light background
this.iconColor = const Color(0xFF3B81F9), // dark blue lines this.iconColor = const Color(0xFF00BFFF), // dark blue lines
}); });
@override @override

View File

@ -22,7 +22,7 @@ class SideMenu extends ConsumerWidget {
children: [ children: [
// Header // Header
DrawerHeader( DrawerHeader(
decoration: BoxDecoration(color: Color(0xFF3B81F9)), decoration: const BoxDecoration(color: Color(0xFF00BFFF)),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: Row( child: Row(
@ -59,19 +59,19 @@ class SideMenu extends ConsumerWidget {
// --- INTEGRATIONS --- // --- INTEGRATIONS ---
_sectionHeader("INTEGRATIONS"), _sectionHeader("INTEGRATIONS"),
_menuItem(context, "", "Turn14", AppRoutePaths.turn14), _menuItem(context, "", "Turn14", AppRoutePaths.turn14),
_menuItem(context, "🛍️", "eBay", 'ebay'), _menuItem(context, "🛍️", "eBay", AppRoutePaths.ebay),
_menuItem(context, "🛒", "Store", 'store'), _menuItem(context, "🛒", "Store", AppRoutePaths.store),
// --- MANAGE --- // --- MANAGE ---
_sectionHeader("MANAGE"), _sectionHeader("MANAGE"),
_menuItem(context, "🏷️", "Brands", 'brands'), _menuItem(context, "🏷️", "Brands", AppRoutePaths.brands),
_menuItem(context, "📦", "Products", 'products'), _menuItem(context, "📦", "Products", AppRoutePaths.products),
_menuItem(context, "⬇️", "Imports", 'imports'), _menuItem(context, "⬇️", "Imports", AppRoutePaths.imports),
// --- ACCOUNT --- // --- ACCOUNT ---
_sectionHeader("ACCOUNT"), _sectionHeader("ACCOUNT"),
_menuItem(context, "👤", "My Account", AppRoutePaths.auth), _menuItem(context, "👤", "My Account", AppRoutePaths.myAccount),
_menuItem(context, "💰", "Pricing Plan", 'pricing'), _menuItem(context, "💰", "Pricing Plan", AppRoutePaths.pricing),
], ],
), ),
), ),
@ -112,8 +112,10 @@ class SideMenu extends ConsumerWidget {
// Section title widget // Section title widget
Widget _sectionHeader(String title) { Widget _sectionHeader(String title) {
return Container( return Container(
padding: EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(color: Color.fromARGB(255, 225, 236, 255)), decoration: const BoxDecoration(
color: Color.fromARGB(255, 225, 236, 255),
),
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: Text( child: Text(
@ -123,45 +125,72 @@ class SideMenu extends ConsumerWidget {
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 15, fontSize: 15,
letterSpacing: 0.5, letterSpacing: 0.5,
color: Color(0xFF3B81F9), color: Color(0xFF00BFFF),
), ),
), ),
), ),
); );
} }
// Menu item widget // Menu item widget with persistent left indicator
Widget _menuItem( Widget _menuItem(
BuildContext context, BuildContext context,
String emoji, String emoji,
String title, String title,
String route, String route,
) { ) {
final bool isSelected = selected == route; return ValueListenableBuilder(
valueListenable: selectedRoute,
builder: (_, current, __) {
final bool isSelected = current == route;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
onTap: () { onTap: () async {
onItemSelected(route); Navigator.pop(context);
Navigator.pushNamed(context, route);
selectedRoute.value = route; // indicator ALWAYS updates
if (ModalRoute.of(context)?.settings.name != route) {
Navigator.pushReplacementNamed(context, route);
}
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? const Color(0x143B81F9) : Colors.transparent, color: isSelected
? const Color(0x143B81F9)
: Colors.transparent,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Row( child: Row(
children: [ children: [
Container(
width: 4,
height: 48,
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF3B81F9)
: Colors.transparent,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
bottomLeft: Radius.circular(10),
),
),
),
const SizedBox(width: 8),
Text(emoji, style: const TextStyle(fontSize: 18)), Text(emoji, style: const TextStyle(fontSize: 18)),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
title, title,
style: TextStyle( style: TextStyle(
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, fontWeight: isSelected
color: isSelected ? const Color(0xFF3B81F9) : Colors.black87, ? FontWeight.w600
: FontWeight.normal,
color: isSelected
? const Color(0xFF3B81F9)
: Colors.black87,
fontSize: 16, fontSize: 16,
), ),
), ),
@ -170,5 +199,7 @@ class SideMenu extends ConsumerWidget {
), ),
), ),
); );
},
);
} }
} }

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 WaveBackground extends StatelessWidget { class WaveBackground extends StatelessWidget {
@ -19,10 +20,7 @@ class WaveBackground extends StatelessWidget {
height: 220, height: 220,
decoration: const BoxDecoration( decoration: const BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [AppTheme.primary, AppTheme.primary],
Color(0xFF3B81F9), // blue
Color(0xFF5A96F9), // lighter blue
],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
@ -31,31 +29,6 @@ class WaveBackground extends StatelessWidget {
), ),
), ),
// Positioned(
// top: 50,
// left: 0,
// right: 0,
// child: Row(
// children: [
// Image.asset(
// 'assets/auth/ebay.png',
// height: 40,
// fit: BoxFit.contain,
// ),
// // Image.asset(
// // 'assets/auth/data.png',
// // height: 40,
// // fit: BoxFit.contain,
// // ),
// // Image.asset(
// // 'assets/auth/ebay.png',
// // height: 40,
// // fit: BoxFit.contain,
// // ),
// ],
// ),
// ),
// Optional bottom light curve (subtle, not dominant) // Optional bottom light curve (subtle, not dominant)
Positioned( Positioned(
bottom: 0, bottom: 0,
@ -65,7 +38,7 @@ class WaveBackground extends StatelessWidget {
clipper: BottomSoftWaveClipper(), clipper: BottomSoftWaveClipper(),
child: Container( child: Container(
height: 80, height: 80,
color: const Color(0xFF5A96F9), color: AppTheme.primary,
child: Align( child: Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Padding( child: Padding(

View File

@ -0,0 +1,107 @@
import 'package:autos/domain/entities/brands.dart';
class Brand {
final String id;
final String name;
final String logo;
final bool dropship;
final List<PriceGroup> pricegroups;
Brand({
required this.id,
required this.name,
required this.logo,
required this.dropship,
required this.pricegroups,
});
factory Brand.fromJson(Map<String, dynamic> json) {
return Brand(
id: json['id'].toString(),
name: json['name'] ?? '',
logo: json['logo'] ?? '',
dropship: json['dropship'] ?? false,
pricegroups: (json['pricegroups'] as List<dynamic>?)
?.map((e) => PriceGroup.fromJson(e))
.toList() ??
[],
);
}
/// 🔥 Model Entity
BrandEntity toEntity() {
return BrandEntity(
id: id,
name: name,
logo: logo,
dropship: dropship,
pricegroups: pricegroups.map((e) => e.toEntity()).toList(),
);
}
}
class PriceGroup {
final String pricegroupId;
final String pricegroupName;
final String pricegroupPrefix;
final List<dynamic> locationRules;
final List<PurchaseRestriction> purchaseRestrictions;
PriceGroup({
required this.pricegroupId,
required this.pricegroupName,
required this.pricegroupPrefix,
required this.locationRules,
required this.purchaseRestrictions,
});
factory PriceGroup.fromJson(Map<String, dynamic> json) {
return PriceGroup(
pricegroupId: json['pricegroup_id'].toString(),
pricegroupName: json['pricegroup_name'] ?? '',
pricegroupPrefix: json['pricegroup_prefix'] ?? '',
locationRules: json['location_rules'] ?? [],
purchaseRestrictions: (json['purchase_restrictions'] as List<dynamic>?)
?.map((e) => PurchaseRestriction.fromJson(e))
.toList() ??
[],
);
}
/// 🔥 Model Entity
PriceGroupEntity toEntity() {
return PriceGroupEntity(
pricegroupId: pricegroupId,
pricegroupName: pricegroupName,
pricegroupPrefix: pricegroupPrefix,
locationRules: locationRules,
purchaseRestrictions:
purchaseRestrictions.map((e) => e.toEntity()).toList(),
);
}
}
class PurchaseRestriction {
final String program;
final String yourStatus;
PurchaseRestriction({
required this.program,
required this.yourStatus,
});
factory PurchaseRestriction.fromJson(Map<String, dynamic> json) {
return PurchaseRestriction(
program: json['program'] ?? '',
yourStatus: json['your_status'] ?? '',
);
}
/// 🔥 Model Entity
PurchaseRestrictionEntity toEntity() {
return PurchaseRestrictionEntity(
program: program,
yourStatus: yourStatus,
);
}
}

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

@ -0,0 +1,27 @@
import 'package:autos/domain/entities/product.dart';
class ProductModel extends ProductEntity {
ProductModel({
required super.id,
required super.name,
required super.image,
required super.price,
});
factory ProductModel.fromJson(Map<String, dynamic> json) {
return ProductModel(
id: json['id'] ?? 0,
/// SAFE STRING CONVERSION
name: json['name']?.toString() ?? '',
/// API sends `logo`, but entity expects `image`
image: json['logo']?.toString() ?? '',
/// API DOES NOT SEND PRICE DEFAULT TO 0
price: json['price'] != null
? double.tryParse(json['price'].toString()) ?? 0.0
: 0.0,
);
}
}

View File

@ -0,0 +1,112 @@
import 'dart:convert';
import 'package:autos/domain/entities/turn14.dart';
class Turn14Response {
final String code;
final String message;
final String userId;
final String accessToken;
const Turn14Response({
required this.code,
required this.message,
required this.userId,
required this.accessToken,
});
factory Turn14Response.fromJson(Map<String, dynamic> json) {
return Turn14Response(
code: json['code'] ?? '',
message: json['message'] ?? '',
userId: json['userid'] ?? '',
accessToken: json['access_token'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'code': code,
'message': message,
'userid': userId,
'access_token': accessToken,
};
}
/// Convert model raw JSON string
String toRawJson() => jsonEncode(toJson());
/// Convert raw JSON string model
factory Turn14Response.fromRawJson(String raw) =>
Turn14Response.fromJson(jsonDecode(raw));
/// Convert Response Domain Entity
Turn14Entity toEntity() {
return Turn14Entity(
code: code,
message: message,
userId: userId,
accessToken: accessToken,
);
}
@override
String toString() {
return 'Turn14Response(code: $code, message: $message, userId: $userId, accessToken: $accessToken)';
}
}
class Turn14StatusModel {
final String userId;
final bool hasCredentials;
final String? clientId;
final String? clientSecret;
final String? accessToken;
final int? expiresIn; // FIXED: should be int
final String? code; // ADDED
final String? message; // ADDED
Turn14StatusModel({
required this.userId,
required this.hasCredentials,
this.clientId,
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"]?.toString() ?? "",
hasCredentials: json["hasCredentials"] ?? false,
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,
hasCredentials: hasCredentials,
clientId: clientId,
clientSecret: clientSecret,
accessToken: accessToken,
expiresIn: expiresIn,
);
}
}

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,23 +120,52 @@ class UserModel extends User {
super.phoneNumber, super.phoneNumber,
super.message, super.message,
super.code, super.code,
this.store,
this.payment,
}); });
/// 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,
); );
} }
/// FROM ENTITY (LOCAL CONVERSION)
factory UserModel.fromEntity(User user) {
return UserModel(
id: user.id,
name: user.name,
email: user.email,
role: user.role,
plan: user.plan,
paymentStatus: user.paymentStatus,
phoneNumber: user.phoneNumber,
message: user.message,
code: user.code,
);
}
/// TO JSON (FOR LOCAL STORAGE)
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'userid': id, 'userid': id,
@ -40,18 +177,20 @@ class UserModel extends User {
'phonenumber': phoneNumber, 'phonenumber': phoneNumber,
'message': message, 'message': message,
'code': code, 'code': code,
'store': store?.toJson(),
'payment': payment?.toJson(),
}; };
} }
/// Store user as raw JSON string /// STORE USER AS RAW JSON STRING
String toRawJson() => jsonEncode(toJson()); String toRawJson() => jsonEncode(toJson());
/// Parse user from raw JSON string /// RESTORE USER FROM RAW JSON STRING
factory UserModel.fromRawJson(String raw) => factory UserModel.fromRawJson(String raw) =>
UserModel.fromJson(jsonDecode(raw)); UserModel.fromJson(jsonDecode(raw));
@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,127 @@
import 'dart:convert';
import 'package:autos/core/constants/api_endpoints.dart';
import 'package:autos/data/models/brands_model.dart';
import 'package:autos/data/sources/remote/api_service.dart';
import 'package:autos/domain/entities/brands.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 BrandsRepository {
Future<List<BrandEntity>> getBrands();
}
class BrandsRepositoryImpl implements BrandsRepository {
final ApiService _apiService;
final _storage = const FlutterSecureStorage();
static const _userKey = 'logged_in_user';
BrandsRepositoryImpl(this._apiService);
/// ALWAYS CALL TOKEN API (FORCE REFRESH)
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") {
// New token returned
token = data["access_token"];
await _storage.write(key: 'turn14_token', value: token);
} else if (data["code"] == "TOKEN_VALID") {
// Token still valid, read from storage
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;
}
/// FETCH BRANDS USING FRESH TOKEN EVERY TIME
@override
Future<List<BrandEntity>> getBrands() async {
try {
final token = await getTurn14AccessToken();
final response = await _apiService.get(
'/v1/brands',
overrideBaseUrl: 'https://turn14.data4autos.com',
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
if (response.statusCode != 200) {
throw Exception("Server error: ${response.statusCode}");
}
final data = response.data;
if (data is! Map || data["data"] == null) {
throw Exception("Invalid API response");
}
final list = data["data"] as List;
return list.map((json) => Brand.fromJson(json).toEntity()).toList();
} on DioException catch (e) {
throw Exception("Network error: ${e.response?.data ?? e.message}");
} catch (e) {
throw Exception("Unexpected error: $e");
}
}
/// BULK INSERT SELECTED BRANDS
Future<Map<String, dynamic>> saveSelectedBrands({
required String userId,
required List<BrandEntity> brands,
}) async {
try {
final payload = {
"userid": userId,
"brands": brands
.map(
(brand) => {
"id": brand.id.toString(),
"name": brand.name,
"logo": brand.logo,
"dropship": brand.dropship,
},
)
.toList(),
};
final response = await _apiService.postWithOptions(
"/api/brands/bulk-insert",
overrideBaseUrl: "https://ebay.backend.data4autos.com",
data: payload,
);
return response.data;
} on DioException catch (e) {
throw Exception("Save failed: ${e.response?.data ?? e.message}");
} catch (e) {
throw Exception("Unexpected error: $e");
}
}
}

View File

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

View File

@ -0,0 +1,57 @@
import 'package:autos/core/constants/api_endpoints.dart';
import 'package:autos/data/sources/remote/api_service.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class TokenRepository {
final ApiService apiService;
final _storage = const FlutterSecureStorage();
TokenRepository(this.apiService);
static const _tokenKey = "turn14_token";
static const _tokenTimeKey = "turn14_token_time";
/// Get Valid Token (Auto Refresh If Expired)
Future<String> getValidToken(String userId) async {
final token = await _storage.read(key: _tokenKey);
final time = await _storage.read(key: _tokenTimeKey);
if (token != null && time != null) {
final savedTime = DateTime.parse(time);
final diff = DateTime.now().difference(savedTime);
/// Token Valid for 50 minutes
if (diff.inMinutes < 50) {
return token;
}
}
/// 🔄 Token Expired Refresh
return await _refreshToken(userId);
}
/// 🔄 Refresh Token API
Future<String> _refreshToken(String userId) async {
final response = await apiService.post(
ApiEndpoints.getToken,
{"userid": userId},
);
final data = response.data;
if (data["code"] != "TOKEN_UPDATED") {
throw Exception(data["message"]);
}
final token = data["access_token"];
/// Save New Token + Timestamp
await _storage.write(key: _tokenKey, value: token);
await _storage.write(
key: _tokenTimeKey,
value: DateTime.now().toIso8601String(),
);
return token;
}
}

View File

@ -0,0 +1,77 @@
import 'package:autos/core/constants/api_endpoints.dart';
import 'package:autos/data/models/turn14_model.dart';
import 'package:autos/data/sources/remote/api_service.dart';
import 'package:autos/domain/entities/turn14.dart';
import 'package:dio/dio.dart';
abstract class Turn14Repository {
/// Save Turn14 credentials
Future<Turn14Entity> save({
required String userId,
required String clientId,
required String clientSecret,
});
}
class Turn14RepositoryImpl implements Turn14Repository {
final ApiService _apiService;
Turn14RepositoryImpl(this._apiService);
@override
Future<Turn14Entity> save({
required String userId,
required String clientId,
required String clientSecret,
}) async {
try {
final response = await _apiService.post(ApiEndpoints.turn14Save, {
"userid": userId,
"turn14clientid": clientId,
"turn14clientsecret": clientSecret,
});
// Check status
if (response.statusCode == 200) {
final data = response.data;
if (data['code'] == "TURN14_SAVED") {
/// Convert Response Entity
return Turn14Response.fromJson(data).toEntity();
} else {
throw Exception(
data['message'] ?? "Failed to save Turn14 credentials",
);
}
} else {
throw Exception("Server error: ${response.statusCode}");
}
} on DioException catch (e) {
throw Exception("Network error: ${e.message}");
} catch (e) {
throw Exception("Unexpected error: $e");
}
}
Future<Turn14StatusEntity> status(String userId) async {
try {
final response = await _apiService.post(ApiEndpoints.turn14Status, {
"userid": userId,
});
if (response.statusCode == 200) {
final data = response.data;
if (data["code"] == "TURN14_STATUS") {
return Turn14StatusModel.fromJson(data).toEntity();
} else {
throw Exception(data["message"] ?? "Failed to fetch Turn14 status");
}
} else {
throw Exception("Server error: ${response.statusCode}");
}
} catch (e) {
throw Exception("Network/Unexpected error: $e");
}
}
}

View File

@ -14,7 +14,6 @@ class ApiService {
headers: ApiEndpoints.defaultHeaders, headers: ApiEndpoints.defaultHeaders,
), ),
) { ) {
// Add logging interceptor in debug mode
if (kDebugMode) { if (kDebugMode) {
_dio.interceptors.add( _dio.interceptors.add(
LogInterceptor(requestBody: true, responseBody: true), LogInterceptor(requestBody: true, responseBody: true),
@ -22,6 +21,7 @@ class ApiService {
} }
} }
// KEEP OLD POST (so existing files DO NOT BREAK)
Future<Response> post( Future<Response> post(
String endpoint, String endpoint,
Map<String, dynamic> data, { Map<String, dynamic> data, {
@ -30,7 +30,39 @@ class ApiService {
return await _dio.post(endpoint, data: data, options: options); return await _dio.post(endpoint, data: data, options: options);
} }
Future<Response> get(String endpoint, {Map<String, dynamic>? params}) async { // NEW POST (for overrideBaseUrl + headers + params)
return await _dio.get(endpoint, queryParameters: params); Future<Response> postWithOptions(
String endpoint, {
dynamic data,
Map<String, dynamic>? params,
Map<String, dynamic>? headers,
String? overrideBaseUrl,
}) async {
final String requestUrl =
overrideBaseUrl != null ? overrideBaseUrl + endpoint : endpoint;
return await _dio.post(
requestUrl,
data: data,
queryParameters: params,
options: headers != null ? Options(headers: headers) : null,
);
}
// GET (unchanged)
Future<Response> get(
String endpoint, {
Map<String, dynamic>? params,
Map<String, dynamic>? headers,
String? overrideBaseUrl,
}) async {
final String requestUrl =
overrideBaseUrl != null ? overrideBaseUrl + endpoint : endpoint;
return await _dio.get(
requestUrl,
queryParameters: params,
options: headers != null ? Options(headers: headers) : null,
);
} }
} }

View File

@ -0,0 +1,41 @@
class BrandEntity {
final String id;
final String name;
final String logo;
final bool dropship;
final List<PriceGroupEntity> pricegroups;
BrandEntity({
required this.id,
required this.name,
required this.logo,
required this.dropship,
required this.pricegroups,
});
}
class PriceGroupEntity {
final String pricegroupId;
final String pricegroupName;
final String pricegroupPrefix;
final List<dynamic> locationRules;
final List<PurchaseRestrictionEntity> purchaseRestrictions;
PriceGroupEntity({
required this.pricegroupId,
required this.pricegroupName,
required this.pricegroupPrefix,
required this.locationRules,
required this.purchaseRestrictions,
});
}
class PurchaseRestrictionEntity {
final String program;
final String yourStatus;
PurchaseRestrictionEntity({
required this.program,
required this.yourStatus,
});
}

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

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

View File

@ -0,0 +1,31 @@
class Turn14Entity {
final String code;
final String message;
final String userId;
final String accessToken;
const Turn14Entity({
required this.code,
required this.message,
required this.userId,
required this.accessToken,
});
}
class Turn14StatusEntity {
final String userId;
final bool hasCredentials;
final String? clientId;
final String? clientSecret;
final String? accessToken;
final int? expiresIn;
Turn14StatusEntity({
required this.userId,
required this.hasCredentials,
this.clientId,
this.clientSecret,
this.accessToken,
this.expiresIn,
});
}

View File

@ -1,11 +1,24 @@
import 'package:autos/core/routing/app_router.dart'; import 'package:autos/core/routing/app_router.dart';
import 'package:autos/core/routing/navigation_service.dart';
import 'package:autos/core/theme/app_theme.dart';
import 'package:autos/presentation/providers/user_provider.dart'; import 'package:autos/presentation/providers/user_provider.dart';
import 'package:autos/presentation/screens/auth/login_screen.dart'; import 'package:autos/presentation/screens/auth/login_screen.dart';
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart'; 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()));
} }
@ -28,16 +41,16 @@ 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,
@ -53,13 +66,12 @@ class _MyAppState extends ConsumerState<MyApp> {
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: "Autos", title: "Autos",
theme: ThemeData( theme: AppTheme.lightTheme,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
// 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,
onGenerateRoute: AppRouter.generateRoute, onGenerateRoute: AppRouter.generateRoute,
); );
} }

View File

@ -0,0 +1,79 @@
import 'package:autos/data/repositories/brands_repository_impl.dart';
import 'package:autos/data/sources/remote/api_service.dart';
import 'package:autos/domain/entities/brands.dart';
import 'package:autos/presentation/providers/user_provider.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';
/// API Service Provider
final brandsApiServiceProvider = Provider<ApiService>((ref) => ApiService());
/// Repository Provider
final brandsRepositoryProvider = Provider<BrandsRepositoryImpl>(
(ref) => BrandsRepositoryImpl(ref.read(brandsApiServiceProvider)),
);
/// Brands Notifier
class BrandsNotifier extends StateNotifier<AsyncValue<List<BrandEntity>>> {
final BrandsRepositoryImpl repository;
final Ref ref;
BrandsNotifier(this.repository, this.ref) : super(const AsyncValue.loading());
/// Fetch brands every time page opens
Future<void> fetchBrands() async {
state = const AsyncValue.loading();
try {
/// READ USER DIRECTLY FROM userProvider (NOT userDetailsProvider)
final userAsync = ref.read(userProvider);
final user = userAsync.value;
if (user == null || user.id.isEmpty) {
throw Exception("User id is empty");
}
debugPrint("✅ FETCHING BRANDS FOR USER ID: ${user.id}");
final brands = await repository.getBrands();
state = AsyncValue.data(brands);
} catch (e, st) {
state = AsyncValue.error(e, st);
debugPrint("❌ Brands fetch error: $e");
}
}
/// SAVE SELECTED BRANDS TO SERVER (Bulk Insert)
Future<void> saveSelectedBrands(List<BrandEntity> selectedBrands) async {
try {
final userAsync = ref.read(userProvider);
final user = userAsync.value;
if (user == null) throw Exception("User not logged in");
final response = await repository.saveSelectedBrands(
userId: user.id,
brands: selectedBrands,
);
debugPrint("✅ Save Success: ${response["message"]}");
} catch (e) {
debugPrint("❌ Save Error: $e");
rethrow;
}
}
}
/// Brands State Provider
final brandsProvider =
StateNotifierProvider<BrandsNotifier, AsyncValue<List<BrandEntity>>>(
(ref) {
final repo = ref.read(brandsRepositoryProvider);
return BrandsNotifier(repo, ref);
},
);

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

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

View File

@ -0,0 +1,7 @@
import 'package:autos/presentation/providers/user_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:autos/data/repositories/token_repository.dart';
final tokenRepositoryProvider = Provider<TokenRepository>(
(ref) => TokenRepository(ref.read(apiServiceProvider)),
);

View File

@ -0,0 +1,92 @@
import 'dart:convert';
import 'package:autos/data/models/turn14_model.dart';
import 'package:autos/data/repositories/turn14_repository_impl.dart';
import 'package:autos/data/sources/remote/api_service.dart';
import 'package:autos/domain/entities/turn14.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// ------------------------------------------------------------
/// Service Providers
/// ------------------------------------------------------------
final turn14ApiServiceProvider =
Provider<ApiService>((ref) => ApiService());
final turn14RepositoryProvider = Provider<Turn14RepositoryImpl>((ref) {
return Turn14RepositoryImpl(ref.read(turn14ApiServiceProvider));
});
/// ------------------------------------------------------------
/// Turn14 Notifier
/// ------------------------------------------------------------
class Turn14Notifier extends StateNotifier<AsyncValue<Turn14Entity?>> {
final Turn14RepositoryImpl repository;
final _storage = const FlutterSecureStorage();
static const _turn14StorageKey = "turn14_credentials";
Turn14Notifier(this.repository) : super(const AsyncValue.data(null));
/// Save Turn14 credentials
Future<void> saveCredentials({
required String userId,
required String clientId,
required String clientSecret,
}) async {
state = const AsyncValue.loading();
try {
final response = await repository.save(
userId: userId,
clientId: clientId,
clientSecret: clientSecret,
);
// if (response is Turn14Response) {
// await _storage.write(
// key: _turn14StorageKey,
// value: response.toRawJson(),
// );
// }
state = AsyncValue.data(response);
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
/// Load saved Turn14 credentials
Future<void> loadSavedCredentials() async {
final saved = await _storage.read(key: _turn14StorageKey);
if (saved == null) return;
final decoded = jsonDecode(saved);
final model = Turn14Response.fromJson(decoded);
state = AsyncValue.data(model as Turn14Entity?);
}
/// Clear saved Turn14 data
Future<void> clear() async {
await _storage.delete(key: _turn14StorageKey);
state = const AsyncValue.data(null);
}
}
/// ------------------------------------------------------------
/// Riverpod Provider
/// ------------------------------------------------------------
final turn14Provider =
StateNotifierProvider<Turn14Notifier, AsyncValue<Turn14Entity?>>((ref) {
final repository = ref.read(turn14RepositoryProvider);
return Turn14Notifier(repository);
});

View File

@ -4,100 +4,78 @@ 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)),
); );
// Manage user state /// SINGLE GLOBAL USER PROVIDER
final loginProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>(( final userProvider =
ref, StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
) {
final repo = ref.read(userRepositoryProvider); final repo = ref.read(userRepositoryProvider);
return UserNotifier(repo); return UserNotifier(ref, repo);
});
final signupProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>((
ref,
) {
final repo = ref.read(userRepositoryProvider);
return UserNotifier(repo);
});
final userProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>((
ref,
) {
final repo = ref.read(userRepositoryProvider);
return UserNotifier(repo);
});
final userDetailsProvider = FutureProvider<UserModel?>((ref) async {
final userAsync = ref.watch(userProvider);
// Waiting for login provider to finish
if (userAsync.isLoading) return null;
final user = userAsync.value;
if (user == null) return null;
// Fetch Full Details
final repo = ref.read(userRepositoryProvider);
return await repo.getUserDetails(user.id);
}); });
enum AuthAction { idle, login, signup } enum AuthAction { idle, login, signup }
class UserNotifier extends StateNotifier<AsyncValue<User?>> { class UserNotifier extends StateNotifier<AsyncValue<User?>> {
final Ref ref;
final UserRepositoryImpl repository; final UserRepositoryImpl repository;
final _storage = const FlutterSecureStorage(); final _storage = const FlutterSecureStorage();
static const _userKey = 'logged_in_user'; static const _userKey = 'logged_in_user';
AuthAction lastAction = AuthAction.idle; AuthAction lastAction = AuthAction.idle;
UserNotifier(this.repository) : super(const AsyncValue.data(null)); UserNotifier(this.ref, this.repository)
: super(const AsyncValue.data(null));
///Load saved user from storage (auto-login) // 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);
if (jsonString != null) {
debugPrint("RESULT: $jsonString"); if (jsonString == null) {
final jsonData = jsonDecode(jsonString); state = const AsyncValue.data(null);
final user = UserModel.fromJson(jsonData); return;
await Future.microtask(() {});
state = AsyncValue.data(user);
}
} }
///Login final jsonData = jsonDecode(jsonString);
final user = UserModel.fromJson(jsonData);
state = AsyncValue.data(user);
}
// 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;
state = const AsyncValue.loading();
try { try {
final user = await repository.login(email, password); state = const AsyncValue.loading();
// Save user to secure storage
if (user is UserModel) { // 1 Login API (partial user)
await _storage.write(key: _userKey, value: user.toRawJson()); final partialUser = await repository.login(email, password);
}
state = AsyncValue.data(user); // 2 Fetch FULL user details
final fullUser = await repository.getUserDetails(partialUser.id);
// 3 Store FULL user in secure storage
await _storage.write(key: _userKey, value: fullUser.toRawJson());
// 4 Update provider ONCE with FULL data
state = AsyncValue.data(fullUser);
} catch (e, st) { } catch (e, st) {
state = AsyncValue.error(e, st); state = AsyncValue.error(e, st);
} }
} }
/// Logout // SIGNUP
Future<void> logout() async {
await _storage.delete(key: _userKey);
state = const AsyncValue.data(null);
}
///Sign up
Future<void> signup( Future<void> signup(
String name, String name,
String email, String email,
@ -105,47 +83,44 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
String phone, String phone,
) async { ) async {
lastAction = AuthAction.signup; lastAction = AuthAction.signup;
state = const AsyncValue.loading();
try { try {
state = const AsyncValue.loading();
// 1 Signup returns USER ENTITY
final user = await repository.signup(name, email, password, phone); final user = await repository.signup(name, email, password, phone);
// 2 Convert User UserModel for storage
final userModel = UserModel.fromEntity(user);
// 3 Store safely
await _storage.write(
key: _userKey,
value: userModel.toRawJson(),
);
// 4 Update provider state
state = AsyncValue.data(user); state = AsyncValue.data(user);
} catch (e, st) { } catch (e, st) {
state = AsyncValue.error(e, st); state = AsyncValue.error(e, st);
} }
} }
///Reset password
// LOGOUT
Future<void> logout() async {
await _storage.delete(key: _userKey);
state = const AsyncValue.data(null);
}
// PASSWORD RESET
Future<void> sendPasswordResetLink(String email) async { Future<void> sendPasswordResetLink(String email) async {
state = const AsyncValue.loading();
try { try {
state = const AsyncValue.loading();
await repository.sendPasswordResetLink(email); await repository.sendPasswordResetLink(email);
state = const AsyncValue.data(null); state = const AsyncValue.data(null);
} catch (e, st) { } catch (e, st) {
state = AsyncValue.error(e, st); state = AsyncValue.error(e, st);
} }
} }
/// Fetch user details from backend
Future<void> getUserDetails(String userId) async {
state = const AsyncValue.loading();
try {
final user = await repository.getUserDetails(userId);
// Save full details separately
await saveUserDetails(user);
state = AsyncValue.data(user);
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
/// Save full user details separately
Future<void> saveUserDetails(UserModel user) async {
await _storage.write(
key: 'logged_in_user_details',
value: user.toRawJson(),
);
}
} }

View File

@ -1,4 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:autos/core/theme/app_theme.dart';
import 'package:autos/core/theme/app_typography.dart';
import 'package:autos/data/models/user_model.dart'; import 'package:autos/data/models/user_model.dart';
import 'package:autos/presentation/providers/user_provider.dart'; import 'package:autos/presentation/providers/user_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -10,7 +12,8 @@ class ForgotPasswordScreen extends ConsumerStatefulWidget {
const ForgotPasswordScreen({super.key}); const ForgotPasswordScreen({super.key});
@override @override
ConsumerState<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState(); ConsumerState<ForgotPasswordScreen> createState() =>
_ForgotPasswordScreenState();
} }
class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> { class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
@ -51,8 +54,9 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Forgot Password"), title: Text("Forgot Password", style: AppTypo.h3),
backgroundColor: const Color(0xFF3B81F9), backgroundColor: AppTheme.primary,
iconTheme: Theme.of(context).appBarTheme.iconTheme,
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
@ -62,10 +66,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
children: [ children: [
const Text( const Text(
"Reset your password", "Reset your password",
style: TextStyle( style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
fontSize: 24,
fontWeight: FontWeight.bold,
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
const Text( const Text(
@ -104,7 +105,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
Fluttertoast.showToast( Fluttertoast.showToast(
msg: "Reset link sent to $email", msg: "Reset link sent to $email",
backgroundColor: Colors.green, backgroundColor: Colors.green,
toastLength: Toast.LENGTH_LONG toastLength: Toast.LENGTH_LONG,
); );
Navigator.pop(context); Navigator.pop(context);
} catch (e) { } catch (e) {
@ -115,7 +116,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
} }
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3B81F9), backgroundColor: AppTheme.primary,
minimumSize: const Size(double.infinity, 50), minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40), borderRadius: BorderRadius.circular(40),

View File

@ -1,7 +1,8 @@
import 'package:autos/core/routing/route_paths.dart';
import 'package:autos/core/theme/app_theme.dart';
import 'package:autos/core/widgets/sso_icon_button.dart'; import 'package:autos/core/widgets/sso_icon_button.dart';
import 'package:autos/core/widgets/wave_background.dart'; import 'package:autos/core/widgets/wave_background.dart';
import 'package:autos/presentation/providers/user_provider.dart'; import 'package:autos/presentation/providers/user_provider.dart';
import 'package:autos/presentation/screens/auth/forgot_password_screen.dart';
import 'package:autos/presentation/screens/auth/sign_up_screen.dart'; import 'package:autos/presentation/screens/auth/sign_up_screen.dart';
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart'; import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -42,13 +43,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userState = ref.watch(loginProvider); final userState = ref.watch(userProvider);
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final double verticalSpace = size.height * 0.02; final double verticalSpace = size.height * 0.02;
final double horizontalPadding = size.width * 0.06; final double horizontalPadding = size.width * 0.06;
ref.listen(loginProvider, (previous, next) { ref.listen(userProvider, (previous, next) {
if (next.hasValue && next.value != null) { if (next.hasValue && next.value != null) {
_showToast("Login Successful"); _showToast("Login Successful");
Navigator.pushReplacement( Navigator.pushReplacement(
@ -137,16 +138,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
const Spacer(), const Spacer(),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.push( Navigator.pushNamed(
context, context,
MaterialPageRoute( AppRoutePaths.forgotPassword,
builder: (_) => const ForgotPasswordScreen(),
),
); );
}, },
child: const Text( child: Text(
"Forgot Password?", "Forgot Password?",
style: TextStyle(color: Color(0xFF3B81F9)), style: Theme.of(context).textTheme.bodySmall,
), ),
), ),
], ],
@ -160,14 +159,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
: ElevatedButton( : ElevatedButton(
onPressed: () async { onPressed: () async {
await ref await ref
.read(loginProvider.notifier) .read(userProvider.notifier)
.login( .login(
emailController.text.trim(), emailController.text.trim(),
passwordController.text.trim(), passwordController.text.trim(),
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3B81F9), backgroundColor: AppTheme.primary,
minimumSize: Size( minimumSize: Size(
double.infinity, double.infinity,
size.height * 0.05, size.height * 0.05,

View File

@ -1,3 +1,4 @@
import 'package:autos/core/theme/app_theme.dart';
import 'package:autos/core/widgets/wave_background.dart'; import 'package:autos/core/widgets/wave_background.dart';
import 'package:autos/presentation/providers/user_provider.dart'; import 'package:autos/presentation/providers/user_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -29,14 +30,15 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userState = ref.watch(signupProvider); /// FIXED: use userProvider instead of signupProvider
final userState = ref.watch(userProvider);
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final double verticalSpace = size.height * 0.02; final double verticalSpace = size.height * 0.02;
final double horizontalPadding = final double horizontalPadding = size.width * 0.06;
size.width * 0.06; // dynamic horizontal padding
ref.listen(signupProvider, (previous, next) { /// FIXED: listen to userProvider
ref.listen(userProvider, (previous, next) {
if (next.hasValue && next.value != null) { if (next.hasValue && next.value != null) {
Fluttertoast.showToast( Fluttertoast.showToast(
msg: "Sign up successful! Please log in.", msg: "Sign up successful! Please log in.",
@ -73,20 +75,18 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: size.width < 500 maxWidth: size.width < 500 ? size.width : 450,
? size.width
: 450, // better center layout on tablets
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
SizedBox(height: size.height * 0.07), SizedBox(height: size.height * 0.07),
/// Logo
Image.asset( Image.asset(
'assets/auth/autos_transp.png', 'assets/auth/autos_transp.png',
height: size.height * 0.14, height: size.height * 0.14,
), ),
Row( Row(
children: const [ children: const [
Text( Text(
@ -99,9 +99,9 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
Spacer(), Spacer(),
], ],
), ),
SizedBox(height: verticalSpace), SizedBox(height: verticalSpace),
/// Name field
TextField( TextField(
controller: nameController, controller: nameController,
decoration: const InputDecoration( decoration: const InputDecoration(
@ -109,9 +109,9 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
prefixIcon: Icon(Icons.person_outline), prefixIcon: Icon(Icons.person_outline),
), ),
), ),
SizedBox(height: verticalSpace), SizedBox(height: verticalSpace),
/// Email field
TextField( TextField(
controller: emailController, controller: emailController,
decoration: const InputDecoration( decoration: const InputDecoration(
@ -119,9 +119,9 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
prefixIcon: Icon(Icons.email_outlined), prefixIcon: Icon(Icons.email_outlined),
), ),
), ),
SizedBox(height: verticalSpace), SizedBox(height: verticalSpace),
/// Password field
TextField( TextField(
controller: passwordController, controller: passwordController,
obscureText: !isPasswordVisible, obscureText: !isPasswordVisible,
@ -142,9 +142,9 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
), ),
), ),
), ),
SizedBox(height: verticalSpace), SizedBox(height: verticalSpace),
/// Phone field
TextField( TextField(
controller: phoneController, controller: phoneController,
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
@ -153,24 +153,25 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
prefixIcon: Icon(Icons.phone_outlined), prefixIcon: Icon(Icons.phone_outlined),
), ),
), ),
SizedBox(height: verticalSpace * 2), SizedBox(height: verticalSpace * 2),
/// Sign Up button /// FIXED SIGNUP BUTTON
userState.isLoading userState.isLoading
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: ElevatedButton( : ElevatedButton(
onPressed: () async { onPressed: () async {
await ref await ref
.read(signupProvider.notifier) .read(userProvider.notifier)
.signup( .signup(
nameController.text, nameController.text.trim(),
emailController.text, emailController.text.trim(),
passwordController.text, passwordController.text.trim(),
phoneController.text, phoneController.text.trim(),
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3B81F9), backgroundColor: AppTheme.primary,
minimumSize: Size( minimumSize: Size(
double.infinity, double.infinity,
size.height * 0.05, size.height * 0.05,
@ -191,7 +192,6 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
SizedBox(height: verticalSpace * 2.5), SizedBox(height: verticalSpace * 2.5),
/// Already have account text
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -203,13 +203,14 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
child: const Text( child: const Text(
"Sign In", "Sign In",
style: TextStyle( style: TextStyle(
color: Color(0xFF3B81F9), color: AppTheme.primary,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
), ),
], ],
), ),
SizedBox(height: verticalSpace), SizedBox(height: verticalSpace),
], ],
), ),

View File

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

View File

@ -19,7 +19,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = ref.watch(userDetailsProvider); final userAsync = ref.watch(userProvider);
final double topPadding = MediaQuery.of(context).padding.top + 16; final double topPadding = MediaQuery.of(context).padding.top + 16;
return Scaffold( return Scaffold(
@ -32,7 +32,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
), ),
body: Stack( body: Stack(
children: [ children: [
// TOP BAR RESPONSIVE & CENTERED
Positioned( Positioned(
top: topPadding, top: topPadding,
left: 0, left: 0,
@ -47,31 +46,20 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
], ],
), ),
), ),
// 👇 MAIN CONTENT SECTION
SingleChildScrollView( SingleChildScrollView(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(16, topPadding + 70, 16, 20),
16,
topPadding + 70, // pushes content below title
16,
20,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// --- Welcome Header ---
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: [ children: [
Text("👋", style: const TextStyle(fontSize: 32)), const Text("Hi, 👋", style: TextStyle(fontSize: 32)),
const SizedBox(width: 8), const SizedBox(width: 8),
Text("Hi,", style: AppTypo.h3), Text("Hi,", style: AppTypo.h3),
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
"Manage your Turn14 & eBay integrations effortlessly in one platform.", "Manage your Turn14 & eBay integrations effortlessly in one platform.",
style: AppTypo.body.copyWith( style: AppTypo.body.copyWith(
@ -79,50 +67,57 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
height: 1.4, height: 1.4,
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// INFO CARDS RESPONSIVE // Info cards
InfoCard( const InfoCard(
emoji: "🎥", emoji: "🎥",
heading: "How to Access Your Store", heading: "How to Access Your Store",
content: "Watch the full walkthrough.", content: "Watch the full walkthrough.",
url: "https://youtu.be/g6qV2cQ2Fhw", url: "https://youtu.be/g6qV2cQ2Fhw",
), ),
const InfoCard(
InfoCard(
emoji: "📊", emoji: "📊",
heading: "About Data4Autos", heading: "About Data4Autos",
content: content:
"Data4Autos is an advanced automation platform for eCommerce sellers. Connect, manage, and automate your Turn14 and eBay stores effortlessly — all in one place.", "Data4Autos is an advanced automation platform for eCommerce sellers. Connect, manage, and automate your Turn14 and eBay stores effortlessly — all in one place.",
), ),
const InfoCard(
InfoCard(
emoji: "⚙️", emoji: "⚙️",
heading: "About Turn14 Integration", heading: "About Turn14 Integration",
content: content:
"Sync your Turn14 account to automatically import products, pricing, and stock updates. Keep your online store current without manual effort.", "Sync your Turn14 account to automatically import products, pricing, and stock updates. Keep your online store current without manual effort.",
), ),
const InfoCard(
InfoCard(
emoji: "🛒", emoji: "🛒",
heading: "About eBay Integration", heading: "About eBay Integration",
content: content:
"Simplify eBay listing management, stock updates, and order automation directly from your dashboard.", "Simplify eBay listing management, stock updates, and order automation directly from your dashboard.",
), ),
const InfoCard(
InfoCard(
emoji: "💰", emoji: "💰",
heading: "Pricing & Plans", heading: "Pricing & Plans",
content: content:
"Explore flexible pricing options for every business size. Upgrade anytime to unlock advanced features.", "Explore flexible pricing options for every business size. Upgrade anytime to unlock advanced features.",
), ),
user.when( // USER DETAILS CARD (FIXED)
loading: () => const CircularProgressIndicator(), userAsync.when(
error: (err, _) => Text("Error: $err"), loading: () => const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
),
error: (err, _) => Padding(
padding: const EdgeInsets.all(16),
child: Text("Error loading user: $err"),
),
data: (user) { data: (user) {
if (user == null) return SizedBox(); if (user == null) {
return const Padding(
padding: EdgeInsets.all(16),
child: Text("No user data available"),
);
}
return UserDetailsCard( return UserDetailsCard(
name: user.name, name: user.name,
@ -137,8 +132,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
], ],
), ),
), ),
// 🍔 Common Hamburger Button
HamburgerButton(scaffoldKey: _scaffoldKey), HamburgerButton(scaffoldKey: _scaffoldKey),
], ],
), ),
@ -146,9 +139,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
} }
} }
// ------------------------------------------------------------ // ----------------------- INFO CARD -----------------------
// REUSABLE INFO CARD
// ------------------------------------------------------------
class InfoCard extends StatefulWidget { class InfoCard extends StatefulWidget {
final String emoji; final String emoji;
final String heading; final String heading;
@ -195,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(
@ -202,7 +194,6 @@ class _InfoCardState extends State<InfoCard> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header
Row( Row(
children: [ children: [
Text(widget.emoji, style: const TextStyle(fontSize: 26)), Text(widget.emoji, style: const TextStyle(fontSize: 26)),
@ -224,8 +215,6 @@ class _InfoCardState extends State<InfoCard> {
style: const TextStyle(fontSize: 15, height: 1.4), style: const TextStyle(fontSize: 15, height: 1.4),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
// YouTube Player
if (_controller != null) if (_controller != null)
!_showPlayer !_showPlayer
? GestureDetector( ? GestureDetector(
@ -233,7 +222,6 @@ class _InfoCardState extends State<InfoCard> {
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
// Thumbnail
Container( Container(
height: 180, height: 180,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -246,7 +234,6 @@ class _InfoCardState extends State<InfoCard> {
), ),
), ),
), ),
// Play button overlay
Container( Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
@ -276,6 +263,7 @@ class _InfoCardState extends State<InfoCard> {
} }
} }
// ----------------------- USER DETAILS CARD -----------------------
class UserDetailsCard extends StatelessWidget { class UserDetailsCard extends StatelessWidget {
final String name; final String name;
final String email; final String email;
@ -294,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)),
@ -307,13 +296,14 @@ class UserDetailsCard extends StatelessWidget {
Text("👤", style: TextStyle(fontSize: 26)), Text("👤", style: TextStyle(fontSize: 26)),
SizedBox(width: 10), SizedBox(width: 10),
Expanded( Expanded(
child: Text("User Details", style: AppTypo.sectionHeader), child: Text(
"User Details",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
), ),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_detailRow("Name", name), _detailRow("Name", name),
_detailRow("Email", email), _detailRow("Email", email),
_detailRow("Phone", phone), _detailRow("Phone", phone),
@ -327,34 +317,33 @@ class UserDetailsCard extends StatelessWidget {
Widget _detailRow(String label, String value) { Widget _detailRow(String label, String value) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.only(bottom: 6),
child: RichText( child: Row(
text: TextSpan( crossAxisAlignment: CrossAxisAlignment.start,
style: const TextStyle(
fontSize: 15,
color: Colors.black87,
height: 1.4,
),
children: [ children: [
TextSpan( SizedBox(
text: "$label: ", width: 60, // FIXED WIDTH FOR LEFT LABEL
child: Text(
"$label:",
style: const TextStyle( style: const TextStyle(
fontSize: 15, fontSize: 15,
color: Colors.black87, color: Colors.black87,
height: 1.4,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
height: 1.4,
), ),
), ),
TextSpan( ),
text: value, Expanded(
child: Text(
value,
style: const TextStyle( style: const TextStyle(
fontSize: 15, fontSize: 15,
color: Colors.black87, color: Colors.black87,
height: 1.4, height: 1.4,
), ),
), ),
),
], ],
), ),
),
); );
} }
} }

View File

@ -0,0 +1,122 @@
import 'package:autos/core/theme/app_typography.dart';
import 'package:flutter/material.dart';
import 'package:autos/core/widgets/hamburger_button.dart';
import 'package:autos/core/widgets/side_menu.dart';
class EbayScreen extends StatefulWidget {
const EbayScreen({super.key});
@override
State<EbayScreen> createState() => _EbayScreenState();
}
class _EbayScreenState extends State<EbayScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String selected = "ebay";
@override
Widget build(BuildContext context) {
final double topPadding = MediaQuery.of(context).padding.top + 16;
return Scaffold(
key: _scaffoldKey,
drawer: SideMenu(
selected: selected,
onItemSelected: (key) {
setState(() => selected = key);
},
),
// backgroundColor: const Color(0xFFEFFAFF),
body: Stack(
children: [
/// TITLE
Positioned(
top: topPadding,
left: 0,
right: 0,
child: Center(
child: Text(
"eBay Settings",
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
),
),
),
/// MAIN BOX UI
Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.90,
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 40),
margin: EdgeInsets.only(top: topPadding + 60),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 25,
offset: const Offset(0, 12),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
/// Description
Text(
"Connect your eBay store to enable product sync, inventory updates, and order flow.",
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 15,
color: Colors.black54,
height: 1.5,
),
),
const SizedBox(height: 30),
/// BUTTON
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// TODO: Add eBay authorization flow
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00CFFF),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
"Connect your eBay store",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
),
const SizedBox(height: 20),
Text(
"You'll be redirected to eBay to authorize access, then returned here.",
style: const TextStyle(fontSize: 13, color: Colors.black45),
textAlign: TextAlign.center,
),
],
),
),
),
/// HAMBURGER BUTTON
HamburgerButton(scaffoldKey: _scaffoldKey),
],
),
);
}
}

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

@ -0,0 +1,310 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:autos/core/widgets/hamburger_button.dart';
import 'package:autos/core/widgets/side_menu.dart';
import 'package:autos/core/theme/app_typography.dart';
import 'package:autos/presentation/providers/user_provider.dart';
class AccountScreen extends ConsumerStatefulWidget {
const AccountScreen({super.key});
@override
ConsumerState<AccountScreen> createState() => _AccountScreenState();
}
class _AccountScreenState extends ConsumerState<AccountScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String selected = 'account';
@override
Widget build(BuildContext context) {
final double topPadding = MediaQuery.of(context).padding.top + 16;
final userAsync = ref.watch(userProvider); // FIXED
return Scaffold(
key: _scaffoldKey,
drawer: SideMenu(
selected: selected,
onItemSelected: (key) => setState(() => selected = key),
),
backgroundColor: const Color(0xFFF6FDFF),
body: Stack(
children: [
/// PAGE TITLE
Positioned(
top: topPadding,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"My Account",
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
),
],
),
),
/// MAIN CONTENT
SingleChildScrollView(
padding: EdgeInsets.fromLTRB(16, topPadding + 70, 16, 20),
child: Column(
children: [
_subscriptionCard(),
const SizedBox(height: 16),
_billingCard(),
const SizedBox(height: 16),
/// PROFILE CARD (FIXED)
userAsync.when(
loading: () => const Padding(
padding: EdgeInsets.all(20),
child: CircularProgressIndicator(),
),
error: (e, _) => _baseCard(
title: "Profile",
child: Text("Error loading user: $e"),
),
data: (user) {
if (user == null) {
return _baseCard(
title: "Profile",
child: const Text("No user data available."),
);
}
return _baseCard(
title: "Profile",
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_row("Full Name", user.name),
_row("Email", user.email),
_row("Phone", user.phoneNumber ?? "-"),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {},
child: const Text("Update Details"),
),
),
const SizedBox(width: 10),
Expanded(
child: OutlinedButton(
onPressed: () {},
child: const Text("Change Password"),
),
),
],
),
],
),
);
},
),
const SizedBox(height: 28),
_billingHistoryCard(),
const SizedBox(height: 40),
const Divider(thickness: 0.6),
const SizedBox(height: 12),
const Text(
"© 2025. Data4Autos. All rights reserved.",
style: TextStyle(
fontSize: 13,
color: Colors.black45,
fontWeight: FontWeight.w500,
),
),
],
),
),
HamburgerButton(scaffoldKey: _scaffoldKey),
],
),
);
}
// ------------------------------------------------------------
// SUBSCRIPTION CARD
// ------------------------------------------------------------
Widget _subscriptionCard() {
return _baseCard(
title: "Subscription",
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_row("Subscription", "growth_monthly"),
_row("Period", "24 Nov 2025 - 24 Dec 2025"),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(
0xFFFF4C4C,
), // override global primary color
foregroundColor: Colors.white, // text color
elevation: 6,
shadowColor: Colors.black.withValues(alpha: 0.2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 14),
textStyle: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: const Text("Cancel Subscription"),
),
),
],
),
);
}
// ------------------------------------------------------------
// BILLING CARD
// ------------------------------------------------------------
Widget _billingCard() {
return _baseCard(
title: "Billing",
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_row("Card", "•••• •••• •••• 1325 VISA"),
_row("Expires", "3 / 2028"),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {},
child: const Text("Update Billing"),
),
),
const SizedBox(width: 10),
Expanded(
child: OutlinedButton(
onPressed: () {},
child: const Text("Add Coupon"),
),
),
],
),
],
),
);
}
// ------------------------------------------------------------
// BILLING HISTORY
// ------------------------------------------------------------
Widget _billingHistoryCard() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: _cardDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Billing History",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
_tableRow(
isHeader: true,
cells: ["Invoice", "Date", "Status", "Amount", "PDF"],
),
_tableRow(
cells: ["INV-0001", "24 Nov 2025", "Paid", "\$99.00", "View"],
),
const SizedBox(height: 14),
const Text(
"Showing 1 of 1 entries",
style: TextStyle(color: Colors.black54),
),
],
),
);
}
// ------------------------------------------------------------
// HELPERS
// ------------------------------------------------------------
Widget _baseCard({required String title, required Widget child}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: _cardDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
child,
],
),
);
}
BoxDecoration _cardDecoration() {
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
blurRadius: 12,
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 6),
),
],
);
}
Widget _row(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(color: Colors.black54)),
Text(value, style: const TextStyle(fontWeight: FontWeight.w600)),
],
),
);
}
Widget _tableRow({bool isHeader = false, required List<String> cells}) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isHeader ? const Color(0xFFF5F8FA) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: cells.map((e) {
return Expanded(
child: Text(
e,
style: TextStyle(
fontWeight: isHeader ? FontWeight.bold : FontWeight.normal,
color: e == "Paid" ? Colors.green : Colors.black,
),
),
);
}).toList(),
),
);
}
}

View File

@ -0,0 +1,323 @@
import 'package:autos/core/widgets/hamburger_button.dart';
import 'package:autos/core/widgets/side_menu.dart';
import 'package:flutter/material.dart';
import 'package:autos/core/theme/app_typography.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PricingScreen extends ConsumerStatefulWidget {
const PricingScreen({super.key});
@override
ConsumerState<PricingScreen> createState() => _PricingScreenState();
}
class _PricingScreenState extends ConsumerState<PricingScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String selected = 'pricing';
bool isMonthly = true;
@override
Widget build(BuildContext context) {
final double topPadding = MediaQuery.of(context).padding.top + 16;
return Scaffold(
key: _scaffoldKey,
drawer: SideMenu(
selected: selected,
onItemSelected: (key) {
setState(() => selected = key);
},
),
backgroundColor: const Color(0xFFF6FDFF),
body: Stack(
children: [
// Top centered title (same style as your dashboard)
Positioned(
top: topPadding,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Choose Your Plan",
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
),
],
),
),
// Main content
SingleChildScrollView(
padding: EdgeInsets.fromLTRB(16, topPadding + 70, 16, 20),
child: Column(
children: [
const Text(
"🚀 Subscribe to a plan and start automating your eBay listings instantly.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15, color: Colors.black54),
),
const SizedBox(height: 22),
// Monthly / Yearly toggle
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
border: Border.all(color: const Color(0xFF00BFFF)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_toggleButton("Monthly", isMonthly, () {
setState(() => isMonthly = true);
}),
_toggleButton("Yearly (Save 15%)", !isMonthly, () {
setState(() => isMonthly = false);
}),
],
),
),
const SizedBox(height: 30),
// Pricing cards
Column(
children: [
_pricingCard(
title: "Starter Sync",
subtitle: "Upload up to 100 products per month",
price: isMonthly ? "\$49" : "\$499",
features: const [
"Auto price & inventory updates",
"Daily sync",
"Manual sync option",
"Basic reporting dashboard",
],
isPopular: false,
),
const SizedBox(height: 20),
_pricingCard(
title: "Growth Sync",
subtitle: "Upload up to 250 products per month",
price: isMonthly ? "\$99" : "\$999",
features: const [
"Everything in Starter",
"3-hour sync interval",
"Bulk product import",
"Priority email support",
],
isPopular: true,
),
const SizedBox(height: 20),
_pricingCard(
title: "Pro Sync",
subtitle: "Upload up to 1000 products per month",
price: isMonthly ? "\$249" : "\$2499",
features: const [
"Everything in Growth",
"Real-time sync",
"Advanced analytics dashboard",
"Dedicated account manager",
"API access",
],
isPopular: false,
),
],
),
const SizedBox(height: 40),
const Divider(thickness: 0.6),
const SizedBox(height: 12),
const Text(
"© 2025. Data4Autos. All rights reserved.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: Colors.black45,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 20),
],
),
),
// Hamburger button (keeps same behavior as other screens)
HamburgerButton(scaffoldKey: _scaffoldKey),
],
),
);
}
/// Toggle button
Widget _toggleButton(String text, bool active, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: active ? const Color(0xFF00BFFF) : Colors.transparent,
borderRadius: BorderRadius.circular(30),
),
child: Text(
text,
style: TextStyle(
color: active ? Colors.white : const Color(0xFF00BFFF),
fontWeight: FontWeight.w600,
),
),
),
);
}
/// Pricing card widget
Widget _pricingCard({
required String title,
required String subtitle,
required String price,
required List<String> features,
required bool isPopular,
}) {
return Stack(
alignment: Alignment.topCenter,
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.only(top: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isPopular ? const Color(0xFF00BFFF) : Colors.transparent,
width: 2,
),
boxShadow: [
BoxShadow(
blurRadius: 12,
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 6),
),
],
),
child: Column(
children: [
const SizedBox(height: 10),
Text(
title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
subtitle,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.black54),
),
const SizedBox(height: 18),
RichText(
text: TextSpan(
children: [
TextSpan(
text: price,
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const TextSpan(
text: " / month",
style: TextStyle(color: Colors.black54),
),
],
),
),
const SizedBox(height: 20),
Column(
children: features
.map(
(e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
const Icon(
Icons.check_circle,
color: Color(0xFF00BFFF),
size: 18,
),
const SizedBox(width: 10),
Expanded(child: Text(e)),
],
),
),
)
.toList(),
),
const SizedBox(height: 25),
// Subscribe button
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: isPopular
? const Color(0xFF00BFFF)
: Colors.white,
foregroundColor: isPopular
? Colors.white
: const Color(0xFF00BFFF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: Color(0xFF00BFFF)),
),
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: isPopular ? 6 : 0,
),
onPressed: () {},
child: Text(
isMonthly ? "Subscribe Monthly" : "Subscribe Yearly",
),
),
),
],
),
),
// Most popular badge
if (isPopular)
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF00BFFF),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
"MOST POPULAR",
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
],
);
}
}

View File

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

View File

@ -0,0 +1,237 @@
import 'package:autos/core/theme/app_theme.dart';
import 'package:flutter/material.dart';
class CreateLocationScreen extends StatefulWidget {
const CreateLocationScreen({super.key});
@override
State<CreateLocationScreen> createState() => _CreateLocationScreenState();
}
class _CreateLocationScreenState extends State<CreateLocationScreen> {
// Controllers
final TextEditingController storeName = TextEditingController();
final TextEditingController phone = TextEditingController();
final TextEditingController address1 = TextEditingController();
final TextEditingController city = TextEditingController();
final TextEditingController stateCtrl = TextEditingController();
final TextEditingController postalCode = TextEditingController();
final TextEditingController country = TextEditingController();
final TextEditingController timeZone = TextEditingController(
text: "America/New_York",
);
TimeOfDay openTime = const TimeOfDay(hour: 9, minute: 0);
TimeOfDay closeTime = const TimeOfDay(hour: 18, minute: 0);
Future<void> pickTime({required bool isOpen}) async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: isOpen ? openTime : closeTime,
);
if (picked != null) {
setState(() {
if (isOpen)
openTime = picked;
else
closeTime = picked;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFEFFAFF),
appBar: AppBar(
title: const Text(
"Create New Location",
style: TextStyle(color: Colors.white),
),
backgroundColor: AppTheme.primary,
elevation: 0,
foregroundColor: Colors.black,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Container(
padding: const EdgeInsets.all(30),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_input("Store Name *", "Enter store name", storeName),
_input("Phone *", "Enter phone number", phone),
_input("Address Line 1 *", "Enter address", address1),
Row(
children: [
Expanded(child: _input("City *", "City", city)),
const SizedBox(width: 12),
Expanded(child: _input("State *", "State", stateCtrl)),
],
),
Row(
children: [
Expanded(
child: _input("Postal Code *", "Postal Code", postalCode),
),
const SizedBox(width: 12),
Expanded(
child: _input(
"Country *",
"Country Code (e.g. US)",
country,
),
),
],
),
_input("Time Zone *", "America/New_York", timeZone),
const SizedBox(height: 15),
Row(
children: [
Expanded(
child: _timePicker(
label: "Open Time *",
time: openTime,
onTap: () => pickTime(isOpen: true),
),
),
const SizedBox(width: 12),
Expanded(
child: _timePicker(
label: "Close Time *",
time: closeTime,
onTap: () => pickTime(isOpen: false),
),
),
],
),
const SizedBox(height: 25),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
final json = {
"store_name": storeName.text,
"phone": phone.text,
"address1": address1.text,
"city": city.text,
"state": stateCtrl.text,
"postal_code": postalCode.text,
"country": country.text,
"timezone": timeZone.text,
"open_time": openTime.format(context),
"close_time": closeTime.format(context),
};
print("📦 LOCATION JSON → $json");
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00CFFF),
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
"Save & Console JSON",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
),
],
),
),
),
);
}
// Reusable TextField
Widget _input(String label, String hint, TextEditingController controller) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
const SizedBox(height: 8),
TextField(
controller: controller,
decoration: InputDecoration(
hintText: hint,
filled: true,
fillColor: const Color(0xFFF0F6FF),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
),
],
),
);
}
// Time Picker Widget
Widget _timePicker({
required String label,
required TimeOfDay time,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 18),
decoration: BoxDecoration(
color: const Color(0xFFF0F6FF),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Text(
time.format(context),
style: const TextStyle(fontSize: 16),
),
const Spacer(),
const Icon(Icons.access_time, size: 20),
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,132 @@
import 'package:autos/core/routing/route_paths.dart';
import 'package:autos/core/theme/app_typography.dart';
import 'package:flutter/material.dart';
import 'package:autos/core/widgets/hamburger_button.dart';
import 'package:autos/core/widgets/side_menu.dart';
class StoreScreen extends StatefulWidget {
const StoreScreen({super.key});
@override
State<StoreScreen> createState() => _StoreScreenState();
}
class _StoreScreenState extends State<StoreScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String selected = "ebay";
@override
Widget build(BuildContext context) {
final double topPadding = MediaQuery.of(context).padding.top + 16;
return Scaffold(
key: _scaffoldKey,
drawer: SideMenu(
selected: selected,
onItemSelected: (key) {
setState(() => selected = key);
},
),
body: Stack(
children: [
/// TITLE
Positioned(
top: topPadding,
left: 0,
right: 0,
child: Center(
child: Text(
"eBay Locations",
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
),
),
),
/// MAIN WITH BUTTONS
Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.90,
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 40),
margin: EdgeInsets.only(top: topPadding + 60),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 25,
offset: const Offset(0, 12),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
/// SAVE SELECTED BUTTON
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// TODO: Save Selected Flow
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00CFFF),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
"Save Selected",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
),
const SizedBox(height: 20),
/// NEW LOCATION BUTTON
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.pushNamed(
context,
AppRoutePaths.createStoreLocation,
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00CFFF),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
"Create New Location",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
),
],
),
),
),
/// HAMBURGER BUTTON
HamburgerButton(scaffoldKey: _scaffoldKey),
],
),
);
}
}

View File

@ -2,8 +2,10 @@ import 'package:autos/core/theme/app_typography.dart';
import 'package:autos/core/widgets/hamburger_button.dart'; import 'package:autos/core/widgets/hamburger_button.dart';
import 'package:autos/core/widgets/side_menu.dart'; import 'package:autos/core/widgets/side_menu.dart';
import 'package:autos/presentation/providers/user_provider.dart'; import 'package:autos/presentation/providers/user_provider.dart';
import 'package:autos/presentation/providers/turn14_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
class Turn14Screen extends ConsumerStatefulWidget { class Turn14Screen extends ConsumerStatefulWidget {
const Turn14Screen({super.key}); const Turn14Screen({super.key});
@ -16,9 +18,16 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String selected = 'turn14'; String selected = 'turn14';
// controllers
final TextEditingController clientIdController = TextEditingController();
final TextEditingController clientSecretController = TextEditingController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = ref.watch(userDetailsProvider); final asyncUser = ref.watch(userProvider);
final turn14State = ref.watch(turn14Provider);
final user = asyncUser.value;
final double topPadding = MediaQuery.of(context).padding.top + 16; final double topPadding = MediaQuery.of(context).padding.top + 16;
return Scaffold( return Scaffold(
@ -46,13 +55,13 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
), ),
), ),
/// Main Scrollable UI
SingleChildScrollView( SingleChildScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20), padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Description Row
Row( Row(
children: [ children: [
Text("", style: const TextStyle(fontSize: 28)), Text("", style: const TextStyle(fontSize: 28)),
@ -70,21 +79,16 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Client ID Input _inputField(label: "Client ID", controller: clientIdController),
_inputField(
label: "Client ID",
controller: TextEditingController(),
),
const SizedBox(height: 20), const SizedBox(height: 20),
// Secret Key Input With Eye Icon
_passwordField( _passwordField(
label: "Secret Key", label: "Secret Key",
controller: TextEditingController(), controller: clientSecretController,
), ),
const SizedBox(height: 25), const SizedBox(height: 25),
// Save Button /// SAVE BUTTON
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
@ -95,8 +99,55 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
), ),
backgroundColor: const Color(0xFF00C9FF), backgroundColor: const Color(0xFF00C9FF),
), ),
onPressed: () {}, onPressed: turn14State.isLoading
child: const Text( ? null
: () async {
if (user == null) {
Fluttertoast.showToast(
msg: "⚠️ User not found",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
fontSize: 16.0,
);
return;
}
if (clientIdController.text.trim().isEmpty ||
clientSecretController.text.trim().isEmpty) {
Fluttertoast.showToast(
msg: "⚠️ Please fill all fields",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.orange,
textColor: Colors.white,
fontSize: 16.0,
);
return;
}
await ref
.read(turn14Provider.notifier)
.saveCredentials(
userId: user.id,
clientId: clientIdController.text.trim(),
clientSecret:
clientSecretController.text.trim(),
);
Fluttertoast.showToast(
msg: "✅ Turn14 Credentials Saved Successfully",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 16.0,
);
},
child: turn14State.isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text(
"Save Credentials", "Save Credentials",
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
@ -106,54 +157,12 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
), ),
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Info Box _infoBox(),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFE8F1FF),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: const [
Icon(Icons.info, color: Colors.blue),
SizedBox(width: 10),
Text(
"No credentials saved yet.",
style: TextStyle(color: Colors.black87),
),
],
),
),
const SizedBox(height: 20), const SizedBox(height: 20),
_tipsBox(),
// Tips Box
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFE8FCFF),
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"💡 Connection Tips",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
SizedBox(height: 10),
Text("• Ensure your credentials are valid and active."),
Text("• Credentials are encrypted before saving."),
Text("• Contact Turn14 support for API setup help."),
],
),
),
], ],
), ),
), ),
@ -164,6 +173,7 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
); );
} }
/// UI COMPONENTS
Widget _inputField({ Widget _inputField({
required String label, required String label,
required TextEditingController controller, required TextEditingController controller,
@ -224,4 +234,49 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
}, },
); );
} }
Widget _infoBox() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFE8F1FF),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: const [
Icon(Icons.info, color: Colors.blue),
SizedBox(width: 10),
Text(
"No credentials saved yet.",
style: TextStyle(color: Colors.black87),
),
],
),
);
}
Widget _tipsBox() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFE8FCFF),
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"💡 Connection Tips",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
SizedBox(height: 10),
Text("• Ensure your credentials are valid and active."),
Text("• Credentials are encrypted before saving."),
Text("• Contact Turn14 support for API setup help."),
],
),
);
}
} }

View File

@ -17,6 +17,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.7.1" version: "7.7.1"
archive:
dependency: transitive
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
url: "https://pub.dev"
source: hosted
version: "4.0.7"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -49,6 +57,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_config: cli_config:
dependency: transitive dependency: transitive
description: description:
@ -57,6 +73,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -214,6 +238,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.6.0"
flutter_launcher_icons:
dependency: "direct main"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -328,6 +360,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
io: io:
dependency: transitive dependency: transitive
description: description:
@ -344,6 +384,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.7" version: "0.6.7"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -424,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:
@ -488,6 +544,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -512,6 +576,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.2" version: "1.5.2"
posix:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -733,6 +805,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:

View File

@ -20,6 +20,8 @@ dependencies:
fluttertoast: ^9.0.0 fluttertoast: ^9.0.0
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
onesignal_flutter: ^5.3.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -49,3 +51,10 @@ flutter:
weight: 600 weight: 600
- asset: assets/fonts/Nunito-Bold.ttf - asset: assets/fonts/Nunito-Bold.ttf
weight: 700 weight: 700
flutter_launcher_icons:
android: true
ios: true
image_path: "assets/auth/autos_transp.png"
adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/auth/autos_transp.png"