Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6f2b15453 | |||
| 064e753fb9 | |||
| fe49bdf9af | |||
| 6e9ffdb9dd | |||
| a978699dfc | |||
| 595554d541 | |||
| 41970033ad | |||
| dcc8a8ed92 | |||
| 33c15b1a5f |
@ -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
|
||||||
|
|||||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 175 KiB |
@ -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>
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 34 KiB |
4
android/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
@ -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++";
|
||||||
|
|||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 26 KiB |
@ -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/';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
28
lib/core/routing/navigation_service.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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(
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
107
lib/data/models/brands_model.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/data/models/import_product_model.dart
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import 'package:autos/domain/entities/import_product.dart';
|
||||||
|
|
||||||
|
class ImportProductModel {
|
||||||
|
final String id;
|
||||||
|
final String sku;
|
||||||
|
final String image;
|
||||||
|
final String name;
|
||||||
|
final String partNumber;
|
||||||
|
final String category;
|
||||||
|
final String subcategory;
|
||||||
|
final double price;
|
||||||
|
final int inventory;
|
||||||
|
final String offerStatus;
|
||||||
|
|
||||||
|
ImportProductModel({
|
||||||
|
required this.id,
|
||||||
|
required this.sku,
|
||||||
|
required this.image,
|
||||||
|
required this.name,
|
||||||
|
required this.partNumber,
|
||||||
|
required this.category,
|
||||||
|
required this.subcategory,
|
||||||
|
required this.price,
|
||||||
|
required this.inventory,
|
||||||
|
required this.offerStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ImportProductModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ImportProductModel(
|
||||||
|
id: json['id']?.toString() ?? '',
|
||||||
|
sku: json['sku'] ?? '',
|
||||||
|
image: json['imgSrc'] ?? '',
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
partNumber: json['partNumber'] ?? '',
|
||||||
|
category: json['category'] ?? '',
|
||||||
|
subcategory: json['subcategory'] ?? '',
|
||||||
|
price: double.tryParse(json['price'].toString()) ?? 0,
|
||||||
|
inventory: json['inventory'] ?? 0,
|
||||||
|
offerStatus: json['offer_status'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportProductEntity toEntity() {
|
||||||
|
return ImportProductEntity(
|
||||||
|
id: id,
|
||||||
|
sku: sku,
|
||||||
|
image: image,
|
||||||
|
name: name,
|
||||||
|
partNumber: partNumber,
|
||||||
|
category: category,
|
||||||
|
subcategory: subcategory,
|
||||||
|
price: price,
|
||||||
|
inventory: inventory,
|
||||||
|
offerStatus: offerStatus,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
lib/data/models/product_model.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
lib/data/models/turn14_model.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
127
lib/data/repositories/brands_repository_impl.dart
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
147
lib/data/repositories/imports_repository_impl.dart
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:autos/core/constants/api_endpoints.dart';
|
||||||
|
import 'package:autos/data/models/import_product_model.dart';
|
||||||
|
import 'package:autos/data/sources/remote/api_service.dart';
|
||||||
|
import 'package:autos/domain/entities/import_product.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:autos/data/models/user_model.dart';
|
||||||
|
|
||||||
|
abstract class ImportsRepository {
|
||||||
|
Future<List<ImportProductEntity>> getUserProducts({
|
||||||
|
required int page,
|
||||||
|
required int pageSize,
|
||||||
|
String? category,
|
||||||
|
String? subcategory,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImportsRepositoryImpl implements ImportsRepository {
|
||||||
|
final ApiService _apiService;
|
||||||
|
final _storage = const FlutterSecureStorage();
|
||||||
|
static const _userKey = 'logged_in_user';
|
||||||
|
|
||||||
|
ImportsRepositoryImpl(this._apiService);
|
||||||
|
|
||||||
|
/// ✅ TOKEN LOGIC (UNCHANGED)
|
||||||
|
Future<String> getTurn14AccessToken() async {
|
||||||
|
final jsonString = await _storage.read(key: _userKey);
|
||||||
|
if (jsonString == null) {
|
||||||
|
throw Exception("User not found. Please login again.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = UserModel.fromJson(jsonDecode(jsonString));
|
||||||
|
|
||||||
|
final response = await _apiService.post(
|
||||||
|
ApiEndpoints.getToken,
|
||||||
|
{"userid": user.id},
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = response.data;
|
||||||
|
String token;
|
||||||
|
|
||||||
|
if (data["code"] == "TOKEN_UPDATED") {
|
||||||
|
token = data["access_token"];
|
||||||
|
await _storage.write(key: 'turn14_token', value: token);
|
||||||
|
} else if (data["code"] == "TOKEN_VALID") {
|
||||||
|
token = data["access_token"];
|
||||||
|
await _storage.write(key: 'turn14_token', value: token);
|
||||||
|
token = await _storage.read(key: 'turn14_token') ?? '';
|
||||||
|
if (token.isEmpty) {
|
||||||
|
throw Exception("Token missing in storage");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception(data["message"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("✅ Turn14 Token: $token");
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅✅✅ FINAL: FETCH USER PRODUCTS WITH PAGINATION + FILTERS
|
||||||
|
@override
|
||||||
|
Future<List<ImportProductEntity>> getUserProducts({
|
||||||
|
required int page,
|
||||||
|
required int pageSize,
|
||||||
|
String? category,
|
||||||
|
String? subcategory,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final token = await getTurn14AccessToken();
|
||||||
|
final userId = await _getUserId();
|
||||||
|
|
||||||
|
/// ✅ CLEAN PARAMS (SKIPS "All")
|
||||||
|
final Map<String, dynamic> params = {
|
||||||
|
"userid": userId,
|
||||||
|
"page": page,
|
||||||
|
"pageSize": pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (category != null && category.isNotEmpty && category != "All") {
|
||||||
|
params["category"] = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcategory != null &&
|
||||||
|
subcategory.isNotEmpty &&
|
||||||
|
subcategory != "All") {
|
||||||
|
params["subcategory"] = subcategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("📡 FETCH USER PRODUCTS");
|
||||||
|
debugPrint("➡️ PARAMS: $params");
|
||||||
|
|
||||||
|
final response = await _apiService.get(
|
||||||
|
"/api/user-products",
|
||||||
|
overrideBaseUrl: "https://ebay.backend.data4autos.com",
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
params: params,
|
||||||
|
);
|
||||||
|
|
||||||
|
debugPrint("✅ API STATUS: ${response.statusCode}");
|
||||||
|
debugPrint("✅ RAW RESPONSE: ${response.data}");
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("Server error: ${response.statusCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = response.data;
|
||||||
|
|
||||||
|
/// ✅ API RETURNS `items`
|
||||||
|
if (data is! Map || data["items"] == null) {
|
||||||
|
throw Exception("Invalid API structure: 'items' key missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
final List list = data["items"];
|
||||||
|
|
||||||
|
debugPrint("✅ TOTAL ITEMS LOADED: ${list.length}");
|
||||||
|
|
||||||
|
return list
|
||||||
|
.map(
|
||||||
|
(json) => ImportProductModel.fromJson(json).toEntity(),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
debugPrint("❌ NETWORK ERROR: ${e.response?.data ?? e.message}");
|
||||||
|
throw Exception("Network error: ${e.response?.data ?? e.message}");
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint("❌ PARSING ERROR: $e");
|
||||||
|
debugPrintStack(stackTrace: st);
|
||||||
|
throw Exception("Unexpected error: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ GET USER ID FROM STORAGE
|
||||||
|
Future<String> _getUserId() async {
|
||||||
|
final jsonString = await _storage.read(key: _userKey);
|
||||||
|
if (jsonString == null) {
|
||||||
|
throw Exception("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = UserModel.fromJson(jsonDecode(jsonString));
|
||||||
|
return user.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
65
lib/data/repositories/product_repository.dart
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/data/repositories/token_repository.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
77
lib/data/repositories/turn14_repository_impl.dart
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
lib/domain/entities/brands.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
25
lib/domain/entities/import_product.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
class ImportProductEntity {
|
||||||
|
final String id;
|
||||||
|
final String sku;
|
||||||
|
final String image;
|
||||||
|
final String name;
|
||||||
|
final String partNumber;
|
||||||
|
final String category;
|
||||||
|
final String subcategory;
|
||||||
|
final double price;
|
||||||
|
final int inventory;
|
||||||
|
final String offerStatus;
|
||||||
|
|
||||||
|
ImportProductEntity({
|
||||||
|
required this.id,
|
||||||
|
required this.sku,
|
||||||
|
required this.image,
|
||||||
|
required this.name,
|
||||||
|
required this.partNumber,
|
||||||
|
required this.category,
|
||||||
|
required this.subcategory,
|
||||||
|
required this.price,
|
||||||
|
required this.inventory,
|
||||||
|
required this.offerStatus,
|
||||||
|
});
|
||||||
|
}
|
||||||
13
lib/domain/entities/product.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
31
lib/domain/entities/turn14.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
79
lib/presentation/providers/brand_provider.dart
Normal 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);
|
||||||
|
},
|
||||||
|
);
|
||||||
84
lib/presentation/providers/imports_provider.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'package:autos/data/repositories/imports_repository_impl.dart';
|
||||||
|
import 'package:autos/domain/entities/import_product.dart';
|
||||||
|
import 'package:autos/presentation/providers/user_provider.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_riverpod/legacy.dart';
|
||||||
|
|
||||||
|
/// ✅ Repository Provider
|
||||||
|
final importsRepositoryProvider = Provider<ImportsRepository>(
|
||||||
|
(ref) => ImportsRepositoryImpl(ref.read(apiServiceProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// ✅ Main Imports Provider
|
||||||
|
final importsProvider = StateNotifierProvider<
|
||||||
|
ImportsNotifier, AsyncValue<List<ImportProductEntity>>>(
|
||||||
|
(ref) => ImportsNotifier(ref.read(importsRepositoryProvider)),
|
||||||
|
);
|
||||||
|
class ImportsNotifier
|
||||||
|
extends StateNotifier<AsyncValue<List<ImportProductEntity>>> {
|
||||||
|
final ImportsRepository _repo;
|
||||||
|
|
||||||
|
ImportsNotifier(this._repo) : super(const AsyncLoading());
|
||||||
|
|
||||||
|
/// ✅ STATE
|
||||||
|
int _page = 1;
|
||||||
|
final int _pageSize = 48;
|
||||||
|
|
||||||
|
String _category = "All";
|
||||||
|
String _subcategory = "All";
|
||||||
|
|
||||||
|
int get currentPage => _page;
|
||||||
|
|
||||||
|
/// ✅ LOAD PRODUCTS FROM API
|
||||||
|
Future<void> loadProducts({bool resetPage = false}) async {
|
||||||
|
try {
|
||||||
|
if (resetPage) _page = 1;
|
||||||
|
|
||||||
|
state = const AsyncLoading();
|
||||||
|
|
||||||
|
debugPrint("📡 Fetching products");
|
||||||
|
debugPrint("➡ Page: $_page");
|
||||||
|
debugPrint("➡ Category: $_category");
|
||||||
|
debugPrint("➡ SubCategory: $_subcategory");
|
||||||
|
|
||||||
|
final products = await _repo.getUserProducts(
|
||||||
|
page: _page,
|
||||||
|
pageSize: _pageSize,
|
||||||
|
category: _category == "All" ? null : _category,
|
||||||
|
subcategory: _subcategory == "All" ? null : _subcategory,
|
||||||
|
);
|
||||||
|
|
||||||
|
state = AsyncData(products);
|
||||||
|
} catch (e, st) {
|
||||||
|
state = AsyncError(e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ CATEGORY CHANGE (RESETS SUBCATEGORY AUTOMATICALLY)
|
||||||
|
void changeCategory(String value) {
|
||||||
|
_category = value;
|
||||||
|
_subcategory = "All"; // ✅ RESET HERE
|
||||||
|
loadProducts(resetPage: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ SUBCATEGORY CHANGE
|
||||||
|
void changeSubcategory(String value) {
|
||||||
|
_subcategory = value;
|
||||||
|
loadProducts(resetPage: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ NEXT PAGE
|
||||||
|
void nextPage() {
|
||||||
|
_page++;
|
||||||
|
loadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ PREVIOUS PAGE
|
||||||
|
void prevPage() {
|
||||||
|
if (_page > 1) {
|
||||||
|
_page--;
|
||||||
|
loadProducts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lib/presentation/providers/products_provider.dart
Normal 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([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
lib/presentation/providers/token_provider.dart
Normal 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)),
|
||||||
|
);
|
||||||
92
lib/presentation/providers/turn14_provider.dart
Normal 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);
|
||||||
|
});
|
||||||
@ -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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
367
lib/presentation/screens/brands/brands_screen.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
122
lib/presentation/screens/ebay/ebay_screen.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
245
lib/presentation/screens/imports/imports_header.dart
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import 'package:autos/core/theme/app_typography.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ImportsHeader extends StatefulWidget {
|
||||||
|
final int visibleCount;
|
||||||
|
final int pageSize;
|
||||||
|
final int totalCount;
|
||||||
|
final int selectedCount;
|
||||||
|
|
||||||
|
final Function(String) onSearch;
|
||||||
|
final Function(bool) onStockToggle;
|
||||||
|
final Function(String) onCategoryChange;
|
||||||
|
final Function(String) onSubcategoryChange;
|
||||||
|
final Function(String) onOfferStatusChange;
|
||||||
|
final Function(double) onPriceChange;
|
||||||
|
|
||||||
|
const ImportsHeader({
|
||||||
|
super.key,
|
||||||
|
required this.visibleCount,
|
||||||
|
required this.pageSize,
|
||||||
|
required this.totalCount,
|
||||||
|
required this.selectedCount,
|
||||||
|
required this.onSearch,
|
||||||
|
required this.onStockToggle,
|
||||||
|
required this.onCategoryChange,
|
||||||
|
required this.onSubcategoryChange,
|
||||||
|
required this.onOfferStatusChange,
|
||||||
|
required this.onPriceChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImportsHeader> createState() => _ImportsHeaderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImportsHeaderState extends State<ImportsHeader> {
|
||||||
|
String category = "All";
|
||||||
|
String subcategory = "All";
|
||||||
|
String offerStatus = "All";
|
||||||
|
bool inStockOnly = false;
|
||||||
|
double priceFloor = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final bool isMobile = MediaQuery.of(context).size.width < 900;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
/// ✅ TOP SECTION (OVERFLOW SAFE)
|
||||||
|
isMobile ? _mobileTopBar() : _desktopTopBar(),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
/// ✅ FILTER ROW
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: [
|
||||||
|
/// ✅ CATEGORY (RESETS SUBCATEGORY)
|
||||||
|
_dropdown(
|
||||||
|
label: "Category",
|
||||||
|
value: category,
|
||||||
|
items: const ["All", "Floor Mats", "Wheels"],
|
||||||
|
onChanged: (v) {
|
||||||
|
final value = v ?? "All";
|
||||||
|
setState(() {
|
||||||
|
category = value;
|
||||||
|
subcategory = "All"; // ✅ RESET
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.onCategoryChange(value);
|
||||||
|
widget.onSubcategoryChange("All"); // ✅ FORCE API RESET
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ SUBCATEGORY
|
||||||
|
_dropdown(
|
||||||
|
label: "Subcategory",
|
||||||
|
value: subcategory,
|
||||||
|
items: const ["All", "Wheels - Cast", "Wheels - Forged"],
|
||||||
|
onChanged: (v) {
|
||||||
|
final value = v ?? "All";
|
||||||
|
setState(() => subcategory = value);
|
||||||
|
widget.onSubcategoryChange(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ OFFER STATUS
|
||||||
|
_dropdown(
|
||||||
|
label: "Offer Status",
|
||||||
|
value: offerStatus,
|
||||||
|
items: const ["All", "Published", "Draft"],
|
||||||
|
onChanged: (v) {
|
||||||
|
final value = v ?? "All";
|
||||||
|
setState(() => offerStatus = value);
|
||||||
|
widget.onOfferStatusChange(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ PRICE SLIDER
|
||||||
|
SizedBox(
|
||||||
|
width: 260,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text("Price Floor"),
|
||||||
|
Slider(
|
||||||
|
min: 0,
|
||||||
|
max: 1000,
|
||||||
|
value: priceFloor,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => priceFloor = value);
|
||||||
|
widget.onPriceChange(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text("Price ≥ \$${priceFloor.toInt()}"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ DESKTOP TOP BAR
|
||||||
|
Widget _desktopTopBar() {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _titleSection()),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Flexible(child: _searchBox()),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [_stockToggle(), _actionButton()],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ MOBILE TOP BAR
|
||||||
|
Widget _mobileTopBar() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_titleSection(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_searchBox(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Wrap(spacing: 12, children: [_stockToggle(), _actionButton()]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ TITLE
|
||||||
|
Widget _titleSection() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Imports", style: AppTypo.h3),
|
||||||
|
Text(
|
||||||
|
"${widget.visibleCount} of ${widget.totalCount} products",
|
||||||
|
style: AppTypo.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ SEARCH BOX
|
||||||
|
Widget _searchBox() {
|
||||||
|
return SizedBox(
|
||||||
|
height: 44,
|
||||||
|
child: TextField(
|
||||||
|
onChanged: widget.onSearch,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: "Search imports...",
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ STOCK TOGGLE
|
||||||
|
Widget _stockToggle() {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Switch(
|
||||||
|
value: inStockOnly,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() => inStockOnly = v);
|
||||||
|
widget.onStockToggle(v);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
const Text("In Stock Only"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ ACTION BUTTON
|
||||||
|
Widget _actionButton() {
|
||||||
|
return ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Text("Bulk Action"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ REUSABLE DROPDOWN
|
||||||
|
Widget _dropdown({
|
||||||
|
required String label,
|
||||||
|
required String value,
|
||||||
|
required List<String> items,
|
||||||
|
required Function(String?) onChanged,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
items: items
|
||||||
|
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
|
||||||
|
.toList(),
|
||||||
|
decoration: const InputDecoration(border: OutlineInputBorder()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
315
lib/presentation/screens/imports/imports_screen.dart
Normal file
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
310
lib/presentation/screens/my_account/account_screen.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
323
lib/presentation/screens/pricing/pricing_screen.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
304
lib/presentation/screens/products/products_screen.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
237
lib/presentation/screens/store/create_location_screen.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
132
lib/presentation/screens/store/store.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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."),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
pubspec.lock
@ -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:
|
||||||
|
|||||||
@ -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"
|
||||||