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">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application
|
||||
android:label="autos"
|
||||
android:label="Autos"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<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;
|
||||
buildSettings = {
|
||||
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_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@ -484,7 +484,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
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_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
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" : "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"
|
||||
}
|
||||
}
|
||||
{"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"}}
|
||||
|
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 {
|
||||
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 receiveTimeout = Duration(seconds: 10);
|
||||
@ -9,8 +9,21 @@ class ApiEndpoints {
|
||||
};
|
||||
|
||||
///Authentication
|
||||
static const login = '/auth/login';
|
||||
static const signup = '/auth/signup';
|
||||
static const forgotPassword = '/auth/forgot-password';
|
||||
static const userDetails = '/auth/users/';
|
||||
static const login = '/api/auth/login';
|
||||
static const signup = '/api/auth/signup';
|
||||
static const forgotPassword = '/api/auth/forgot-password';
|
||||
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/login_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/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:flutter/material.dart';
|
||||
|
||||
@ -45,6 +53,30 @@ class AppRouter {
|
||||
case AppRoutePaths.turn14:
|
||||
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:
|
||||
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 {
|
||||
///Auth
|
||||
static const auth = '/auth';
|
||||
static const signup = '/signup';
|
||||
static const forgotPassword = '/forgotPassword';
|
||||
|
||||
///Screens
|
||||
static const dashboard = '/dashboard';
|
||||
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';
|
||||
|
||||
class AppTypo {
|
||||
@ -13,15 +14,16 @@ class AppTypo {
|
||||
);
|
||||
|
||||
static const TextStyle h2 = TextStyle(
|
||||
color: Color(0xFF3B81F9),
|
||||
color: Color(0xFF00BFFF),
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w700,
|
||||
);
|
||||
|
||||
static const TextStyle h3 = TextStyle(
|
||||
color: Colors.white,
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 24,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
@ -49,7 +51,7 @@ class AppTypo {
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF2272f6),
|
||||
color: AppTheme.primary,
|
||||
);
|
||||
|
||||
static const TextStyle menu = TextStyle(
|
||||
|
||||
@ -9,7 +9,7 @@ class HamburgerButton extends StatefulWidget {
|
||||
super.key,
|
||||
required this.scaffoldKey,
|
||||
this.backgroundColor = const Color(0xFFEAF1FF), // light background
|
||||
this.iconColor = const Color(0xFF3B81F9), // dark blue lines
|
||||
this.iconColor = const Color(0xFF00BFFF), // dark blue lines
|
||||
});
|
||||
|
||||
@override
|
||||
|
||||
@ -22,7 +22,7 @@ class SideMenu extends ConsumerWidget {
|
||||
children: [
|
||||
// Header
|
||||
DrawerHeader(
|
||||
decoration: BoxDecoration(color: Color(0xFF3B81F9)),
|
||||
decoration: const BoxDecoration(color: Color(0xFF00BFFF)),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
@ -59,19 +59,19 @@ class SideMenu extends ConsumerWidget {
|
||||
// --- INTEGRATIONS ---
|
||||
_sectionHeader("INTEGRATIONS"),
|
||||
_menuItem(context, "⚡", "Turn14", AppRoutePaths.turn14),
|
||||
_menuItem(context, "🛍️", "eBay", 'ebay'),
|
||||
_menuItem(context, "🛒", "Store", 'store'),
|
||||
_menuItem(context, "🛍️", "eBay", AppRoutePaths.ebay),
|
||||
_menuItem(context, "🛒", "Store", AppRoutePaths.store),
|
||||
|
||||
// --- MANAGE ---
|
||||
_sectionHeader("MANAGE"),
|
||||
_menuItem(context, "🏷️", "Brands", 'brands'),
|
||||
_menuItem(context, "📦", "Products", 'products'),
|
||||
_menuItem(context, "⬇️", "Imports", 'imports'),
|
||||
_menuItem(context, "🏷️", "Brands", AppRoutePaths.brands),
|
||||
_menuItem(context, "📦", "Products", AppRoutePaths.products),
|
||||
_menuItem(context, "⬇️", "Imports", AppRoutePaths.imports),
|
||||
|
||||
// --- ACCOUNT ---
|
||||
_sectionHeader("ACCOUNT"),
|
||||
_menuItem(context, "👤", "My Account", AppRoutePaths.auth),
|
||||
_menuItem(context, "💰", "Pricing Plan", 'pricing'),
|
||||
_menuItem(context, "👤", "My Account", AppRoutePaths.myAccount),
|
||||
_menuItem(context, "💰", "Pricing Plan", AppRoutePaths.pricing),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -112,8 +112,10 @@ class SideMenu extends ConsumerWidget {
|
||||
// Section title widget
|
||||
Widget _sectionHeader(String title) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(color: Color.fromARGB(255, 225, 236, 255)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color.fromARGB(255, 225, 236, 255),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
@ -123,52 +125,81 @@ class SideMenu extends ConsumerWidget {
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 15,
|
||||
letterSpacing: 0.5,
|
||||
color: Color(0xFF3B81F9),
|
||||
color: Color(0xFF00BFFF),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Menu item widget
|
||||
// Menu item widget with persistent left indicator
|
||||
Widget _menuItem(
|
||||
BuildContext context,
|
||||
String emoji,
|
||||
String title,
|
||||
String route,
|
||||
) {
|
||||
final bool isSelected = selected == route;
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: selectedRoute,
|
||||
builder: (_, current, __) {
|
||||
final bool isSelected = current == route;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () {
|
||||
onItemSelected(route);
|
||||
Navigator.pushNamed(context, route);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0x143B81F9) : Colors.transparent,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(emoji, style: const TextStyle(fontSize: 18)),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: isSelected ? const Color(0xFF3B81F9) : Colors.black87,
|
||||
fontSize: 16,
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
|
||||
selectedRoute.value = route; // ⭐ indicator ALWAYS updates
|
||||
|
||||
if (ModalRoute.of(context)?.settings.name != route) {
|
||||
Navigator.pushReplacementNamed(context, route);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? const Color(0x143B81F9)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
],
|
||||
child: Row(
|
||||
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)),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
color: isSelected
|
||||
? const Color(0xFF3B81F9)
|
||||
: Colors.black87,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:autos/core/theme/app_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WaveBackground extends StatelessWidget {
|
||||
@ -19,10 +20,7 @@ class WaveBackground extends StatelessWidget {
|
||||
height: 220,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF3B81F9), // blue
|
||||
Color(0xFF5A96F9), // lighter blue
|
||||
],
|
||||
colors: [AppTheme.primary, AppTheme.primary],
|
||||
begin: Alignment.topLeft,
|
||||
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)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
@ -65,7 +38,7 @@ class WaveBackground extends StatelessWidget {
|
||||
clipper: BottomSoftWaveClipper(),
|
||||
child: Container(
|
||||
height: 80,
|
||||
color: const Color(0xFF5A96F9),
|
||||
color: AppTheme.primary,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
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 'package:autos/domain/entities/user.dart';
|
||||
|
||||
/// ✅ STORE MODEL
|
||||
class StoreModel {
|
||||
final String name;
|
||||
final String description;
|
||||
final String url;
|
||||
final String urlPath;
|
||||
final String? lastOpenedTime;
|
||||
final String? lastOpenedTimeRaw;
|
||||
final String? logoUrl;
|
||||
|
||||
const StoreModel({
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.url,
|
||||
required this.urlPath,
|
||||
this.lastOpenedTime,
|
||||
this.lastOpenedTimeRaw,
|
||||
this.logoUrl,
|
||||
});
|
||||
|
||||
factory StoreModel.fromJson(Map<String, dynamic> json) {
|
||||
return StoreModel(
|
||||
name: json['name'] ?? '',
|
||||
description: json['description'] ?? '',
|
||||
url: json['url'] ?? '',
|
||||
urlPath: json['urlPath'] ?? '',
|
||||
lastOpenedTime: json['lastOpenedTime'],
|
||||
lastOpenedTimeRaw: json['lastOpenedTimeRaw'],
|
||||
logoUrl: json['logo']?['url'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'url': url,
|
||||
'urlPath': urlPath,
|
||||
'lastOpenedTime': lastOpenedTime,
|
||||
'lastOpenedTimeRaw': lastOpenedTimeRaw,
|
||||
'logo': {
|
||||
'url': logoUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ PAYMENT MODEL
|
||||
class PaymentModel {
|
||||
final int id;
|
||||
final String email;
|
||||
final String amount;
|
||||
final String plan;
|
||||
final String status;
|
||||
final String? stripeSessionId;
|
||||
final String? stripePaymentIntentId;
|
||||
final String? subscriptionId;
|
||||
final String? startDate;
|
||||
final String? endDate;
|
||||
|
||||
const PaymentModel({
|
||||
required this.id,
|
||||
required this.email,
|
||||
required this.amount,
|
||||
required this.plan,
|
||||
required this.status,
|
||||
this.stripeSessionId,
|
||||
this.stripePaymentIntentId,
|
||||
this.subscriptionId,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
});
|
||||
|
||||
factory PaymentModel.fromJson(Map<String, dynamic> json) {
|
||||
return PaymentModel(
|
||||
id: json['id'] ?? 0,
|
||||
email: json['email'] ?? '',
|
||||
amount: json['amount'] ?? '',
|
||||
plan: json['plan'] ?? '',
|
||||
status: json['status'] ?? '',
|
||||
stripeSessionId: json['stripeSessionId'],
|
||||
stripePaymentIntentId: json['stripePaymentIntentId'],
|
||||
subscriptionId: json['subscriptionId'],
|
||||
startDate: json['startDate'],
|
||||
endDate: json['endDate'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'email': email,
|
||||
'amount': amount,
|
||||
'plan': plan,
|
||||
'status': status,
|
||||
'stripeSessionId': stripeSessionId,
|
||||
'stripePaymentIntentId': stripePaymentIntentId,
|
||||
'subscriptionId': subscriptionId,
|
||||
'startDate': startDate,
|
||||
'endDate': endDate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ MAIN USER MODEL
|
||||
class UserModel extends User {
|
||||
final StoreModel? store;
|
||||
final PaymentModel? payment;
|
||||
|
||||
const UserModel({
|
||||
required super.id,
|
||||
required super.name,
|
||||
@ -12,23 +120,52 @@ class UserModel extends User {
|
||||
super.phoneNumber,
|
||||
super.message,
|
||||
super.code,
|
||||
this.store,
|
||||
this.payment,
|
||||
});
|
||||
|
||||
/// ✅ FROM API JSON
|
||||
factory UserModel.fromJson(Map<String, dynamic> json) {
|
||||
final payment = json['payment'] ?? {};
|
||||
final paymentJson = json['payment'];
|
||||
final storeJson = json['store'];
|
||||
|
||||
final payment =
|
||||
paymentJson != null ? PaymentModel.fromJson(paymentJson) : null;
|
||||
|
||||
final store =
|
||||
storeJson != null ? StoreModel.fromJson(storeJson) : null;
|
||||
|
||||
return UserModel(
|
||||
id: json['userid'] ?? json['id']?.toString() ?? '',
|
||||
name: json['name'] ?? '',
|
||||
email: json['email'] ?? '',
|
||||
role: json['role'] ?? '',
|
||||
plan: payment['plan'] ?? json['plan'],
|
||||
paymentStatus: payment['status'] ?? json['paymentStatus'],
|
||||
plan: payment?.plan ?? json['plan'],
|
||||
paymentStatus: payment?.status ?? json['paymentStatus'],
|
||||
phoneNumber: json['phonenumber'] ?? '',
|
||||
message: json['message'] ?? '',
|
||||
code: json['code'] ?? '',
|
||||
store: store,
|
||||
payment: payment,
|
||||
);
|
||||
}
|
||||
|
||||
/// ✅ 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() {
|
||||
return {
|
||||
'userid': id,
|
||||
@ -40,18 +177,20 @@ class UserModel extends User {
|
||||
'phonenumber': phoneNumber,
|
||||
'message': message,
|
||||
'code': code,
|
||||
'store': store?.toJson(),
|
||||
'payment': payment?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Store user as raw JSON string
|
||||
/// ✅ STORE USER AS RAW JSON STRING
|
||||
String toRawJson() => jsonEncode(toJson());
|
||||
|
||||
/// Parse user from raw JSON string
|
||||
/// ✅ RESTORE USER FROM RAW JSON STRING
|
||||
factory UserModel.fromRawJson(String raw) =>
|
||||
UserModel.fromJson(jsonDecode(raw));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserModel(id: $id, name: $name, email: $email, role: $role, phone: $phoneNumber, plan: $plan, status: $paymentStatus, code: $code)';
|
||||
return 'UserModel(id: $id, name: $name, email: $email, role: $role, plan: $plan, status: $paymentStatus)';
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,15 +6,14 @@ class ApiService {
|
||||
final Dio _dio;
|
||||
|
||||
ApiService({String? baseUrl})
|
||||
: _dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl ?? ApiEndpoints.baseUrl,
|
||||
connectTimeout: ApiEndpoints.connectTimeout,
|
||||
receiveTimeout: ApiEndpoints.receiveTimeout,
|
||||
headers: ApiEndpoints.defaultHeaders,
|
||||
),
|
||||
) {
|
||||
// Add logging interceptor in debug mode
|
||||
: _dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl ?? ApiEndpoints.baseUrl,
|
||||
connectTimeout: ApiEndpoints.connectTimeout,
|
||||
receiveTimeout: ApiEndpoints.receiveTimeout,
|
||||
headers: ApiEndpoints.defaultHeaders,
|
||||
),
|
||||
) {
|
||||
if (kDebugMode) {
|
||||
_dio.interceptors.add(
|
||||
LogInterceptor(requestBody: true, responseBody: true),
|
||||
@ -22,6 +21,7 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ KEEP OLD POST (so existing files DO NOT BREAK)
|
||||
Future<Response> post(
|
||||
String endpoint,
|
||||
Map<String, dynamic> data, {
|
||||
@ -30,7 +30,39 @@ class ApiService {
|
||||
return await _dio.post(endpoint, data: data, options: options);
|
||||
}
|
||||
|
||||
Future<Response> get(String endpoint, {Map<String, dynamic>? params}) async {
|
||||
return await _dio.get(endpoint, queryParameters: params);
|
||||
// ✅ NEW POST (for overrideBaseUrl + headers + 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/navigation_service.dart';
|
||||
import 'package:autos/core/theme/app_theme.dart';
|
||||
import 'package:autos/presentation/providers/user_provider.dart';
|
||||
import 'package:autos/presentation/screens/auth/login_screen.dart';
|
||||
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
// ✅ ADD THIS IMPORT
|
||||
import 'package:onesignal_flutter/onesignal_flutter.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// ONESIGNAL SETUP (ADDED SAFELY)
|
||||
OneSignal.Debug.setLogLevel(OSLogLevel.verbose);
|
||||
OneSignal.initialize("271c3931-07ee-46b6-8629-7f0d63f58085");
|
||||
OneSignal.Notifications.requestPermission(false);
|
||||
|
||||
// KEEP YOUR ORIGINAL LOGIC
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
}
|
||||
|
||||
@ -28,16 +41,16 @@ class _MyAppState extends ConsumerState<MyApp> {
|
||||
Future<void> _restoreSession() async {
|
||||
final userNotifier = ref.read(userProvider.notifier);
|
||||
|
||||
// Load user from secure storage
|
||||
// ✅ Load user from secure storage (UNCHANGED)
|
||||
await userNotifier.loadUserFromStorage();
|
||||
|
||||
// Stop loading after user is restored
|
||||
// ✅ Stop loading after user is restored (UNCHANGED)
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Show loading screen until user session is restored
|
||||
// ✅ Loading screen (UNCHANGED)
|
||||
if (_isLoading) {
|
||||
return const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
@ -53,13 +66,12 @@ class _MyAppState extends ConsumerState<MyApp> {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: "Autos",
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
theme: AppTheme.lightTheme,
|
||||
|
||||
// Dynamic home based on session
|
||||
// ✅ Dynamic home based on session (UNCHANGED)
|
||||
home: user != null ? const DashboardScreen() : const LoginScreen(),
|
||||
|
||||
navigatorKey: NavigationService.navigatorKey,
|
||||
onGenerateRoute: AppRouter.generateRoute,
|
||||
);
|
||||
}
|
||||
|
||||
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,148 +4,123 @@ import 'package:autos/data/models/user_model.dart';
|
||||
import 'package:autos/data/repositories/user_repository_impl.dart';
|
||||
import 'package:autos/data/sources/remote/api_service.dart';
|
||||
import 'package:autos/domain/entities/user.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_riverpod/legacy.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
// Provide a single ApiService instance across the app
|
||||
/// Provide a single ApiService instance across the app
|
||||
final apiServiceProvider = Provider<ApiService>((ref) => ApiService());
|
||||
|
||||
// Provide repository that depends on ApiService
|
||||
/// Provide repository that depends on ApiService
|
||||
final userRepositoryProvider = Provider<UserRepositoryImpl>(
|
||||
(ref) => UserRepositoryImpl(ref.read(apiServiceProvider)),
|
||||
);
|
||||
|
||||
// Manage user state
|
||||
final loginProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>((
|
||||
ref,
|
||||
) {
|
||||
/// SINGLE GLOBAL USER PROVIDER
|
||||
final userProvider =
|
||||
StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
|
||||
final repo = ref.read(userRepositoryProvider);
|
||||
return UserNotifier(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);
|
||||
return UserNotifier(ref, repo);
|
||||
});
|
||||
|
||||
enum AuthAction { idle, login, signup }
|
||||
|
||||
class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
||||
final Ref ref;
|
||||
final UserRepositoryImpl repository;
|
||||
|
||||
final _storage = const FlutterSecureStorage();
|
||||
static const _userKey = 'logged_in_user';
|
||||
|
||||
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 {
|
||||
final jsonString = await _storage.read(key: _userKey);
|
||||
if (jsonString != null) {
|
||||
debugPrint("RESULT: $jsonString");
|
||||
final jsonData = jsonDecode(jsonString);
|
||||
final user = UserModel.fromJson(jsonData);
|
||||
await Future.microtask(() {});
|
||||
state = AsyncValue.data(user);
|
||||
|
||||
if (jsonString == null) {
|
||||
state = const AsyncValue.data(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final jsonData = jsonDecode(jsonString);
|
||||
final user = UserModel.fromJson(jsonData);
|
||||
|
||||
state = AsyncValue.data(user);
|
||||
}
|
||||
|
||||
///Login
|
||||
// LOGIN FLOW (FULLY SYNCHRONIZED)
|
||||
Future<void> login(String email, String password) async {
|
||||
lastAction = AuthAction.login;
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
try {
|
||||
final user = await repository.login(email, password);
|
||||
// ✅ Save user to secure storage
|
||||
if (user is UserModel) {
|
||||
await _storage.write(key: _userKey, value: user.toRawJson());
|
||||
}
|
||||
state = AsyncValue.data(user);
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
// 1️⃣ Login API (partial user)
|
||||
final partialUser = await repository.login(email, password);
|
||||
|
||||
// 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) {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
/// Logout
|
||||
// SIGNUP
|
||||
Future<void> signup(
|
||||
String name,
|
||||
String email,
|
||||
String password,
|
||||
String phone,
|
||||
) async {
|
||||
lastAction = AuthAction.signup;
|
||||
|
||||
try {
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
// ✅ 1️⃣ Signup → returns USER ENTITY
|
||||
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);
|
||||
} catch (e, st) {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ✅ ✅ LOGOUT
|
||||
Future<void> logout() async {
|
||||
await _storage.delete(key: _userKey);
|
||||
state = const AsyncValue.data(null);
|
||||
}
|
||||
|
||||
///Sign up
|
||||
Future<void> signup(
|
||||
String name,
|
||||
String email,
|
||||
String password,
|
||||
String phone,
|
||||
) async {
|
||||
lastAction = AuthAction.signup;
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final user = await repository.signup(name, email, password, phone);
|
||||
state = AsyncValue.data(user);
|
||||
} catch (e, st) {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
///Reset password
|
||||
// PASSWORD RESET
|
||||
Future<void> sendPasswordResetLink(String email) async {
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
state = const AsyncValue.loading();
|
||||
await repository.sendPasswordResetLink(email);
|
||||
state = const AsyncValue.data(null);
|
||||
} catch (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 '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/presentation/providers/user_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -10,7 +12,8 @@ class ForgotPasswordScreen extends ConsumerStatefulWidget {
|
||||
const ForgotPasswordScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
|
||||
ConsumerState<ForgotPasswordScreen> createState() =>
|
||||
_ForgotPasswordScreenState();
|
||||
}
|
||||
|
||||
class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
@ -51,8 +54,9 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Forgot Password"),
|
||||
backgroundColor: const Color(0xFF3B81F9),
|
||||
title: Text("Forgot Password", style: AppTypo.h3),
|
||||
backgroundColor: AppTheme.primary,
|
||||
iconTheme: Theme.of(context).appBarTheme.iconTheme,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
@ -62,10 +66,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
children: [
|
||||
const Text(
|
||||
"Reset your password",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
@ -104,7 +105,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
Fluttertoast.showToast(
|
||||
msg: "Reset link sent to $email",
|
||||
backgroundColor: Colors.green,
|
||||
toastLength: Toast.LENGTH_LONG
|
||||
toastLength: Toast.LENGTH_LONG,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
@ -115,7 +116,7 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF3B81F9),
|
||||
backgroundColor: AppTheme.primary,
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
shape: RoundedRectangleBorder(
|
||||
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/wave_background.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/dashboard/dashboard_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -42,13 +43,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userState = ref.watch(loginProvider);
|
||||
final userState = ref.watch(userProvider);
|
||||
|
||||
final size = MediaQuery.of(context).size;
|
||||
final double verticalSpace = size.height * 0.02;
|
||||
final double horizontalPadding = size.width * 0.06;
|
||||
|
||||
ref.listen(loginProvider, (previous, next) {
|
||||
ref.listen(userProvider, (previous, next) {
|
||||
if (next.hasValue && next.value != null) {
|
||||
_showToast("Login Successful");
|
||||
Navigator.pushReplacement(
|
||||
@ -137,16 +138,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ForgotPasswordScreen(),
|
||||
),
|
||||
AppRoutePaths.forgotPassword,
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
child: Text(
|
||||
"Forgot Password?",
|
||||
style: TextStyle(color: Color(0xFF3B81F9)),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -160,14 +159,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(loginProvider.notifier)
|
||||
.read(userProvider.notifier)
|
||||
.login(
|
||||
emailController.text.trim(),
|
||||
passwordController.text.trim(),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF3B81F9),
|
||||
backgroundColor: AppTheme.primary,
|
||||
minimumSize: Size(
|
||||
double.infinity,
|
||||
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/presentation/providers/user_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -29,14 +30,15 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
|
||||
@override
|
||||
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 double verticalSpace = size.height * 0.02;
|
||||
final double horizontalPadding =
|
||||
size.width * 0.06; // dynamic horizontal padding
|
||||
final double horizontalPadding = size.width * 0.06;
|
||||
|
||||
ref.listen(signupProvider, (previous, next) {
|
||||
/// ✅ FIXED: listen to userProvider
|
||||
ref.listen(userProvider, (previous, next) {
|
||||
if (next.hasValue && next.value != null) {
|
||||
Fluttertoast.showToast(
|
||||
msg: "Sign up successful! Please log in.",
|
||||
@ -73,20 +75,18 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
child: SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: size.width < 500
|
||||
? size.width
|
||||
: 450, // better center layout on tablets
|
||||
maxWidth: size.width < 500 ? size.width : 450,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: size.height * 0.07),
|
||||
|
||||
/// Logo
|
||||
Image.asset(
|
||||
'assets/auth/autos_transp.png',
|
||||
height: size.height * 0.14,
|
||||
),
|
||||
|
||||
Row(
|
||||
children: const [
|
||||
Text(
|
||||
@ -99,9 +99,9 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
Spacer(),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: verticalSpace),
|
||||
|
||||
/// Name field
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(
|
||||
@ -109,9 +109,9 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: verticalSpace),
|
||||
|
||||
/// Email field
|
||||
TextField(
|
||||
controller: emailController,
|
||||
decoration: const InputDecoration(
|
||||
@ -119,9 +119,9 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
prefixIcon: Icon(Icons.email_outlined),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: verticalSpace),
|
||||
|
||||
/// Password field
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: !isPasswordVisible,
|
||||
@ -142,9 +142,9 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: verticalSpace),
|
||||
|
||||
/// Phone field
|
||||
TextField(
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
@ -153,24 +153,25 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
prefixIcon: Icon(Icons.phone_outlined),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: verticalSpace * 2),
|
||||
|
||||
/// Sign Up button
|
||||
/// ✅ ✅ ✅ FIXED SIGNUP BUTTON
|
||||
userState.isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(signupProvider.notifier)
|
||||
.read(userProvider.notifier)
|
||||
.signup(
|
||||
nameController.text,
|
||||
emailController.text,
|
||||
passwordController.text,
|
||||
phoneController.text,
|
||||
nameController.text.trim(),
|
||||
emailController.text.trim(),
|
||||
passwordController.text.trim(),
|
||||
phoneController.text.trim(),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF3B81F9),
|
||||
backgroundColor: AppTheme.primary,
|
||||
minimumSize: Size(
|
||||
double.infinity,
|
||||
size.height * 0.05,
|
||||
@ -191,7 +192,6 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
|
||||
SizedBox(height: verticalSpace * 2.5),
|
||||
|
||||
/// Already have account text
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -203,13 +203,14 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
child: const Text(
|
||||
"Sign In",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF3B81F9),
|
||||
color: AppTheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(userDetailsProvider);
|
||||
final userAsync = ref.watch(userProvider);
|
||||
final double topPadding = MediaQuery.of(context).padding.top + 16;
|
||||
|
||||
return Scaffold(
|
||||
@ -32,7 +32,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// TOP BAR — RESPONSIVE & CENTERED
|
||||
Positioned(
|
||||
top: topPadding,
|
||||
left: 0,
|
||||
@ -47,31 +46,20 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 👇 MAIN CONTENT SECTION
|
||||
SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
topPadding + 70, // pushes content below title
|
||||
16,
|
||||
20,
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(16, topPadding + 70, 16, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- Welcome Header ---
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Text("👋", style: const TextStyle(fontSize: 32)),
|
||||
const Text("Hi, 👋", style: TextStyle(fontSize: 32)),
|
||||
const SizedBox(width: 8),
|
||||
Text("Hi,", style: AppTypo.h3),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 6),
|
||||
|
||||
Text(
|
||||
"Manage your Turn14 & eBay integrations effortlessly in one platform.",
|
||||
style: AppTypo.body.copyWith(
|
||||
@ -79,50 +67,57 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// ⭐ INFO CARDS — RESPONSIVE
|
||||
InfoCard(
|
||||
// Info cards
|
||||
const InfoCard(
|
||||
emoji: "🎥",
|
||||
heading: "How to Access Your Store",
|
||||
content: "Watch the full walkthrough.",
|
||||
url: "https://youtu.be/g6qV2cQ2Fhw",
|
||||
),
|
||||
|
||||
InfoCard(
|
||||
const InfoCard(
|
||||
emoji: "📊",
|
||||
heading: "About Data4Autos",
|
||||
content:
|
||||
"Data4Autos is an advanced automation platform for eCommerce sellers. Connect, manage, and automate your Turn14 and eBay stores effortlessly — all in one place.",
|
||||
),
|
||||
|
||||
InfoCard(
|
||||
const InfoCard(
|
||||
emoji: "⚙️",
|
||||
heading: "About Turn14 Integration",
|
||||
content:
|
||||
"Sync your Turn14 account to automatically import products, pricing, and stock updates. Keep your online store current without manual effort.",
|
||||
),
|
||||
|
||||
InfoCard(
|
||||
const InfoCard(
|
||||
emoji: "🛒",
|
||||
heading: "About eBay Integration",
|
||||
content:
|
||||
"Simplify eBay listing management, stock updates, and order automation directly from your dashboard.",
|
||||
),
|
||||
|
||||
InfoCard(
|
||||
const InfoCard(
|
||||
emoji: "💰",
|
||||
heading: "Pricing & Plans",
|
||||
content:
|
||||
"Explore flexible pricing options for every business size. Upgrade anytime to unlock advanced features.",
|
||||
),
|
||||
|
||||
user.when(
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (err, _) => Text("Error: $err"),
|
||||
// ✅ USER DETAILS CARD (FIXED)
|
||||
userAsync.when(
|
||||
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) {
|
||||
if (user == null) return SizedBox();
|
||||
if (user == null) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text("No user data available"),
|
||||
);
|
||||
}
|
||||
|
||||
return UserDetailsCard(
|
||||
name: user.name,
|
||||
@ -137,8 +132,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 🍔 Common Hamburger Button
|
||||
HamburgerButton(scaffoldKey: _scaffoldKey),
|
||||
],
|
||||
),
|
||||
@ -146,9 +139,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// ⭐ REUSABLE INFO CARD
|
||||
// ------------------------------------------------------------
|
||||
// ----------------------- INFO CARD -----------------------
|
||||
class InfoCard extends StatefulWidget {
|
||||
final String emoji;
|
||||
final String heading;
|
||||
@ -195,6 +186,7 @@ class _InfoCardState extends State<InfoCard> {
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withValues(alpha: 0.5),
|
||||
margin: const EdgeInsets.only(bottom: 18),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
@ -202,7 +194,6 @@ class _InfoCardState extends State<InfoCard> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
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),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// YouTube Player
|
||||
if (_controller != null)
|
||||
!_showPlayer
|
||||
? GestureDetector(
|
||||
@ -233,7 +222,6 @@ class _InfoCardState extends State<InfoCard> {
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Thumbnail
|
||||
Container(
|
||||
height: 180,
|
||||
decoration: BoxDecoration(
|
||||
@ -246,7 +234,6 @@ class _InfoCardState extends State<InfoCard> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Play button overlay
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
@ -276,6 +263,7 @@ class _InfoCardState extends State<InfoCard> {
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- USER DETAILS CARD -----------------------
|
||||
class UserDetailsCard extends StatelessWidget {
|
||||
final String name;
|
||||
final String email;
|
||||
@ -294,6 +282,7 @@ class UserDetailsCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
shadowColor: Colors.black.withValues(alpha: 0.5),
|
||||
elevation: 2,
|
||||
margin: const EdgeInsets.only(bottom: 18),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
@ -307,13 +296,14 @@ class UserDetailsCard extends StatelessWidget {
|
||||
Text("👤", style: TextStyle(fontSize: 26)),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text("User Details", style: AppTypo.sectionHeader),
|
||||
child: Text(
|
||||
"User Details",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_detailRow("Name", name),
|
||||
_detailRow("Email", email),
|
||||
_detailRow("Phone", phone),
|
||||
@ -327,33 +317,32 @@ class UserDetailsCard extends StatelessWidget {
|
||||
Widget _detailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.black87,
|
||||
height: 1.4,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "$label: ",
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 60, // ✅ FIXED WIDTH FOR LEFT LABEL
|
||||
child: Text(
|
||||
"$label:",
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.black87,
|
||||
height: 1.4,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: value,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.black87,
|
||||
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/side_menu.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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
class Turn14Screen extends ConsumerStatefulWidget {
|
||||
const Turn14Screen({super.key});
|
||||
@ -16,9 +18,16 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
String selected = 'turn14';
|
||||
|
||||
// controllers
|
||||
final TextEditingController clientIdController = TextEditingController();
|
||||
final TextEditingController clientSecretController = TextEditingController();
|
||||
|
||||
@override
|
||||
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;
|
||||
|
||||
return Scaffold(
|
||||
@ -46,13 +55,13 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
|
||||
),
|
||||
),
|
||||
|
||||
/// Main Scrollable UI
|
||||
SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Description Row
|
||||
Row(
|
||||
children: [
|
||||
Text("⚡", style: const TextStyle(fontSize: 28)),
|
||||
@ -70,21 +79,16 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Client ID Input
|
||||
_inputField(
|
||||
label: "Client ID",
|
||||
controller: TextEditingController(),
|
||||
),
|
||||
_inputField(label: "Client ID", controller: clientIdController),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Secret Key Input With Eye Icon
|
||||
_passwordField(
|
||||
label: "Secret Key",
|
||||
controller: TextEditingController(),
|
||||
controller: clientSecretController,
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// Save Button
|
||||
/// SAVE BUTTON
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
@ -95,65 +99,70 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
|
||||
),
|
||||
backgroundColor: const Color(0xFF00C9FF),
|
||||
),
|
||||
onPressed: () {},
|
||||
child: const Text(
|
||||
"Save Credentials",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
onPressed: turn14State.isLoading
|
||||
? 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",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Info Box
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_infoBox(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 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."),
|
||||
],
|
||||
),
|
||||
),
|
||||
_tipsBox(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -164,6 +173,7 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// UI COMPONENTS
|
||||
Widget _inputField({
|
||||
required String label,
|
||||
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"
|
||||
source: hosted
|
||||
version: "7.7.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -49,6 +57,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -57,6 +73,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -214,6 +238,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -328,6 +360,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -344,6 +384,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -424,6 +472,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
onesignal_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: onesignal_flutter
|
||||
sha256: b5bb43bf496ddb5e3975ba54c6477cc2d1fcd18fb3698f195d2e0bfd376ddafd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.4"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -488,6 +544,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -512,6 +576,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -733,6 +805,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
11
pubspec.yaml
@ -20,6 +20,8 @@ dependencies:
|
||||
fluttertoast: ^9.0.0
|
||||
flutter_secure_storage: ^9.2.4
|
||||
youtube_player_flutter: ^9.1.3
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
onesignal_flutter: ^5.3.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -48,4 +50,11 @@ flutter:
|
||||
- asset: assets/fonts/Nunito-SemiBold.ttf
|
||||
weight: 600
|
||||
- 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"
|
||||