diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index 697cd7d..f69803f 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.4\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"path_provider_android","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.19\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_android-2.4.13\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.4\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"path_provider_linux","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_linux-2.4.1\\\\","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false}],"windows":[{"name":"path_provider_windows","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_windows-2.4.1\\\\","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false}],"web":[{"name":"shared_preferences_web","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_web-2.4.3\\\\","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2025-11-27 16:07:43.320239","version":"3.32.2","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"image_picker_ios","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_ios-0.8.13\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.4\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_plugin_android_lifecycle-2.0.31\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_android","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_android-0.8.13+1\\\\","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"path_provider_android","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.19\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_android-2.4.13\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"file_selector_macos","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\file_selector_macos-0.9.4+4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_macos","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_macos-0.2.2\\\\","native_build":false,"dependencies":["file_selector_macos"],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.4\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"file_selector_linux","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\file_selector_linux-0.9.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_linux","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_linux-0.2.2\\\\","native_build":false,"dependencies":["file_selector_linux"],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_linux-2.4.1\\\\","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false}],"windows":[{"name":"file_selector_windows","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\file_selector_windows-0.9.3+5\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_windows","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_windows-0.2.2\\\\","native_build":false,"dependencies":["file_selector_windows"],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_windows-2.4.1\\\\","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false}],"web":[{"name":"image_picker_for_web","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_for_web-3.1.1\\\\","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"C:\\\\Users\\\\malia\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_web-2.4.3\\\\","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2025-12-03 11:42:36.705771","version":"3.32.2","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index fd27643..dfaeced 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -8,7 +8,7 @@ plugins { android { namespace = "com.example.kent_logistics_app" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3e9c080..70f970d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,15 @@ + + + + + + + - + + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" /> + + - + - - + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 3aec1cc..7ec31d6 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,11 @@ + NSPhotoLibraryUsageDescription + App requires photo library access to update profile picture. + NSCameraUsageDescription + App requires camera access to take profile picture. + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName diff --git a/lib/config/api_config.dart b/lib/config/api_config.dart index 50800a0..4f285c1 100644 --- a/lib/config/api_config.dart +++ b/lib/config/api_config.dart @@ -1,4 +1,3 @@ class ApiConfig { - // Android emulator (use 10.0.2.2), change for physical device or iOS simulator - static const String baseUrl = "http://10.0.2.2:8000/api"; + static const String baseUrl = "http://10.207.50.74:8000/api"; } diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart new file mode 100644 index 0000000..5c09b72 --- /dev/null +++ b/lib/config/app_config.dart @@ -0,0 +1,13 @@ +class AppConfig { + // For Website & Browser (PC) + static const String logoUrlWeb = "http://127.0.0.1:8000/images/kent_logo2.png"; + + // For Android Emulator + static const String logoUrlEmulator = "http://10.0.2.2:8000/images/kent_logo2.png"; + + // For Physical Device (Replace with your actual PC local IP) + static const String logoUrlDevice = "http://10.207.50.74:8000/images/kent_logo2.png"; + + // Which one to use? + static const String logoUrl = logoUrlDevice; // CHANGE THIS WHEN TESTING ON REAL DEVICE +} diff --git a/lib/main.dart b/lib/main.dart index 92817bb..b1c0a83 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,28 +1,78 @@ import 'package:flutter/material.dart'; +import 'package:kent_logistics_app/providers/dashboard_provider.dart'; +import 'package:kent_logistics_app/providers/invoice_provider.dart'; +import 'package:kent_logistics_app/providers/mark_list_provider.dart'; +import 'package:kent_logistics_app/providers/order_provider.dart'; +import 'package:kent_logistics_app/services/dio_client.dart'; +import 'package:kent_logistics_app/services/order_service.dart'; import 'package:provider/provider.dart'; import 'providers/auth_provider.dart'; +import 'providers/user_profile_provider.dart'; // NEW IMPORT import 'screens/splash_screen.dart'; import 'package:google_fonts/google_fonts.dart'; -void main() { - runApp(const KentApp()); +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + final auth = AuthProvider(); + await auth.init(); // IMPORTANT: ensure prefs loaded before build + + runApp(MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => auth), + ChangeNotifierProvider(create: (_) => UserProfileProvider()), + ChangeNotifierProvider(create: (_) => DashboardProvider()), + ChangeNotifierProvider(create: (_) => MarkListProvider()), + ChangeNotifierProvider( + create: (context) => OrderProvider( + OrderService(DioClient.getInstance(context)), + ), + + ), + ChangeNotifierProvider(create: (_) => InvoiceProvider()), + ], + child: const KentApp(), + )); } + class KentApp extends StatelessWidget { const KentApp({super.key}); @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => AuthProvider(), + + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => AuthProvider()), + ChangeNotifierProvider(create: (_) => UserProfileProvider()), // NEW + ChangeNotifierProvider(create: (_) => DashboardProvider()), + ChangeNotifierProvider(create: (_) => MarkListProvider()), + ChangeNotifierProvider( + create: (context) => OrderProvider( + OrderService(DioClient.getInstance(context)), + ), + ), + ChangeNotifierProvider(create: (_) => InvoiceProvider()), + + + ], child: MaterialApp( title: 'Kent Logistics', debugShowCheckedModeBanner: false, theme: ThemeData( useMaterial3: true, textTheme: GoogleFonts.interTextTheme(), - colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), + colorScheme: ColorScheme.fromSeed(seedColor: Colors.red), + scaffoldBackgroundColor: const Color(0xfff8f6ff), // your light background + appBarTheme: const AppBarTheme( + backgroundColor: Colors.indigo, // FIX + foregroundColor: Colors.white, // white text + icons + elevation: 1, + centerTitle: true, + ), ), + home: const SplashScreen(), ), ); diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart new file mode 100644 index 0000000..9dc2b2d --- /dev/null +++ b/lib/models/user_profile.dart @@ -0,0 +1,46 @@ +class UserProfile { + final String customerId; + final String customerName; + final String companyName; + final String? designation; + final String email; + final String mobile; + final String? address; + final String? pincode; + final String? status; + final String? customerType; + final String? profileImage; + final String? date; + + UserProfile({ + required this.customerId, + required this.customerName, + required this.companyName, + this.designation, + required this.email, + required this.mobile, + this.address, + this.pincode, + this.status, + this.customerType, + this.profileImage, + required this.date, + }); + + factory UserProfile.fromJson(Map json) { + return UserProfile( + customerId: json['customer_id'], + customerName: json['customer_name'], + companyName: json['company_name'], + designation: json['designation'], + email: json['email'], + mobile: json['mobile'], + address: json['address'], + pincode: json['pincode'], + status: json['status'], + customerType: json['customer_type'], + profileImage: json['profile_image'], + date: json['date'], // nullable ok now + ); + } +} diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index 4681d61..ca28d91 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -4,25 +4,43 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../services/auth_service.dart'; class AuthProvider extends ChangeNotifier { - final AuthService _service = AuthService(); + AuthService? _service; bool _loading = false; bool get loading => _loading; + bool initialized = false; + String? _token; Map? _user; String? get token => _token; Map? get user => _user; + bool get isLoggedIn => _token != null && _token!.isNotEmpty; + // Inject context after provider initializes + void initContext(BuildContext context) { + _service = AuthService(context); + } + AuthProvider() { _loadFromPrefs(); } + // ---------------------- NEW FIX: SAFE INIT ----------------------- + Future init() async { + if (!initialized) { + await _loadFromPrefs(); + } + } + + // ---------------------- LOAD FROM PREFS ----------------------- Future _loadFromPrefs() async { final prefs = await SharedPreferences.getInstance(); + _token = prefs.getString('token'); + final userJson = prefs.getString('user'); if (userJson != null) { try { @@ -31,22 +49,33 @@ class AuthProvider extends ChangeNotifier { _user = null; } } + + initialized = true; notifyListeners(); } - Future> login(String loginId, String password) async { + // -------------------------- LOGIN ----------------------------- + Future> login( + BuildContext context, String loginId, String password) async { + _service ??= AuthService(context); + _loading = true; notifyListeners(); - final res = await _service.login(loginId, password); + final res = await _service!.login(loginId, password); _loading = false; if (res['success'] == true) { final token = res['token']?.toString(); - final userMap = res['user'] is Map ? Map.from(res['user']) : null; + final userMap = + res['user'] is Map ? Map.from(res['user']) : null; if (token != null && userMap != null) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString("saved_login_id", loginId); + await prefs.setString("saved_password", password); + await _saveSession(token, userMap); } } @@ -55,27 +84,64 @@ class AuthProvider extends ChangeNotifier { return res; } - Future _saveSession(String token, Map userMap) async { + // --------------------- SAVE SESSION --------------------------- + Future _saveSession( + String token, Map userMap) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('token', token); await prefs.setString('user', jsonEncode(userMap)); + _token = token; _user = userMap; + notifyListeners(); } - Future logout() async { - // optional: call API logout if implemented - if (_token != null) { - try { - await _service.logout(_token!); - } catch (_) {} - } + // ----------------------- LOGOUT ------------------------------- + Future logout(BuildContext context) async { + _service ??= AuthService(context); + + try { + await _service!.logout(); + } catch (_) {} final prefs = await SharedPreferences.getInstance(); await prefs.remove('token'); await prefs.remove('user'); + _token = null; _user = null; notifyListeners(); } + + // -------------------- AUTO LOGIN ------------------------------ + Future autoLoginFromSavedCredentials(BuildContext context) async { + final prefs = await SharedPreferences.getInstance(); + final loginId = prefs.getString('saved_login_id'); + final password = prefs.getString('saved_password'); + + if (loginId == null || password == null) return false; + + final res = await login(context, loginId, password); + return res['success'] == true; + } + + // --------------------- REFRESH TOKEN -------------------------- + Future tryRefreshToken(BuildContext context) async { + final prefs = await SharedPreferences.getInstance(); + final oldToken = prefs.getString('token'); + + if (oldToken == null) return false; + + _service ??= AuthService(context); + + final res = await _service!.refreshToken(oldToken); + + if (res['success'] == true && res['token'] != null) { + await prefs.setString('token', res['token']); + _token = res['token']; + notifyListeners(); + return true; + } + return false; + } } diff --git a/lib/providers/dashboard_provider.dart b/lib/providers/dashboard_provider.dart new file mode 100644 index 0000000..b76a8eb --- /dev/null +++ b/lib/providers/dashboard_provider.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import '../services/dashboard_service.dart'; + +class DashboardProvider extends ChangeNotifier { + DashboardService? _service; + + bool loading = false; + + int activeOrders = 0; + int inTransitOrders = 0; + int deliveredOrders = 0; + String totalValue = "0"; + double totalRaw = 0; + + void init(BuildContext context) { + _service = DashboardService(context); + } + + Future loadSummary(BuildContext context) async { + loading = true; + notifyListeners(); + + _service ??= DashboardService(context); + final res = await _service!.getSummary(); + + if (res['status'] == true) { + final s = res['summary']; + + activeOrders = s['active_orders'] ?? 0; + inTransitOrders = s['in_transit_orders'] ?? 0; + deliveredOrders = s['delivered_orders'] ?? 0; + totalValue = s['total_value']?.toString() ?? "0"; + totalRaw = double.tryParse(s['total_raw'].toString()) ?? 0; + } + + loading = false; + notifyListeners(); + } +} diff --git a/lib/providers/invoice_installment_screen.dart b/lib/providers/invoice_installment_screen.dart new file mode 100644 index 0000000..d73002f --- /dev/null +++ b/lib/providers/invoice_installment_screen.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import '../services/dio_client.dart'; +import '../services/invoice_service.dart'; + +class InvoiceInstallmentScreen extends StatefulWidget { + final int invoiceId; + const InvoiceInstallmentScreen({super.key, required this.invoiceId}); + + @override + State createState() => + _InvoiceInstallmentScreenState(); +} + +class _InvoiceInstallmentScreenState extends State { + bool loading = true; + List installments = []; + + @override + void initState() { + super.initState(); + load(); + } + + Future load() async { + final service = InvoiceService(DioClient.getInstance(context)); + final res = await service.getInstallments(widget.invoiceId); + + if (res['success'] == true) { + installments = res['installments'] ?? []; + } + + loading = false; + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Installments")), + body: loading + ? const Center(child: CircularProgressIndicator()) + : installments.isEmpty + ? const Center( + child: Text("Installments not created yet", + style: TextStyle(fontSize: 18)), + ) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: installments.length, + itemBuilder: (_, i) { + final inst = installments[i]; + return Card( + child: ListTile( + title: Text( + "Amount: ₹${inst['amount']?.toString() ?? '0'}"), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Date: ${inst['installment_date'] ?? 'N/A'}"), + Text( + "Payment: ${inst['payment_method'] ?? 'N/A'}"), + Text( + "Reference: ${inst['reference_no'] ?? 'N/A'}"), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/providers/invoice_provider.dart b/lib/providers/invoice_provider.dart new file mode 100644 index 0000000..5bd70cc --- /dev/null +++ b/lib/providers/invoice_provider.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import '../services/invoice_service.dart'; +import '../services/dio_client.dart'; + +class InvoiceProvider extends ChangeNotifier { + bool loading = false; + List invoices = []; + + Future loadInvoices(BuildContext context) async { + loading = true; + notifyListeners(); + + final service = InvoiceService(DioClient.getInstance(context)); + final res = await service.getAllInvoices(); + + if (res['success'] == true) { + invoices = res['invoices'] ?? []; + } + + loading = false; + notifyListeners(); + } +} diff --git a/lib/providers/mark_list_provider.dart b/lib/providers/mark_list_provider.dart new file mode 100644 index 0000000..d23fb84 --- /dev/null +++ b/lib/providers/mark_list_provider.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import '../services/mark_list_service.dart'; + +class MarkListProvider extends ChangeNotifier { + MarkListService? _service; + + List marks = []; + bool loading = false; + + void init(BuildContext context) { + _service = MarkListService(context); + } + + Future loadMarks(BuildContext context) async { + loading = true; + notifyListeners(); + + _service ??= MarkListService(context); + + final res = await _service!.getMarks(); + + if (res['success'] == true) { + marks = res['data'] ?? []; + } + + loading = false; + notifyListeners(); + } + + Future> addMark(BuildContext context, String mark, String origin, String destination) async { + _service ??= MarkListService(context); + + final res = await _service!.addMark({ + "mark_no": mark, + "origin": origin, + "destination": destination + }); + + if (res['success'] == true) { + await loadMarks(context); // refresh list + } + + return res; + } +} diff --git a/lib/providers/order_provider.dart b/lib/providers/order_provider.dart new file mode 100644 index 0000000..9a43e38 --- /dev/null +++ b/lib/providers/order_provider.dart @@ -0,0 +1,26 @@ +import 'package:flutter/cupertino.dart'; + +import '../services/order_service.dart'; + +class OrderProvider extends ChangeNotifier { + final OrderService service; + + OrderProvider(this.service); + + bool loading = false; + List orders = []; + + Future loadOrders() async { + loading = true; + notifyListeners(); + + final res = await service.getOrders(); + + if (res['success'] == true) { + orders = res['orders']; + } + + loading = false; + notifyListeners(); + } +} diff --git a/lib/providers/user_profile_provider.dart b/lib/providers/user_profile_provider.dart new file mode 100644 index 0000000..0249e63 --- /dev/null +++ b/lib/providers/user_profile_provider.dart @@ -0,0 +1,70 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import '../models/user_profile.dart'; +import '../services/user_profile_service.dart'; + +class UserProfileProvider extends ChangeNotifier { + UserProfileService? _service; + + UserProfile? profile; + bool loading = false; + + void init(BuildContext context) { + _service = UserProfileService(context); + } + + Future loadProfile(BuildContext context) async { + _service ??= UserProfileService(context); + + loading = true; + notifyListeners(); + + final res = await _service!.getProfile(); + + if (res['success'] == true) { + profile = UserProfile.fromJson(res['data']); + } + + loading = false; + notifyListeners(); + } + + Future updateProfileImage(BuildContext context, File image) async { + _service ??= UserProfileService(context); + + loading = true; + notifyListeners(); + + final res = await _service!.updateProfileImage(image); + + if (res['success'] == true) { + profile = UserProfile.fromJson(res['data']); + loading = false; + notifyListeners(); + return true; + } + + loading = false; + notifyListeners(); + return false; + } + + /// NEW: Send profile update request (admin approval required) + Future sendProfileUpdateRequest( + BuildContext context, + Map data, + ) async { + _service ??= UserProfileService(context); + + loading = true; + notifyListeners(); + + final res = await _service!.sendUpdateRequest(data); + + loading = false; + notifyListeners(); + + return res['success'] == true; + } +} + diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart new file mode 100644 index 0000000..35bc017 --- /dev/null +++ b/lib/screens/chat_screen.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class ChatScreen extends StatelessWidget { + const ChatScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: Text("Invoice Content He", style: TextStyle(fontSize: 18)), + ); + } +} diff --git a/lib/screens/dashboard_screen.dart b/lib/screens/dashboard_screen.dart index 4e95cbd..f51f4cc 100644 --- a/lib/screens/dashboard_screen.dart +++ b/lib/screens/dashboard_screen.dart @@ -1,35 +1,260 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/auth_provider.dart'; -import 'welcome_screen.dart'; +import '../providers/dashboard_provider.dart'; +import '../providers/mark_list_provider.dart'; +import 'mark_list_screen.dart'; -class DashboardScreen extends StatelessWidget { +class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); + @override + State createState() => _DashboardScreenState(); +} + +class _DashboardScreenState extends State { + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + final auth = Provider.of(context, listen: false); + + // STEP 1: Try refresh token BEFORE any API calls + await auth.tryRefreshToken(context); + + // STEP 2: Now safe to load dashboard + final dash = Provider.of(context, listen: false); + dash.init(context); + await dash.loadSummary(context); + + // STEP 3: Load marks AFTER refresh + final marks = Provider.of(context, listen: false); + marks.init(context); + await marks.loadMarks(context); + }); + } + + + void _showAddMarkForm() { + final markCtrl = TextEditingController(); + final originCtrl = TextEditingController(); + final destCtrl = TextEditingController(); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + builder: (_) { + return Padding( + padding: EdgeInsets.fromLTRB( + 18, + 18, + 18, + MediaQuery.of(context).viewInsets.bottom + 20, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "Add Mark No", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + + TextField(controller: markCtrl, decoration: const InputDecoration(labelText: "Mark No")), + const SizedBox(height: 12), + + TextField(controller: originCtrl, decoration: const InputDecoration(labelText: "Origin")), + const SizedBox(height: 12), + + TextField(controller: destCtrl, decoration: const InputDecoration(labelText: "Destination")), + const SizedBox(height: 20), + + ElevatedButton( + onPressed: () async { + final mark = markCtrl.text.trim(); + final origin = originCtrl.text.trim(); + final dest = destCtrl.text.trim(); + + if (mark.isEmpty || origin.isEmpty || dest.isEmpty) { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text("All fields required"))); + return; + } + + final provider = Provider.of(context, listen: false); + final res = await provider.addMark(context, mark, origin, dest); + + if (res['success'] == true) { + Navigator.pop(context); + } else { + final msg = res['message'] ?? "Failed"; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); + } + }, + child: const Text("Submit"), + ), + ], + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { final auth = Provider.of(context); + final dash = Provider.of(context); + final marks = Provider.of(context); + final name = auth.user?['customer_name'] ?? 'User'; - return Scaffold( - appBar: AppBar( - title: const Text("Dashboard"), - actions: [ - IconButton( - icon: const Icon(Icons.logout), - onPressed: () async { - await auth.logout(); - Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (_) => const WelcomeScreen()), (r) => false); - }, - ) + + if (dash.loading) { + return const Center(child: CircularProgressIndicator()); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // HEADER + Text( + "Welcome, $name 👋", + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 20), + + // ORDER SUMMARY + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _statBox("Active", dash.activeOrders, Colors.blue), + _statBox("In Transit", dash.inTransitOrders, Colors.orange), + _statBox("Delivered", dash.deliveredOrders, Colors.green), + ], + ), + const SizedBox(height: 20), + _valueCard("Total Value", dash.totalValue), + const SizedBox(height: 10), + _valueCard("Raw Amount", "₹${dash.totalRaw.toStringAsFixed(2)}"), + + const SizedBox(height: 30), + + // ADD + VIEW ALL BUTTONS SIDE BY SIDE + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton.icon( + icon: const Icon(Icons.add), + label: const Text("Add Mark No"), + onPressed: _showAddMarkForm, + ), + + if (marks.marks.length > 0) + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const MarkListScreen()), + ); + }, + child: const Text( + "View All →", + style: TextStyle( + fontSize: 16, + color: Colors.indigo, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + + const SizedBox(height: 20), + + // MARK LIST (only 10 latest) + const Text( + "Latest Mark Numbers", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + + const SizedBox(height: 10), + + if (marks.loading) + const Center(child: CircularProgressIndicator()) + else + Column( + children: List.generate( + marks.marks.length > 10 ? 10 : marks.marks.length, + (i) { + final m = marks.marks[i]; + return Card( + child: ListTile( + title: Text(m['mark_no']), + subtitle: Text("${m['origin']} → ${m['destination']}"), + trailing: Text( + m['status'], + style: const TextStyle(color: Colors.indigo), + ), + ), + ); + }, + ), + ), + + const SizedBox(height: 30), ], ), - body: Padding( - padding: const EdgeInsets.all(18), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Welcome, ${name}", style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), - const SizedBox(height: 12), - const Text("Your dashboard will appear here. Build shipment list, tracking, create shipment screens next."), - ]), + ); + } + + // UI WIDGETS + Widget _statBox(String title, int value, Color color) { + return Container( + width: 110, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Text(value.toString(), + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Text(title, style: const TextStyle(fontSize: 14)), + ], + ), + ); + } + + Widget _valueCard(String title, String value) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: Colors.indigo.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey)), + const SizedBox(height: 6), + Text( + value, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.indigo, + ), + ), + ], ), ); } diff --git a/lib/screens/edit_profile_screen.dart b/lib/screens/edit_profile_screen.dart new file mode 100644 index 0000000..c8aa497 --- /dev/null +++ b/lib/screens/edit_profile_screen.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/user_profile_provider.dart'; + +class EditProfileScreen extends StatefulWidget { + const EditProfileScreen({super.key}); + + @override + State createState() => _EditProfileScreenState(); +} + +class _EditProfileScreenState extends State { + final nameCtrl = TextEditingController(); + final companyCtrl = TextEditingController(); + final emailCtrl = TextEditingController(); + final mobileCtrl = TextEditingController(); + final addressCtrl = TextEditingController(); + final pincodeCtrl = TextEditingController(); + + @override + void initState() { + super.initState(); + final profile = Provider.of( + context, listen: false).profile; + + nameCtrl.text = profile?.customerName ?? ''; + companyCtrl.text = profile?.companyName ?? ''; + emailCtrl.text = profile?.email ?? ''; + mobileCtrl.text = profile?.mobile ?? ''; + addressCtrl.text = profile?.address ?? ''; + pincodeCtrl.text = profile?.pincode ?? ''; + } + + Future _submit() async { + final provider = + Provider.of(context, listen: false); + + final data = { + "customer_name": nameCtrl.text, + "company_name": companyCtrl.text, + "email": emailCtrl.text, + "mobile_no": mobileCtrl.text, + "address": addressCtrl.text, + "pincode": pincodeCtrl.text, + }; + + final success = + await provider.sendProfileUpdateRequest(context, data); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success + ? "Request submitted. Wait for admin approval." + : "Failed to submit request")), + ); + + if (success) Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Edit Profile")), + body: Padding( + padding: const EdgeInsets.all(18), + child: Column( + children: [ + _field("Name", nameCtrl), + _field("Company", companyCtrl), + _field("Email", emailCtrl), + _field("Mobile", mobileCtrl), + _field("Address", addressCtrl), + _field("Pincode", pincodeCtrl), + + const SizedBox(height: 20), + ElevatedButton( + onPressed: _submit, + child: const Text("Submit Update Request"), + ) + ], + ), + ), + ); + } + + Widget _field(String title, TextEditingController ctrl) { + return Padding( + padding: const EdgeInsets.only(bottom: 14), + child: TextField( + controller: ctrl, + decoration: InputDecoration( + labelText: title, + border: OutlineInputBorder(), + ), + ), + ); + } +} diff --git a/lib/screens/invoice_detail_screen.dart b/lib/screens/invoice_detail_screen.dart new file mode 100644 index 0000000..8a8f19e --- /dev/null +++ b/lib/screens/invoice_detail_screen.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import '../services/dio_client.dart'; +import '../services/invoice_service.dart'; + +class InvoiceDetailScreen extends StatefulWidget { + final int invoiceId; + const InvoiceDetailScreen({super.key, required this.invoiceId}); + + @override + State createState() => _InvoiceDetailScreenState(); +} + +class _InvoiceDetailScreenState extends State { + bool loading = true; + Map invoice = {}; + + @override + void initState() { + super.initState(); + load(); + } + + Future load() async { + final service = InvoiceService(DioClient.getInstance(context)); + final res = await service.getInvoiceDetails(widget.invoiceId); + + if (res['success'] == true) { + invoice = res['invoice'] ?? {}; + } + + loading = false; + setState(() {}); + } + + /// ---------- REUSABLE ROW ---------- + Widget row(String label, dynamic value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)), + Expanded( + child: Text( + value?.toString().isNotEmpty == true ? value.toString() : "N/A", + textAlign: TextAlign.end, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Invoice Details")), + body: loading + ? const Center(child: CircularProgressIndicator()) + + /// ================ INVOICE DATA ================ + : Padding( + padding: const EdgeInsets.all(16), + child: ListView( + children: [ + /// -------- Invoice Summary -------- + const Text( + "Invoice Summary", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + row("Invoice No", invoice['invoice_number']), + row("Invoice Date", invoice['invoice_date']), + row("Due Date", invoice['due_date']), + row("Status", invoice['status']), + + const Divider(height: 30), + + /// -------- Customer Details -------- + const Text( + "Customer Details", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + row("Name", invoice['customer_name']), + row("Company", invoice['company_name']), + row("Email", invoice['customer_email']), + row("Mobile", invoice['customer_mobile']), + row("Address", invoice['customer_address']), + row("Pincode", invoice['pincode']), + + const Divider(height: 30), + + /// -------- Amounts & Taxes -------- + const Text( + "Amounts", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + row("Final Amount", invoice['final_amount']), + row("Tax Type", invoice['tax_type']), + row("GST %", invoice['gst_percent']), + row("GST Amount", invoice['gst_amount']), + row("Final with GST", invoice['final_amount_with_gst']), + + const Divider(height: 30), + + /// -------- Payment Details -------- + const Text( + "Payment Details", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + row("Payment Method", invoice['payment_method']), + row("Reference No", invoice['reference_no']), + row("Notes", invoice['notes']), + + const Divider(height: 30), + + /// -------- PDF -------- + if (invoice['pdf_path'] != null) + ElevatedButton.icon( + icon: const Icon(Icons.picture_as_pdf), + label: const Text("Download PDF"), + onPressed: () {}, + style: + ElevatedButton.styleFrom(backgroundColor: Colors.red), + ), + + const SizedBox(height: 20), + + /// -------- Invoice Items -------- + if (invoice['items'] != null) + const Text( + "Invoice Items", + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + ...List.generate(invoice['items']?.length ?? 0, (i) { + final item = invoice['items'][i]; + return Card( + child: ListTile( + title: Text(item['description'] ?? "Item"), + subtitle: Text("Qty: ${item['qty'] ?? 0}"), + trailing: Text( + "₹${item['ttl_amount'] ?? 0}", + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.indigo), + ), + ), + ); + }), + ], + ), + ), + ); + } +} diff --git a/lib/screens/invoice_screen.dart b/lib/screens/invoice_screen.dart new file mode 100644 index 0000000..2838db9 --- /dev/null +++ b/lib/screens/invoice_screen.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/invoice_installment_screen.dart'; +import '../providers/invoice_provider.dart'; +import '../services/dio_client.dart'; +import '../services/invoice_service.dart'; +import 'invoice_detail_screen.dart'; + + +class InvoiceScreen extends StatefulWidget { + const InvoiceScreen({super.key}); + + @override + State createState() => _InvoiceScreenState(); +} + +class _InvoiceScreenState extends State { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + Provider.of(context, listen: false) + .loadInvoices(context); + }); + } + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); + + if (provider.loading) { + return const Center(child: CircularProgressIndicator()); + } + + if (provider.invoices.isEmpty) { + return const Center( + child: Text("No invoices found", style: TextStyle(fontSize: 18))); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: provider.invoices.length, + itemBuilder: (_, i) { + final inv = provider.invoices[i]; + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Padding( + padding: const EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Invoice ${inv['invoice_number'] ?? 'N/A'}", + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + + const SizedBox(height: 6), + Text("Date: ${inv['invoice_date'] ?? 'N/A'}"), + Text("Status: ${inv['status'] ?? 'N/A'}"), + Text("Amount: ₹${inv['formatted_amount'] ?? '0'}"), + + const SizedBox(height: 10), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + OutlinedButton( + child: const Text("Invoice Details"), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => InvoiceDetailScreen( + invoiceId: inv['invoice_id'], + ), + ), + ); + }, + ), + + OutlinedButton( + child: const Text("Installments"), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => InvoiceInstallmentScreen( + invoiceId: inv['invoice_id'], + ), + ), + ); + }, + ), + ], + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 1ffcb42..7977597 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:kent_logistics_app/screens/welcome_screen.dart'; import 'package:provider/provider.dart'; import '../providers/auth_provider.dart'; import '../widgets/rounded_input.dart'; import '../widgets/primary_button.dart'; -import 'dashboard_screen.dart'; +import 'main_bottom_nav.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -25,21 +26,23 @@ class _LoginScreenState extends State { Future _login() async { final auth = Provider.of(context, listen: false); - // Basic validation final loginId = cLoginId.text.trim(); final password = cPassword.text.trim(); + if (loginId.isEmpty || password.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter login id and password'))); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please enter login id and password')), + ); return; } - final res = await auth.login(loginId, password); + final res = await auth.login(context, loginId, password); - // Your controller returns { success: true/false, message, token, user } if (res['success'] == true) { - // Navigate to dashboard if (!mounted) return; - Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const DashboardScreen())); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const MainBottomNav()), + ); } else { final msg = res['message']?.toString() ?? 'Login failed'; if (!mounted) return; @@ -51,15 +54,35 @@ class _LoginScreenState extends State { Widget build(BuildContext context) { final auth = Provider.of(context); final width = MediaQuery.of(context).size.width; + return Scaffold( - appBar: AppBar(title: const Text('Login')), + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const WelcomeScreen()), + ); + }, + ), + title: const Text('Login'), + ), + body: Padding( padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: 20), child: Column( children: [ - RoundedInput(controller: cLoginId, hint: 'Email / Mobile / Customer ID', keyboardType: TextInputType.text), + RoundedInput( + controller: cLoginId, + hint: 'Email / Mobile / Customer ID', + ), const SizedBox(height: 12), - RoundedInput(controller: cPassword, hint: 'Password', obscure: true), + RoundedInput( + controller: cPassword, + hint: 'Password', + obscure: true, + ), const SizedBox(height: 18), PrimaryButton(label: 'Login', onTap: _login, busy: auth.loading), ], diff --git a/lib/screens/main_bottom_nav.dart b/lib/screens/main_bottom_nav.dart new file mode 100644 index 0000000..2c6b519 --- /dev/null +++ b/lib/screens/main_bottom_nav.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import '../widgets/main_app_bar.dart'; +import 'dashboard_screen.dart'; +import 'order_screen.dart'; +import 'invoice_screen.dart'; +import 'chat_screen.dart'; +import 'settings_screen.dart'; + +class MainBottomNav extends StatefulWidget { + const MainBottomNav({super.key}); + + @override + State createState() => MainBottomNavState(); +} + +class MainBottomNavState extends State { + int _currentIndex = 0; + + void setIndex(int index) { + setState(() { + _currentIndex = index; + }); + } + + final List _screens = const [ + DashboardScreen(), + OrdersScreen(), + InvoiceScreen(), + ChatScreen(), + SettingsScreen(), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const MainAppBar(), + body: _screens[_currentIndex], + bottomNavigationBar: BottomNavigationBar( + currentIndex: _currentIndex, + selectedItemColor: Colors.red, + unselectedItemColor: Colors.black, + type: BottomNavigationBarType.fixed, + onTap: (index) { + setState(() => _currentIndex = index); + }, + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.dashboard_outlined), label: "Dashboard"), + BottomNavigationBarItem(icon: Icon(Icons.shopping_bag_outlined), label: "Orders"), + BottomNavigationBarItem(icon: Icon(Icons.receipt_long_outlined), label: "Invoice"), + BottomNavigationBarItem(icon: Icon(Icons.chat_bubble_outline), label: "Chat"), + BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), label: "Settings"), + ], + ), + ); + } +} diff --git a/lib/screens/mark_list_screen.dart b/lib/screens/mark_list_screen.dart new file mode 100644 index 0000000..ee17ca4 --- /dev/null +++ b/lib/screens/mark_list_screen.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/mark_list_provider.dart'; + +class MarkListScreen extends StatefulWidget { + const MarkListScreen({super.key}); + + @override + State createState() => _MarkListScreenState(); +} + +class _MarkListScreenState extends State { + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final provider = Provider.of(context, listen: false); + provider.init(context); + provider.loadMarks(context); // Load full list again + }); + } + + @override + Widget build(BuildContext context) { + final marks = Provider.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text("All Mark Numbers"), + ), + + body: marks.loading + ? const Center(child: CircularProgressIndicator()) + : ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: marks.marks.length, + itemBuilder: (_, i) { + final m = marks.marks[i]; + + return Card( + child: ListTile( + title: Text(m['mark_no']), + subtitle: Text("${m['origin']} → ${m['destination']}"), + trailing: Text( + m['status'], + style: const TextStyle(color: Colors.indigo), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/screens/order_detail_screen.dart b/lib/screens/order_detail_screen.dart new file mode 100644 index 0000000..0ac3755 --- /dev/null +++ b/lib/screens/order_detail_screen.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import '../services/dio_client.dart'; +import '../services/order_service.dart'; + + +class OrderDetailScreen extends StatefulWidget { + final String orderId; + const OrderDetailScreen({super.key, required this.orderId}); + + @override + State createState() => _OrderDetailScreenState(); +} + +class _OrderDetailScreenState extends State { + bool loading = true; + Map order = {}; + + @override + void initState() { + super.initState(); + load(); + } + + Future load() async { + final service = OrderService(DioClient.getInstance(context)); + final res = await service.getOrderDetails(widget.orderId); + + if (res['success'] == true) { + order = res['order']; + } + + loading = false; + setState(() {}); + } + + Widget _row(String label, dynamic value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: const TextStyle(fontSize: 14, color: Colors.grey)), + Text(value?.toString() ?? 'N/A', + style: const TextStyle( + fontSize: 15, fontWeight: FontWeight.w600)), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final items = order['items'] ?? []; + + return Scaffold( + appBar: AppBar(title: const Text("Order Details")), + body: loading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(16), + child: ListView( + children: [ + // ---------------- ORDER SUMMARY ---------------- + const Text( + "Order Summary", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + _row("Order ID", order['order_id']), + _row("Mark No", order['mark_no']), + _row("Origin", order['origin']), + _row("Destination", order['destination']), + _row("Status", order['status']), + + const Divider(height: 30), + + const Text( + "Totals", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + _row("CTN", order['ctn']), + _row("Qty", order['qty']), + _row("Total Qty", order['ttl_qty']), + _row("Amount", "₹${order['ttl_amount'] ?? 0}"), + _row("CBM", order['cbm']), + _row("Total CBM", order['ttl_cbm']), + _row("KG", order['kg']), + _row("Total KG", order['ttl_kg']), + + const Divider(height: 30), + + // ---------------- ORDER ITEMS ---------------- + const Text( + "Order Items", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + ...List.generate(items.length, (i) { + final item = items[i]; + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item['description'] ?? "No description", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600), + ), + const SizedBox(height: 6), + + _row("Qty", item['qty']), + _row("Unit", item['unit']), + _row("CBM", item['cbm']), + _row("KG", item['kg']), + _row("Amount", "₹${item['ttl_amount'] ?? 0}"), + _row("Shop No", item['shop_no']), + ], + ), + ), + ); + }), + ], + ), + ), + ); + } +} diff --git a/lib/screens/order_invoice_screen.dart b/lib/screens/order_invoice_screen.dart new file mode 100644 index 0000000..51fc98a --- /dev/null +++ b/lib/screens/order_invoice_screen.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import '../services/dio_client.dart'; +import '../services/order_service.dart'; + + +class OrderInvoiceScreen extends StatefulWidget { + final String orderId; + const OrderInvoiceScreen({super.key, required this.orderId}); + + @override + State createState() => _OrderInvoiceScreenState(); +} + +class _OrderInvoiceScreenState extends State { + bool loading = true; + Map invoice = {}; + + @override + void initState() { + super.initState(); + load(); + } + + Future load() async { + final service = OrderService(DioClient.getInstance(context)); + final res = await service.getInvoice(widget.orderId); + + if (res['success'] == true) { + invoice = res['invoice'] ?? {}; + } + + loading = false; + setState(() {}); + } + + Widget _row(String label, dynamic value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: const TextStyle(fontSize: 14, color: Colors.grey)), + Text(value?.toString() ?? "N/A", + style: + const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final items = invoice['items'] ?? []; + + return Scaffold( + appBar: AppBar(title: const Text("Invoice")), + body: loading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(16), + child: ListView( + children: [ + const Text("Invoice Summary", + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + + _row("Invoice No", invoice['invoice_number']), + _row("Invoice Date", invoice['invoice_date']), + _row("Due Date", invoice['due_date']), + _row("Payment Method", invoice['payment_method']), + _row("Reference No", invoice['reference_no']), + _row("Status", invoice['status']), + + const Divider(height: 30), + + const Text("Amount Details", + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + + _row("Amount (without GST)", invoice['final_amount']), + _row("GST Amount", invoice['gst_amount']), + _row("Final Amount With GST", + invoice['final_amount_with_gst']), + _row("Tax Type", invoice['tax_type']), + _row("CGST %", invoice['cgst_percent']), + _row("SGST %", invoice['sgst_percent']), + _row("IGST %", invoice['igst_percent']), + + const Divider(height: 30), + + _row("Customer Name", invoice['customer_name']), + _row("Company Name", invoice['company_name']), + _row("Email", invoice['customer_email']), + _row("Mobile", invoice['customer_mobile']), + _row("Address", invoice['customer_address']), + _row("Pincode", invoice['pincode']), + _row("Notes", invoice['notes']), + + const Divider(height: 30), + + // PDF DOWNLOAD + if (invoice['pdf_path'] != null) + TextButton.icon( + onPressed: () { + // open pdf + }, + icon: const Icon(Icons.picture_as_pdf, color: Colors.red), + label: const Text("Download PDF"), + ), + + const SizedBox(height: 20), + + const Text("Invoice Items", + style: + TextStyle(fontSize: 17, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + + ...List.generate(items.length, (i) { + final item = items[i]; + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + title: Text(item['description'] ?? "Item"), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Qty: ${item['qty'] ?? 0}"), + Text("Price: ₹${item['price'] ?? 0}"), + ], + ), + trailing: Text( + "₹${item['ttl_amount'] ?? 0}", + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.indigo), + ), + ), + ); + }), + ], + ), + ), + ); + } +} diff --git a/lib/screens/order_screen.dart b/lib/screens/order_screen.dart new file mode 100644 index 0000000..9d4f559 --- /dev/null +++ b/lib/screens/order_screen.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/order_provider.dart'; +import 'order_detail_screen.dart'; +import 'order_shipment_screen.dart'; +import 'order_invoice_screen.dart'; +import 'order_track_screen.dart'; + +class OrdersScreen extends StatefulWidget { + const OrdersScreen({super.key}); + + @override + State createState() => _OrdersScreenState(); +} + +class _OrdersScreenState extends State { + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + Provider.of(context, listen: false).loadOrders(); + }); + } + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); + + if (provider.loading) { + return const Center(child: CircularProgressIndicator()); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: provider.orders.length, + itemBuilder: (_, i) { + final o = provider.orders[i]; + + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order ID: ${o['order_id']}", + style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Text(o['description']), + Text("₹ ${o['amount']}"), + Text(o['status'], style: const TextStyle(color: Colors.indigo)), + + const SizedBox(height: 10), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _btn("Order", () => _openOrderDetails(o['order_id'])), + _btn("Shipment", () => _openShipment(o['order_id'])), + _btn("Invoice", () => _openInvoice(o['order_id'])), + _btn("Track", () => _openTrack(o['order_id'])), + ], + ) + ], + ), + ), + ); + }, + ); + } + + Widget _btn(String text, VoidCallback onTap) { + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Colors.indigo.shade50, + ), + child: Text(text, style: const TextStyle(color: Colors.indigo)), + ), + ); + } + + void _openOrderDetails(String id) { + Navigator.push(context, MaterialPageRoute( + builder: (_) => OrderDetailScreen(orderId: id))); + } + + void _openShipment(String id) { + Navigator.push(context, MaterialPageRoute( + builder: (_) => OrderShipmentScreen(orderId: id))); + } + + void _openInvoice(String id) { + Navigator.push(context, MaterialPageRoute( + builder: (_) => OrderInvoiceScreen(orderId: id))); + } + + void _openTrack(String id) { + Navigator.push(context, MaterialPageRoute( + builder: (_) => OrderTrackScreen(orderId: id))); + } +} diff --git a/lib/screens/order_shipment_screen.dart b/lib/screens/order_shipment_screen.dart new file mode 100644 index 0000000..c3f5626 --- /dev/null +++ b/lib/screens/order_shipment_screen.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import '../services/dio_client.dart'; +import '../services/order_service.dart'; + + +class OrderShipmentScreen extends StatefulWidget { + final String orderId; + const OrderShipmentScreen({super.key, required this.orderId}); + + @override + State createState() => _OrderShipmentScreenState(); +} + +class _OrderShipmentScreenState extends State { + bool loading = true; + Map? shipment; // nullable + + @override + void initState() { + super.initState(); + load(); + } + + Future load() async { + final service = OrderService(DioClient.getInstance(context)); + final res = await service.getShipment(widget.orderId); + + if (res['success'] == true) { + shipment = res['shipment']; // may be null + } + + loading = false; + setState(() {}); + } + + Widget _row(String label, dynamic value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: const TextStyle(fontSize: 14, color: Colors.grey)), + Text(value?.toString() ?? "N/A", + style: const TextStyle( + fontSize: 15, fontWeight: FontWeight.w600)), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Shipment Details")), + body: loading + ? const Center(child: CircularProgressIndicator()) + + // --------------------------------------- + // 🚨 CASE 1: Shipment NOT created yet + // --------------------------------------- + : (shipment == null) + ? const Center( + child: Text( + "Order not shipped yet", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey), + ), + ) + + // --------------------------------------- + // 🚛 CASE 2: Shipment available + // --------------------------------------- + : Padding( + padding: const EdgeInsets.all(16), + child: ListView( + children: [ + const Text( + "Shipment Summary", + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + _row("Shipment ID", shipment!['shipment_id']), + _row("Status", shipment!['status']), + _row("Shipment Date", shipment!['shipment_date']), + _row("Origin", shipment!['origin']), + _row("Destination", shipment!['destination']), + + const Divider(height: 30), + + const Text( + "Totals", + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + + _row("Total CTN", shipment!['total_ctn']), + _row("Total Qty", shipment!['total_qty']), + _row("Total TTL Qty", shipment!['total_ttl_qty']), + _row("Total Amount", shipment!['total_amount']), + _row("Total CBM", shipment!['total_cbm']), + _row("Total TTL CBM", shipment!['total_ttl_cbm']), + _row("Total KG", shipment!['total_kg']), + _row("Total TTL KG", shipment!['total_ttl_kg']), + + const Divider(height: 30), + + _row("Meta", shipment!['meta']), + + const Divider(height: 30), + + const Text( + "Shipment Items", + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + ...List.generate(shipment!['items']?.length ?? 0, (i) { + final item = shipment!['items'][i]; + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + // 🔹 Title: Order ID + Text( + "Order ID: ${item['order_id'] ?? 'N/A'}", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + + // 🔹 Mark No (optional) + if (item['mark_no'] != null) + Text( + "Mark No: ${item['mark_no']}", + style: const TextStyle(fontSize: 14), + ), + + const SizedBox(height: 6), + + // 🔹 Total Quantity + Text("Total Qty: ${item['total_ttl_qty'] ?? 0}"), + + // 🔹 Total CBM (optional) + if (item['total_ttl_cbm'] != null) + Text("Total CBM: ${item['total_ttl_cbm']}"), + + // 🔹 Total KG (optional) + if (item['total_ttl_kg'] != null) + Text("Total KG: ${item['total_ttl_kg']}"), + + const SizedBox(height: 6), + + // 🔹 Total Amount + Text( + "Amount: ₹${item['total_amount'] ?? 0}", + style: const TextStyle( + color: Colors.indigo, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + }) + + ], + ), + ), + ); + } +} diff --git a/lib/screens/order_track_screen.dart b/lib/screens/order_track_screen.dart new file mode 100644 index 0000000..4e405d2 --- /dev/null +++ b/lib/screens/order_track_screen.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import '../services/dio_client.dart'; +import '../services/order_service.dart'; + + +class OrderTrackScreen extends StatefulWidget { + final String orderId; + const OrderTrackScreen({super.key, required this.orderId}); + + @override + State createState() => _OrderTrackScreenState(); +} + +class _OrderTrackScreenState extends State { + bool loading = true; + Map data = {}; + + @override + void initState() { + super.initState(); + load(); + } + + Future load() async { + final service = OrderService(DioClient.getInstance(context)); + final res = await service.trackOrder(widget.orderId); + + if (res['success'] == true) { + data = res['track']; + } + + loading = false; + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Track Order")), + body: loading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order ID: ${data['order_id']}"), + Text("Shipment Status: ${data['shipment_status']}"), + Text("Shipment Date: ${data['shipment_date']}"), + + const SizedBox(height: 20), + Center( + child: Icon( + Icons.local_shipping, + size: 100, + color: Colors.indigo.shade300, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/otp_screen.dart b/lib/screens/otp_screen.dart index e9b7dd1..c882471 100644 --- a/lib/screens/otp_screen.dart +++ b/lib/screens/otp_screen.dart @@ -33,7 +33,7 @@ class _OtpScreenState extends State { setState(() => verifying = true); // send signup payload to backend - final res = await RequestService().sendSignup(widget.signupPayload); + final res = await RequestService(context).sendSignup(widget.signupPayload); setState(() => verifying = false); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart new file mode 100644 index 0000000..2d020fa --- /dev/null +++ b/lib/screens/settings_screen.dart @@ -0,0 +1,193 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; +import '../providers/auth_provider.dart'; +import '../providers/user_profile_provider.dart'; +import 'edit_profile_screen.dart'; +import 'login_screen.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + final auth = Provider.of(context, listen: false); + + while (!auth.initialized) { + await Future.delayed(const Duration(milliseconds: 100)); + } + + final profileProvider = + Provider.of(context, listen: false); + profileProvider.init(context); + + await profileProvider.loadProfile(context); + }); + } + + Future _pickImage() async { + final picked = await ImagePicker().pickImage( + source: ImageSource.gallery, + imageQuality: 80, + ); + + if (picked != null) { + final file = File(picked.path); + final profileProvider = + Provider.of(context, listen: false); + + profileProvider.init(context); + + final success = await profileProvider.updateProfileImage(context, file); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(success ? "Profile updated" : "Failed to update")), + ); + } + } + + Future _logout() async { + final auth = Provider.of(context, listen: false); + + final confirm = await showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text("Logout"), + content: const Text("Are you sure you want to logout?"), + actions: [ + TextButton( + child: const Text("Cancel"), + onPressed: () => Navigator.pop(context, false), + ), + TextButton( + child: const Text("Logout", style: TextStyle(color: Colors.red)), + onPressed: () => Navigator.pop(context, true), + ), + ], + ), + ); + + if (confirm == true) { + await auth.logout(context); + + if (!mounted) return; + + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const LoginScreen()), + (route) => false, + ); + } + } + + @override + Widget build(BuildContext context) { + final profileProvider = Provider.of(context); + + if (profileProvider.loading || profileProvider.profile == null) { + return const Center(child: CircularProgressIndicator()); + } + + final p = profileProvider.profile!; + + return SingleChildScrollView( + padding: const EdgeInsets.all(18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ------------------ PROFILE IMAGE ------------------ + Center( + child: GestureDetector( + onTap: _pickImage, + child: CircleAvatar( + radius: 55, + backgroundImage: p.profileImage != null + ? NetworkImage(p.profileImage!) + : null, + child: p.profileImage == null + ? const Icon(Icons.person, size: 55) + : null, + ), + ), + ), + + const SizedBox(height: 25), + + // ------------------ PROFILE INFO ------------------ + _infoRow("Customer ID", p.customerId), + _infoRow("Name", p.customerName), + _infoRow("Company", p.companyName), + _infoRow("Email", p.email), + _infoRow("Mobile", p.mobile), + _infoRow("Address", p.address ?? "Not provided"), + _infoRow("Pincode", p.pincode ?? "Not provided"), + _infoRow("Status", p.status ?? "N/A"), + _infoRow("Customer Type", p.customerType ?? "N/A"), + + const SizedBox(height: 40), + + Center( + child: ElevatedButton( + child: const Text("Edit Profile"), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const EditProfileScreen()), + ); + }, + ), + ), + + // ------------------ LOGOUT BUTTON ------------------ + Center( + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14), + ), + icon: const Icon(Icons.logout, color: Colors.white), + label: const Text( + "Logout", + style: TextStyle(color: Colors.white, fontSize: 16), + ), + onPressed: _logout, + ), + ), + + + + + const SizedBox(height: 30), + ], + ), + ); + } + + Widget _infoRow(String title, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.grey)), + const SizedBox(height: 4), + Text(value, style: const TextStyle(fontSize: 16)), + ], + ), + ); + } +} diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index a966540..6932d3b 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -3,10 +3,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/auth_provider.dart'; import 'dashboard_screen.dart'; +import 'main_bottom_nav.dart'; import 'welcome_screen.dart'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); + @override State createState() => _SplashScreenState(); } @@ -19,15 +21,23 @@ class _SplashScreenState extends State { } void _init() async { - // small delay to show logo - await Future.delayed(const Duration(milliseconds: 900)); + await Future.delayed(const Duration(milliseconds: 500)); + final auth = Provider.of(context, listen: false); - // ensure provider has loaded prefs - await Future.delayed(const Duration(milliseconds: 300)); + + // 🟢 IMPORTANT → WAIT FOR PREFERENCES TO LOAD + await auth.init(); + + if (!mounted) return; + if (auth.isLoggedIn) { - Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const DashboardScreen())); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const MainBottomNav()), + ); } else { - Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const WelcomeScreen())); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const WelcomeScreen()), + ); } } @@ -40,11 +50,23 @@ class _SplashScreenState extends State { Container( width: size.width * 0.34, height: size.width * 0.34, - decoration: BoxDecoration(shape: BoxShape.circle, color: Theme.of(context).primaryColor.withOpacity(0.14)), - child: Center(child: Text("K", style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor))), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).primaryColor.withOpacity(0.14), + ), + child: Center( + child: Text( + "K", + style: TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor), + ), + ), ), const SizedBox(height: 18), - const Text("Kent Logistics", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + const Text("Kent Logistics", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), ]), ), ); diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 949316f..f901088 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,16 +1,16 @@ import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; import '../config/api_config.dart'; +import 'dio_client.dart'; class AuthService { - final Dio _dio = Dio(BaseOptions( - baseUrl: ApiConfig.baseUrl, - connectTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 15), - // You can add headers here if needed: - // headers: {'Accept': 'application/json'}, - )); + late final Dio _dio; - /// Calls /api/user/login with login_id and password + AuthService(BuildContext context) { + _dio = DioClient.getInstance(context); + } + + /// Login API Future> login(String loginId, String password) async { try { final response = await _dio.post('/user/login', data: { @@ -18,45 +18,44 @@ class AuthService { 'password': password, }); - // Ensure we return a Map - if (response.data is Map) { - return Map.from(response.data); - } else { - return { - 'success': false, - 'message': 'Invalid response from server', - }; - } + return Map.from(response.data); } on DioException catch (e) { - // Try to extract message from server response - dynamic respData = e.response?.data; - String message = 'Login failed'; - if (respData is Map && respData['message'] != null) { - message = respData['message'].toString(); - } else if (e.message != null) { - message = e.message!; - } + final data = e.response?.data; return { 'success': false, - 'message': message, + 'message': data is Map && data['message'] != null + ? data['message'] + : e.message ?? 'Login failed' }; - } catch (e) { - return { - 'success': false, - 'message': e.toString(), - }; - } - } - - /// Optional: logout (if you have logout endpoint) - Future> logout(String token) async { - try { - final Dio dio = Dio(BaseOptions(baseUrl: ApiConfig.baseUrl)); - dio.options.headers['Authorization'] = 'Bearer $token'; - final response = await dio.post('/user/logout'); - return Map.from(response.data ?? {'success': true}); } catch (e) { return {'success': false, 'message': e.toString()}; } } + + /// Logout API + Future> logout() async { + try { + final response = await _dio.post('/user/logout'); + return Map.from(response.data); + } catch (e) { + return {'success': false, 'message': e.toString()}; + } + } + + /// Refresh token + Future> refreshToken(String oldToken) async { + try { + final response = await _dio.post( + '/user/refresh', + options: Options(headers: { + 'Authorization': 'Bearer $oldToken', + }), + ); + + return Map.from(response.data); + } on DioException catch (e) { + final msg = e.response?.data?['message'] ?? 'Refresh failed'; + return {'success': false, 'message': msg}; + } + } } diff --git a/lib/services/dashboard_service.dart b/lib/services/dashboard_service.dart new file mode 100644 index 0000000..052cf7e --- /dev/null +++ b/lib/services/dashboard_service.dart @@ -0,0 +1,20 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'dio_client.dart'; + +class DashboardService { + late final Dio _dio; + + DashboardService(BuildContext context) { + _dio = DioClient.getInstance(context); + } + + Future> getSummary() async { + try { + final res = await _dio.get('/user/order-summary'); + return Map.from(res.data); + } catch (e) { + return {'status': false, 'message': e.toString()}; + } + } +} diff --git a/lib/services/dio_client.dart b/lib/services/dio_client.dart new file mode 100644 index 0000000..3d41978 --- /dev/null +++ b/lib/services/dio_client.dart @@ -0,0 +1,33 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../config/api_config.dart'; +import '../providers/auth_provider.dart'; +import 'token_interceptor.dart'; + +class DioClient { + static Dio? _dio; // Singleton instance + + static Dio getInstance(BuildContext context) { + if (_dio == null) { + _dio = Dio( + BaseOptions( + baseUrl: ApiConfig.baseUrl, + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 15), + headers: { + "Accept": "application/json", + }, + ), + ); + + final authProvider = Provider.of(context, listen: false); + + _dio!.interceptors.add( + TokenInterceptor(authProvider, context, _dio!), + ); + } + + return _dio!; + } +} diff --git a/lib/services/invoice_service.dart b/lib/services/invoice_service.dart new file mode 100644 index 0000000..9fb4a48 --- /dev/null +++ b/lib/services/invoice_service.dart @@ -0,0 +1,34 @@ +import 'package:dio/dio.dart'; + +class InvoiceService { + final Dio dio; + InvoiceService(this.dio); + + Future> getAllInvoices() async { + try { + final res = await dio.get("/user/invoices"); + return Map.from(res.data); + } catch (e) { + return {"success": false, "message": e.toString()}; + } + } + + Future> getInstallments(int invoiceId) async { + try { + final res = await dio.get("/user/invoice/$invoiceId/installments"); + return Map.from(res.data); + } catch (e) { + return {"success": false, "message": e.toString()}; + } + } + + /// 🔵 NEW FUNCTION — Fetch Full Invoice Details + Future> getInvoiceDetails(int invoiceId) async { + try { + final res = await dio.get("/user/invoice/$invoiceId/details"); + return Map.from(res.data); + } catch (e) { + return {"success": false, "message": e.toString()}; + } + } +} diff --git a/lib/services/mark_list_service.dart b/lib/services/mark_list_service.dart new file mode 100644 index 0000000..3e05578 --- /dev/null +++ b/lib/services/mark_list_service.dart @@ -0,0 +1,29 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'dio_client.dart'; + +class MarkListService { + late Dio _dio; + + MarkListService(BuildContext context) { + _dio = DioClient.getInstance(context); + } + + Future> addMark(Map data) async { + try { + final res = await _dio.post('/add-mark-no', data: data); + return Map.from(res.data); + } catch (e) { + return {'success': false, 'message': e.toString()}; + } + } + + Future> getMarks() async { + try { + final res = await _dio.get('/show-mark-list'); + return Map.from(res.data); + } catch (e) { + return {'success': false, 'message': e.toString()}; + } + } +} diff --git a/lib/services/order_service.dart b/lib/services/order_service.dart new file mode 100644 index 0000000..04f1aaa --- /dev/null +++ b/lib/services/order_service.dart @@ -0,0 +1,32 @@ +import 'package:dio/dio.dart'; + +class OrderService { + final Dio _dio; + + OrderService(this._dio); + + Future> getOrders() async { + final res = await _dio.get('/user/orders'); + return res.data; + } + + Future> getOrderDetails(String id) async { + final res = await _dio.get('/user/order/$id/details'); + return res.data; + } + + Future> getShipment(String id) async { + final res = await _dio.get('/user/order/$id/shipment'); + return res.data; + } + + Future> getInvoice(String id) async { + final res = await _dio.get('/user/order/$id/invoice'); + return res.data; + } + + Future> trackOrder(String id) async { + final res = await _dio.get('/user/order/$id/track'); + return res.data; + } +} diff --git a/lib/services/request_service.dart b/lib/services/request_service.dart index 2544318..83b543d 100644 --- a/lib/services/request_service.dart +++ b/lib/services/request_service.dart @@ -1,18 +1,19 @@ import 'package:dio/dio.dart'; -import '../config/api_config.dart'; +import 'package:flutter/material.dart'; +import 'dio_client.dart'; class RequestService { - final Dio _dio = Dio( - BaseOptions( - baseUrl: ApiConfig.baseUrl, - connectTimeout: const Duration(seconds: 15), - ), - ); - /// Send signup request to backend (after OTP verified) + late final Dio _dio; + + RequestService(BuildContext context) { + _dio = DioClient.getInstance(context); + } + + /// Signup request after OTP Future> sendSignup(Map payload) async { try { final resp = await _dio.post('/signup-request', data: payload); - return resp.data is Map ? Map.from(resp.data) : {'status': true, 'message': 'OK'}; + return Map.from(resp.data); } on DioException catch (e) { return { 'status': false, diff --git a/lib/services/token_interceptor.dart b/lib/services/token_interceptor.dart new file mode 100644 index 0000000..76c9e73 --- /dev/null +++ b/lib/services/token_interceptor.dart @@ -0,0 +1,37 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import '../providers/auth_provider.dart'; + +class TokenInterceptor extends Interceptor { + final AuthProvider auth; + final BuildContext context; + final Dio dio; + + TokenInterceptor(this.auth, this.context, this.dio); + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + if (auth.token != null) { + options.headers['Authorization'] = 'Bearer ${auth.token}'; + } + handler.next(options); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) async { + if (err.response?.statusCode == 401 && + err.response?.data['message'] == 'Token has expired') { + + final refreshed = await auth.tryRefreshToken(context); + + if (refreshed) { + err.requestOptions.headers['Authorization'] = 'Bearer ${auth.token}'; + final newResponse = await dio.fetch(err.requestOptions); + return handler.resolve(newResponse); + } + } + + handler.next(err); + } +} + diff --git a/lib/services/user_profile_service.dart b/lib/services/user_profile_service.dart new file mode 100644 index 0000000..5ac3460 --- /dev/null +++ b/lib/services/user_profile_service.dart @@ -0,0 +1,48 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'dio_client.dart'; + +class UserProfileService { + late final Dio _dio; + + UserProfileService(BuildContext context) { + _dio = DioClient.getInstance(context); + } + + /// Get profile + Future> getProfile() async { + try { + final res = await _dio.get('/user/profile'); + return Map.from(res.data); + } catch (e) { + return {"success": false, "message": e.toString()}; + } + } + + /// Update profile IMAGE only + Future> updateProfileImage(File image) async { + try { + final form = FormData.fromMap({ + "profile_image": await MultipartFile.fromFile(image.path), + }); + + final res = await _dio.post('/user/profile-image', data: form); + return Map.from(res.data); + + } catch (e) { + return {"success": false, "message": e.toString()}; + } + } + + /// Send profile update request (admin approval needed) + Future> sendUpdateRequest(Map data) async { + try { + final res = await _dio.post('/user/profile-update-request', data: data); + return Map.from(res.data); + + } catch (e) { + return {"success": false, "message": e.toString()}; + } + } +} diff --git a/lib/widgets/main_app_bar.dart b/lib/widgets/main_app_bar.dart new file mode 100644 index 0000000..3f40b6f --- /dev/null +++ b/lib/widgets/main_app_bar.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/user_profile_provider.dart'; +import '../screens/main_bottom_nav.dart'; +import '../config/app_config.dart'; + +class MainAppBar extends StatelessWidget implements PreferredSizeWidget { + const MainAppBar({super.key}); + + @override + Widget build(BuildContext context) { + final profileProvider = Provider.of(context); + final profileUrl = profileProvider.profile?.profileImage; + + return AppBar( + backgroundColor: Colors.lightGreen, + elevation: 0.8, + surfaceTintColor: Colors.transparent, + automaticallyImplyLeading: false, + + title: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image.network( + AppConfig.logoUrl, + height: 60, + width: 100, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.business, size: 32, color: Colors.indigo), + ), + ), + const SizedBox(width: 10), + const Text( + "Kent Logistics", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.indigo, + ), + ), + ], + ), + + actions: [ + IconButton( + icon: const Icon(Icons.notifications_none, color: Colors.indigo), + onPressed: () {}, + ), + + GestureDetector( + onTap: () { + final bottomNav = context.findAncestorStateOfType(); + bottomNav?.setIndex(4); + }, + + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: CircleAvatar( + radius: 18, + backgroundColor: Colors.grey.shade200, + backgroundImage: profileUrl != null ? NetworkImage(profileUrl) : null, + child: profileUrl == null + ? const Icon(Icons.person, color: Colors.grey) + : null, + ), + ), + ), + ], + ); + } + + @override + Size get preferredSize => const Size.fromHeight(56); +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..64a0ece 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..2db3c22 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b8e2b22..4b4e1ac 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import file_selector_macos import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 9011d48..ec71720 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" + url: "https://pub.dev" + source: hosted + version: "0.3.5+1" crypto: dependency: transitive description: @@ -97,6 +105,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" + url: "https://pub.dev" + source: hosted + version: "0.9.4+4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" flutter: dependency: "direct main" description: flutter @@ -110,6 +150,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476 + url: "https://pub.dev" + source: hosted + version: "2.0.31" flutter_test: dependency: "direct dev" description: flutter @@ -144,6 +192,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" + url: "https://pub.dev" + source: hosted + version: "0.8.13+1" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e + url: "https://pub.dev" + source: hosted + version: "0.8.13" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" leak_tracker: dependency: transitive description: @@ -447,4 +559,4 @@ packages: version: "1.1.0" sdks: dart: ">=3.8.1 <4.0.0" - flutter: ">=3.29.0" + flutter: ">=3.32.0" diff --git a/pubspec.yaml b/pubspec.yaml index f4f39e5..327abd4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,8 @@ dependencies: provider: ^6.0.5 shared_preferences: ^2.2.2 google_fonts: ^4.0.3 + image_picker: ^1.0.7 + # The following adds the Cupertino Icons font to your application. diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..77ab7a0 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..a423a02 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST