diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index 366da61..a62da3a 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":"image_picker_ios","path":"/home/kavinda/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.13+3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"objective_c","path":"/home/kavinda/.pub-cache/hosted/pub.dev/objective_c-9.1.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/kavinda/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/kavinda/.pub-cache/hosted/pub.dev/path_provider_foundation-2.5.0/","native_build":false,"dependencies":["objective_c"],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/kavinda/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/kavinda/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.6/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"/home/kavinda/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.33/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_android","path":"/home/kavinda/.pub-cache/hosted/pub.dev/image_picker_android-0.8.13+10/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/kavinda/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/kavinda/.pub-cache/hosted/pub.dev/path_provider_android-2.2.22/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/kavinda/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.17/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/kavinda/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.28/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"file_selector_macos","path":"/home/kavinda/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_macos","path":"/home/kavinda/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.2+1/","native_build":false,"dependencies":["file_selector_macos"],"dev_dependency":false},{"name":"objective_c","path":"/home/kavinda/.pub-cache/hosted/pub.dev/objective_c-9.1.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/kavinda/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/kavinda/.pub-cache/hosted/pub.dev/path_provider_foundation-2.5.0/","native_build":false,"dependencies":["objective_c"],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/kavinda/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/kavinda/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"file_selector_linux","path":"/home/kavinda/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_linux","path":"/home/kavinda/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2/","native_build":false,"dependencies":["file_selector_linux"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/kavinda/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/kavinda/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/kavinda/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/kavinda/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"file_selector_windows","path":"/home/kavinda/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_windows","path":"/home/kavinda/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.2/","native_build":false,"dependencies":["file_selector_windows"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/kavinda/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/kavinda/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/kavinda/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/kavinda/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"image_picker_for_web","path":"/home/kavinda/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.1/","dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/kavinda/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/kavinda/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/kavinda/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.1/","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":"objective_c","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"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":["objective_c"]},{"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"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-03-29 19:50:49.705162","version":"3.38.6","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\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_ios-0.8.13+3\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.3.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.5.1\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_ios-6.3.6\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_plugin_android_lifecycle-2.0.33\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_android","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_android-0.8.13+14\\\\","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.3.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.22\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_android-2.4.21\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_android-6.3.28\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"file_selector_macos","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\file_selector_macos-0.9.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_macos","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_macos-0.2.2+1\\\\","native_build":false,"dependencies":["file_selector_macos"],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.3.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.5.1\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_macos-3.2.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"file_selector_linux","path":"C:\\\\Users\\\\navee\\\\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\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_linux-0.2.2\\\\","native_build":false,"dependencies":["file_selector_linux"],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.3.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\navee\\\\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\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_linux-2.4.1\\\\","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_linux-3.2.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"file_selector_windows","path":"C:\\\\Users\\\\navee\\\\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\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_windows-0.2.2\\\\","native_build":false,"dependencies":["file_selector_windows"],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.3.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\navee\\\\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\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_windows-2.4.1\\\\","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_windows-3.1.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"image_picker_for_web","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\image_picker_for_web-3.1.1\\\\","dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-8.3.1\\\\","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_web-2.4.3\\\\","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"C:\\\\Users\\\\navee\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_web-2.4.1\\\\","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":"package_info_plus","dependencies":[]},{"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"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-05-07 21:18:24.533193","version":"3.35.4","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/lib/core/models/branch.dart b/lib/core/models/branch.dart index 8ba7c1e..a24d587 100644 --- a/lib/core/models/branch.dart +++ b/lib/core/models/branch.dart @@ -21,10 +21,10 @@ class Branch { /// Create from JSON factory Branch.fromJson(Map json) { return Branch( - id: json['id'], + id: (json['id'] as num?)?.toInt(), name: json['name'] ?? '', location: json['location'] ?? '', - managerId: json['manager_id'], + managerId: (json['manager_id'] as num?)?.toInt(), managerName: json['Manager']?['name'], createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) diff --git a/lib/core/models/branch_manager.dart b/lib/core/models/branch_manager.dart index d939bf5..dd9ea57 100644 --- a/lib/core/models/branch_manager.dart +++ b/lib/core/models/branch_manager.dart @@ -1,6 +1,7 @@ /// Branch Manager Model (GDM/GPM) class BranchManager { final int? id; + final int? profileId; final String name; final String email; final String? phone; @@ -15,6 +16,7 @@ class BranchManager { BranchManager({ this.id, + this.profileId, required this.name, required this.email, this.phone, @@ -32,12 +34,16 @@ class BranchManager { factory BranchManager.fromJson(Map json) { // Extract branchId from nested branchManagerProfile final branchManagerProfile = json['branchManagerProfile']; + final int? profileId = branchManagerProfile != null + ? branchManagerProfile['id'] as int? + : null; final int? branchId = branchManagerProfile != null ? branchManagerProfile['branchId'] : json['branchId']; return BranchManager( id: json['id'], + profileId: profileId, name: json['name'] ?? '', email: json['email'] ?? '', phone: json['phone'], @@ -60,6 +66,7 @@ class BranchManager { Map toJson() { return { if (id != null) 'id': id, + if (profileId != null) 'branchManagerProfileId': profileId, 'name': name, 'email': email, if (phone != null) 'phone': phone, diff --git a/lib/core/routing/app_router.dart b/lib/core/routing/app_router.dart index 2b8f1f5..f1113a9 100644 --- a/lib/core/routing/app_router.dart +++ b/lib/core/routing/app_router.dart @@ -86,11 +86,11 @@ class AppRouter { RouteNames.reportNewIssue: (context) => const ReportNewIssuePage(), RouteNames.reportedIssues: (context) => const ReportedIssuesPage(), - // ✅ Added new routes for the tabbed list pages - RouteNames.gdms: (context) => const NetworkTabsPage(initialIndex: 0), - RouteNames.gpms: (context) => const NetworkTabsPage(initialIndex: 1), - RouteNames.outlets: (context) => const NetworkTabsPage(initialIndex: 2), - RouteNames.mes: (context) => const NetworkTabsPage(initialIndex: 3), + // Manage Network tabs — ME only + RouteNames.gdms: (context) => _meOnly(context, const NetworkTabsPage(initialIndex: 0)), + RouteNames.gpms: (context) => _meOnly(context, const NetworkTabsPage(initialIndex: 1)), + RouteNames.outlets: (context) => _meOnly(context, const NetworkTabsPage(initialIndex: 2)), + RouteNames.mes: (context) => _meOnly(context, const NetworkTabsPage(initialIndex: 3)), // About page removed @@ -101,6 +101,18 @@ class AppRouter { RouteNames.notifications: (context) => const NotificationsPage(), }; + /// Returns [page] only for maintenance_executive; otherwise shows an access-denied screen. + static Widget _meOnly(BuildContext context, Widget page) { + final role = AuthService.instance.currentUser?.role; + if (role == 'maintenance_executive') return page; + return Scaffold( + appBar: AppBar(title: const Text('Access Denied')), + body: const Center( + child: Text('This section is only accessible to Maintenance Executives.'), + ), + ); + } + /// Handles undefined routes. static Route onUnknownRoute(RouteSettings settings) { return MaterialPageRoute(builder: (context) => const UnknownRouteScreen()); diff --git a/lib/core/services/auth_service.dart b/lib/core/services/auth_service.dart index 50b1f02..0943660 100644 --- a/lib/core/services/auth_service.dart +++ b/lib/core/services/auth_service.dart @@ -35,16 +35,18 @@ class UserProfile { final branchManagerProfile = json['branchManagerProfile'] as Map?; if (branchManagerProfile != null) { - branchId = branchManagerProfile['branchId'] as int?; - branchManagerProfileId = branchManagerProfile['id'] as int?; + branchId = (branchManagerProfile['branchId'] as num?)?.toInt(); + branchManagerProfileId = (branchManagerProfile['id'] as num?)?.toInt(); } final meProfile = json['maintenanceExecutiveProfile'] as Map?; if (meProfile != null) { - maintenanceExecutiveProfileId = meProfile['id'] as int?; - // Also extract branchId from ME profile if not already set - branchId ??= meProfile['branchId'] as int?; + maintenanceExecutiveProfileId = (meProfile['id'] as num?)?.toInt(); + branchId ??= (meProfile['branchId'] as num?)?.toInt(); } + // Fallback: read flat keys saved by toJson() when restoring from SharedPreferences + maintenanceExecutiveProfileId ??= (json['maintenanceExecutiveProfileId'] as num?)?.toInt(); + branchManagerProfileId ??= (json['branchManagerProfileId'] as num?)?.toInt(); return UserProfile( id: json['id'] as int, @@ -53,7 +55,7 @@ class UserProfile { role: json['role'] as String, phone: json['phone'] as String?, profilePicture: json['profilePicture'] as String?, - branchId: branchId ?? json['branchId'] as int?, + branchId: branchId ?? (json['branchId'] as num?)?.toInt(), branchManagerProfileId: branchManagerProfileId, maintenanceExecutiveProfileId: maintenanceExecutiveProfileId, ); diff --git a/lib/core/services/branch_manager_service.dart b/lib/core/services/branch_manager_service.dart index 5505c55..f8a0acc 100644 --- a/lib/core/services/branch_manager_service.dart +++ b/lib/core/services/branch_manager_service.dart @@ -75,13 +75,18 @@ class BranchManagerService { String? phone, int? branchId, String? employeeId, + bool clearBranch = false, }) async { try { final data = {}; if (name != null) data['name'] = name; if (email != null) data['email'] = email; if (phone != null) data['phone'] = phone; - if (branchId != null) data['branchId'] = branchId; + if (clearBranch) { + data['branchId'] = null; + } else if (branchId != null) { + data['branchId'] = branchId; + } if (employeeId != null) data['employeeId'] = employeeId; final response = await _apiService.put('/users/branch-managers/$id', data); diff --git a/lib/core/services/branch_service.dart b/lib/core/services/branch_service.dart index 2287a0e..215fbd2 100644 --- a/lib/core/services/branch_service.dart +++ b/lib/core/services/branch_service.dart @@ -67,12 +67,17 @@ class BranchService { String? name, String? location, int? managerId, + bool clearManager = false, }) async { try { final data = {}; if (name != null) data['name'] = name; if (location != null) data['location'] = location; - if (managerId != null) data['manager_id'] = managerId; + if (clearManager) { + data['manager_id'] = null; + } else if (managerId != null) { + data['manager_id'] = managerId; + } final response = await _apiService.put('/branches/$id', data); diff --git a/lib/features/chat/view/pages/chat_box.dart b/lib/features/chat/view/pages/chat_box.dart index beda861..469f2e5 100644 --- a/lib/features/chat/view/pages/chat_box.dart +++ b/lib/features/chat/view/pages/chat_box.dart @@ -46,10 +46,18 @@ class _ChatPageState extends State { _issue = args; _connectSocket(); _scrollToBottom(); + _refreshIssue(args.id); } } } + Future _refreshIssue(int issueId) async { + try { + final fresh = await _issueApiService.getIssueById(issueId); + if (mounted) setState(() { _issue = fresh; }); + } catch (_) {} + } + @override void dispose() { _socket?.disconnect(); @@ -150,35 +158,47 @@ class _ChatPageState extends State { _socket!.on('issue_update', (data) { print('Received issue update: $data'); - // Handle Petty Cash Update - if (data is Map && - data.containsKey('amount') && - data.containsKey('technician_id')) { - if (mounted && _issue != null) { - try { - final newRequest = PettyCashRequestModel.fromJson(data); - final currentRequests = List.from( - _issue!.pettyCashRequests ?? [], - ); + if (data is Map) { + // Handle Petty Cash Update + if (data.containsKey('amount') && data.containsKey('technician_id')) { + if (mounted && _issue != null) { + try { + final newRequest = PettyCashRequestModel.fromJson(data); + final currentRequests = List.from( + _issue!.pettyCashRequests ?? [], + ); - final index = currentRequests.indexWhere( - (r) => r.id == newRequest.id, - ); - if (index != -1) { - currentRequests[index] = newRequest; - } else { - currentRequests.add(newRequest); - } + final index = currentRequests.indexWhere( + (r) => r.id == newRequest.id, + ); + if (index != -1) { + currentRequests[index] = newRequest; + } else { + currentRequests.add(newRequest); + } - setState(() { - _issue = _issue!.copyWith(pettyCashRequests: currentRequests); - }); - _scrollToBottom(); - } catch (e) { - print('Error parsing petty cash update: $e'); + setState(() { + _issue = _issue!.copyWith(pettyCashRequests: currentRequests); + }); + _scrollToBottom(); + } catch (e) { + print('Error parsing petty cash update: $e'); + } } + return; + } + + if (data['success'] == true && data['data'] is Map) { + _applyIssueUpdateFields(data['data'] as Map); + return; + } + + if (data.containsKey('status') || + data.containsKey('technician_id') || + data.containsKey('maintenance_executive_id') || + data.containsKey('third_party_id')) { + _applyIssueUpdateFields(data); } - return; } }); @@ -207,50 +227,11 @@ class _ChatPageState extends State { } }); - // ── issue_update: general issue field updates ── + // ── issue_update_fields: legacy event name ── _socket!.on('issue_update_fields', (data) { if (data is Map && data['success'] == true) { final updateData = data['data'] as Map; - if (mounted && _issue != null) { - setState(() { - _issue = _issue!.copyWith( - status: updateData['status'] != null - ? IssueStatus.fromString(updateData['status']) - : null, - maintenanceExecutiveId: updateData['maintenance_executive_id'], - technicianId: updateData['technician_id'], - thirdPartyId: updateData['third_party_id'], - maintenanceExecutiveAssignedAt: - updateData['maintenance_executive_assigned_at'] != null - ? DateTime.parse( - updateData['maintenance_executive_assigned_at'], - ) - : null, - technicianAssignedAt: updateData['technician_assigned_at'] != null - ? DateTime.parse(updateData['technician_assigned_at']) - : null, - thirdPartyAssignedAt: - updateData['third_party_assigned_at'] != null - ? DateTime.parse(updateData['third_party_assigned_at']) - : null, - updatedAt: updateData['updatedAt'] != null - ? DateTime.parse(updateData['updatedAt']) - : null, - maintenanceExecutive: updateData['maintenanceExecutive'] != null - ? MaintenanceExecutiveInfo.fromJson( - updateData['maintenanceExecutive'], - ) - : null, - technician: updateData['technician'] != null - ? TechnicianInfo.fromJson(updateData['technician']) - : null, - thirdParty: updateData['thirdParty'] != null - ? ThirdPartyInfo.fromJson(updateData['thirdParty']) - : null, - ); - }); - _scrollToBottom(); - } + _applyIssueUpdateFields(updateData); } }); @@ -294,15 +275,105 @@ class _ChatPageState extends State { }); } + void _applyIssueUpdateFields(Map updateData) { + if (!mounted || _issue == null) return; + + setState(() { + _issue = _issue!.copyWith( + status: updateData['status'] != null + ? IssueStatus.fromString(updateData['status']) + : null, + maintenanceExecutiveId: updateData['maintenance_executive_id'], + technicianId: updateData['technician_id'], + thirdPartyId: updateData['third_party_id'], + maintenanceExecutiveAssignedAt: + updateData['maintenance_executive_assigned_at'] != null + ? DateTime.parse( + updateData['maintenance_executive_assigned_at'], + ) + : null, + technicianAssignedAt: updateData['technician_assigned_at'] != null + ? DateTime.parse(updateData['technician_assigned_at']) + : null, + thirdPartyAssignedAt: updateData['third_party_assigned_at'] != null + ? DateTime.parse(updateData['third_party_assigned_at']) + : null, + updatedAt: updateData['updatedAt'] != null + ? DateTime.parse(updateData['updatedAt']) + : null, + maintenanceExecutive: updateData['maintenanceExecutive'] != null + ? MaintenanceExecutiveInfo.fromJson( + updateData['maintenanceExecutive'], + ) + : null, + technician: updateData['technician'] != null + ? TechnicianInfo.fromJson(updateData['technician']) + : null, + thirdParty: updateData['thirdParty'] != null + ? ThirdPartyInfo.fromJson(updateData['thirdParty']) + : null, + ); + }); + _scrollToBottom(); + } + void _sendMessage(String text, String? target) { if (_issue?.maintenanceExecutive == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Cannot send message: No Maintenance Executive accepted.', + final authService = AuthService.instance; + final currentUser = authService.currentUser; + if (currentUser?.role == 'maintenance_executive') { + final meProfileId = currentUser?.maintenanceExecutiveProfileId; + if (meProfileId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Could not find your Maintenance Executive profile. Please log out and log back in.'), + backgroundColor: Colors.red, + ), + ); + return; + } + showAcceptRejectIssueDialog( + context, + onAccept: () async { + try { + _showLoadingDialog('Accepting issue...'); + final updatedIssue = await _issueApiService.assignMaintenanceExecutive( + _issue!.id, + meProfileId, + ); + if (mounted) { + setState(() { _issue = updatedIssue; }); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Issue accepted successfully'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) Navigator.of(context).pop(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to accept: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + onReject: () {}, + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Cannot send message: No Maintenance Executive accepted.', + ), ), - ), - ); + ); + } return; } @@ -401,10 +472,43 @@ class _ChatPageState extends State { } /// Handle action button taps from MessageInputField - void _handleAction(String action) { + Future _handleAction(String action) async { if (_issue == null) return; switch (action) { + case 'Accept Issue': + final authService = AuthService.instance; + final currentUser = authService.currentUser; + if (currentUser == null || currentUser.maintenanceExecutiveProfileId == null) break; + try { + _showLoadingDialog('Accepting issue...'); + final updatedIssue = await _issueApiService.assignMaintenanceExecutive( + _issue!.id, + currentUser.maintenanceExecutiveProfileId!, + ); + if (mounted) { + setState(() { _issue = updatedIssue; }); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Issue accepted successfully'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) Navigator.of(context).pop(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to accept issue: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + break; + case 'Assign a Technician': showAssignGPMDialog(context, (technician) async { try { @@ -412,7 +516,7 @@ class _ChatPageState extends State { _showLoadingDialog('Assigning technician...'); // Call API to assign technician - final updatedIssue = await _issueApiService.assignTechnician( + await _issueApiService.assignTechnician( _issue!.id, technician.id, ); @@ -420,11 +524,6 @@ class _ChatPageState extends State { // Dismiss loading if (mounted) Navigator.of(context).pop(); - // Update local state - setState(() { - _issue = updatedIssue; - }); - // Show success message if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -1906,6 +2005,8 @@ class _ChatPageState extends State { role: myRole, onSend: _sendMessage, onAction: _handleAction, + showAcceptButton: myRole == UserRole.executive && + _issue?.maintenanceExecutive == null, ), ], ), diff --git a/lib/features/chat/view/widgets/action_dialogs.dart b/lib/features/chat/view/widgets/action_dialogs.dart index f44455a..5613021 100644 --- a/lib/features/chat/view/widgets/action_dialogs.dart +++ b/lib/features/chat/view/widgets/action_dialogs.dart @@ -1619,3 +1619,113 @@ Future showRequestPettyCashDialog( builder: (context) => RequestPettyCashDialog(onRequest: onRequest), ); } + +class AcceptRejectIssueDialog extends StatelessWidget { + final VoidCallback onAccept; + final VoidCallback onReject; + + const AcceptRejectIssueDialog({ + super.key, + required this.onAccept, + required this.onReject, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + child: Container( + width: double.infinity, + constraints: const BoxConstraints(maxWidth: 400), + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Issue Assignment', + style: GoogleFonts.outfit( + fontSize: 20, + fontWeight: FontWeight.w600, + color: AppColors.textTitle, + ), + ), + const SizedBox(height: 8), + Text( + 'Do you want to accept or reject this issue?', + style: GoogleFonts.outfit( + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + Navigator.of(context).pop(); + onReject(); + }, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + side: const BorderSide(color: AppColors.grey), + ), + child: Text( + 'Cancel', + style: GoogleFonts.outfit( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.textPrimary, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + onAccept(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.secondary, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + 'Accept', + style: GoogleFonts.outfit( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +Future showAcceptRejectIssueDialog( + BuildContext context, { + required VoidCallback onAccept, + required VoidCallback onReject, +}) { + return showDialog( + context: context, + builder: (_) => AcceptRejectIssueDialog(onAccept: onAccept, onReject: onReject), + ); +} diff --git a/lib/features/chat/view/widgets/messge_input_field.dart b/lib/features/chat/view/widgets/messge_input_field.dart index b2a5dd2..f4eeb27 100644 --- a/lib/features/chat/view/widgets/messge_input_field.dart +++ b/lib/features/chat/view/widgets/messge_input_field.dart @@ -5,12 +5,14 @@ class MessageInputField extends StatefulWidget { final UserRole role; // supplied from saved login role final Function(String text, String? target)? onSend; final Function(String action)? onAction; // callback for action buttons + final bool showAcceptButton; const MessageInputField({ super.key, required this.role, this.onSend, this.onAction, + this.showAcceptButton = false, }); @override @@ -74,7 +76,8 @@ class _MessageInputFieldState extends State { actions = const ['Close Issue']; break; case UserRole.executive: - actions = const [ + actions = [ + if (widget.showAcceptButton) 'Accept Issue', 'Update the Status', 'Assign a Technician', 'Get Outside Support', @@ -158,7 +161,8 @@ class _MessageInputFieldState extends State { ]; break; case UserRole.executive: - actions = const [ + actions = [ + if (widget.showAcceptButton) 'Accept Issue', 'Update the Status', 'Assign a Technician', 'Get Outside Support', diff --git a/lib/features/list_pages/views/gdms_tab_content.dart b/lib/features/list_pages/views/gdms_tab_content.dart index 0bd9d35..4575e33 100644 --- a/lib/features/list_pages/views/gdms_tab_content.dart +++ b/lib/features/list_pages/views/gdms_tab_content.dart @@ -5,16 +5,23 @@ import '../../user/view/edit_gdm_details_page.dart'; import '../widgets/generic_list_tab_content.dart'; import '../widgets/list_item_card.dart'; -class GDMsTabContent extends StatelessWidget { +class GDMsTabContent extends StatefulWidget { const GDMsTabContent({super.key}); @override - Widget build(BuildContext context) { - final BranchManagerService service = BranchManagerService(); + State createState() => _GDMsTabContentState(); +} + +class _GDMsTabContentState extends State { + final BranchManagerService _service = BranchManagerService(); + int _refreshToken = 0; + @override + Widget build(BuildContext context) { return GenericListTabContent( + key: ValueKey(_refreshToken), fetchItems: () async { - final allBranchManagers = await service.getAllBranchManagers(); + final allBranchManagers = await _service.getAllBranchManagers(); return allBranchManagers.where((bm) => bm.branchId != null).toList(); }, emptyMessage: 'No GDMs found', @@ -31,9 +38,11 @@ class GDMsTabContent extends StatelessWidget { : null, ), title: gdm.name, - subtitle: gdm.branchName != null ? '${gdm.employeeId} | ${gdm.branchName}' : 'GDM', - onEdit: () { - Navigator.of(context).push( + subtitle: gdm.branchName != null + ? '${gdm.employeeId} | ${gdm.branchName}' + : 'GDM', + onEdit: () async { + final updated = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => EditGDMDetailsPage( gdmId: gdm.id, @@ -42,6 +51,9 @@ class GDMsTabContent extends StatelessWidget { ), ), ); + if (updated == true && mounted) { + setState(() => _refreshToken++); + } }, ); }, diff --git a/lib/features/list_pages/views/outlets_tab_content.dart b/lib/features/list_pages/views/outlets_tab_content.dart index 930fb60..bfb7c1e 100644 --- a/lib/features/list_pages/views/outlets_tab_content.dart +++ b/lib/features/list_pages/views/outlets_tab_content.dart @@ -5,15 +5,22 @@ import '../../user/view/edit_outlet_page.dart'; import '../widgets/generic_list_tab_content.dart'; import '../widgets/list_item_card.dart'; -class OutletsTabContent extends StatelessWidget { +class OutletsTabContent extends StatefulWidget { const OutletsTabContent({super.key}); @override - Widget build(BuildContext context) { - final BranchService service = BranchService(); + State createState() => _OutletsTabContentState(); +} + +class _OutletsTabContentState extends State { + final BranchService _service = BranchService(); + int _refreshToken = 0; + @override + Widget build(BuildContext context) { return GenericListTabContent( - fetchItems: service.getAllBranches, + key: ValueKey(_refreshToken), + fetchItems: _service.getAllBranches, emptyMessage: 'No outlets found', itemBuilder: (context, outlet) { return ListItemCard( @@ -28,8 +35,8 @@ class OutletsTabContent extends StatelessWidget { ), title: outlet.displayName, subtitle: outlet.displayAddress, - onEdit: () { - Navigator.of(context).push( + onEdit: () async { + final updated = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => EditOutletPage( outletId: outlet.id, @@ -38,6 +45,9 @@ class OutletsTabContent extends StatelessWidget { ), ), ); + if (updated == true && mounted) { + setState(() => _refreshToken++); + } }, ); }, diff --git a/lib/features/profile/controller/profile_controller.dart b/lib/features/profile/controller/profile_controller.dart index d634aab..db5b169 100644 --- a/lib/features/profile/controller/profile_controller.dart +++ b/lib/features/profile/controller/profile_controller.dart @@ -2,6 +2,9 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../../../core/models/app_user.dart'; +import '../../../core/models/branch.dart'; +import '../../../core/services/auth_service.dart'; +import '../../../core/services/branch_service.dart'; import '../data/user_repository.dart'; /// Controller for managing profile state and operations @@ -22,6 +25,12 @@ class ProfileController extends ChangeNotifier { XFile? _selectedImage; Uint8List? _selectedImageBytes; + // Branch dropdown state (branch manager only) + final BranchService _branchService = BranchService(); + List _branches = []; + Branch? _selectedBranch; + bool _isLoadingBranches = false; + ProfileController({required UserRepository repository}) : _repository = repository { _loadCurrentUser(); @@ -32,6 +41,14 @@ class ProfileController extends ChangeNotifier { String? get errorMessage => _errorMessage; XFile? get selectedImage => _selectedImage; Uint8List? get selectedImageBytes => _selectedImageBytes; + List get branches => _branches; + Branch? get selectedBranch => _selectedBranch; + bool get isLoadingBranches => _isLoadingBranches; + + void setSelectedBranch(Branch? branch) { + _selectedBranch = branch; + notifyListeners(); + } /// Get the label for the extra field based on user role String get extraFieldLabel { @@ -46,6 +63,32 @@ class ProfileController extends ChangeNotifier { } } + /// Load branches for branch manager outlet dropdown + Future loadBranches() async { + _isLoadingBranches = true; + notifyListeners(); + try { + _branches = await _branchService.getAllBranches(); + _selectedBranch = null; + + // branchManagerProfile.branchId from login/GET /users/:id == Branch.id + final branchId = AuthService.instance.currentUser?.branchId; + if (branchId != null) { + for (final b in _branches) { + if (b.id == branchId) { + _selectedBranch = b; + break; + } + } + } + } catch (_) { + _branches = []; + } finally { + _isLoadingBranches = false; + notifyListeners(); + } + } + /// Pick an image from gallery Future pickImage() async { try { @@ -95,6 +138,10 @@ class ProfileController extends ChangeNotifier { _currentUser!.email != null) { extraFieldController.text = _currentUser!.email!; } + + if (_currentUser!.role == UserRole.branchManager) { + loadBranches(); + } } _errorMessage = null; @@ -115,6 +162,16 @@ class ProfileController extends ChangeNotifier { notifyListeners(); try { + // For branch managers, use the selected branch ID from dropdown + String? extraField; + if (_currentUser!.role == UserRole.branchManager) { + extraField = _selectedBranch?.id?.toString(); + } else { + extraField = extraFieldController.text.trim().isEmpty + ? null + : extraFieldController.text.trim(); + } + await _repository.updateProfile( userId: _currentUser!.id, firstName: firstNameController.text.trim().isEmpty @@ -129,9 +186,7 @@ class ProfileController extends ChangeNotifier { password: passwordController.text.trim().isEmpty ? null : passwordController.text.trim(), - extraField: extraFieldController.text.trim().isEmpty - ? null - : extraFieldController.text.trim(), + extraField: extraField, profileImageBytes: _selectedImageBytes, profileImageName: _selectedImage?.name, ); diff --git a/lib/features/profile/view/edit_profile_page.dart b/lib/features/profile/view/edit_profile_page.dart index adfb11c..70a4c35 100644 --- a/lib/features/profile/view/edit_profile_page.dart +++ b/lib/features/profile/view/edit_profile_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../../../core/models/app_user.dart'; +import '../../../core/models/branch.dart'; import '../../../theme/app_colors.dart'; import '../controller/profile_controller.dart'; import '../data/api_user_repository.dart'; @@ -313,10 +315,18 @@ class _EditProfilePageState extends State { ), const SizedBox(height: 12), - _FilledField( - controller: _controller.extraFieldController, - hintText: _controller.extraFieldLabel, - ), + if (user.role == UserRole.branchManager) + _OutletDropdown( + isLoading: _controller.isLoadingBranches, + branches: _controller.branches, + value: _controller.selectedBranch, + onChanged: _controller.setSelectedBranch, + ) + else + _FilledField( + controller: _controller.extraFieldController, + hintText: _controller.extraFieldLabel, + ), const SizedBox(height: 36), @@ -464,6 +474,74 @@ class _FlagImage extends StatelessWidget { } } +/// Outlet dropdown for branch manager +class _OutletDropdown extends StatelessWidget { + const _OutletDropdown({ + required this.isLoading, + required this.branches, + required this.value, + required this.onChanged, + }); + + final bool isLoading; + final List branches; + final Branch? value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Container( + height: 45, + decoration: BoxDecoration( + color: AppColors.primary100, + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: isLoading + ? const Center( + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + : DropdownButtonHideUnderline( + child: DropdownButton( + value: value, + hint: const Text( + 'Select Outlet', + style: TextStyle( + color: AppColors.textSecondary, + fontSize: 12, + fontFamily: 'Outfit', + fontWeight: FontWeight.w400, + ), + ), + isExpanded: true, + icon: const Icon( + Icons.keyboard_arrow_down, + color: AppColors.textTitle, + size: 20, + ), + style: const TextStyle( + fontSize: 12, + fontFamily: 'Outfit', + fontWeight: FontWeight.w400, + color: AppColors.textTitle, + ), + items: branches.map((branch) { + return DropdownMenuItem( + value: branch, + child: Text('${branch.name} - ${branch.location}'), + ); + }).toList(), + onChanged: branches.isEmpty ? null : onChanged, + ), + ), + ); + } +} + /// Button class _GradientButton extends StatelessWidget { const _GradientButton({required this.child, this.onPressed}); diff --git a/lib/features/profile/view/profile_container.dart b/lib/features/profile/view/profile_container.dart index 87a229e..a5666c5 100644 --- a/lib/features/profile/view/profile_container.dart +++ b/lib/features/profile/view/profile_container.dart @@ -117,6 +117,8 @@ class _ProfileContainerState extends State { final user = snapshot.data!; + final isME = AuthService.instance.currentUser?.role == 'maintenance_executive'; + return UserProfilePage( fullName: user.fullName, avatarUrl: user.avatarUrl, @@ -124,9 +126,9 @@ class _ProfileContainerState extends State { handle: '@${user.fullName.toLowerCase().split(' ').first}', email: user.email, phone: user.phone, + showManageNetwork: isME, onAccountTap: () async { await Navigator.of(context).pushNamed(RouteNames.editProfile); - // Refresh user data when returning from edit page _refreshUser(); }, onLogout: _handleLogout, diff --git a/lib/features/profile/view/user_profile_page.dart b/lib/features/profile/view/user_profile_page.dart index e1d2731..29c8ee2 100644 --- a/lib/features/profile/view/user_profile_page.dart +++ b/lib/features/profile/view/user_profile_page.dart @@ -16,6 +16,7 @@ class UserProfilePage extends StatelessWidget { this.onLogout, this.email, this.phone, + this.showManageNetwork = false, }); final String? avatarUrl; @@ -28,6 +29,7 @@ class UserProfilePage extends StatelessWidget { final VoidCallback? onAccountTap; final VoidCallback? onManageNetworkTap; final VoidCallback? onHelpTap; + final bool showManageNetwork; final VoidCallback? onAboutTap; final VoidCallback? onLogout; @@ -197,16 +199,17 @@ class UserProfilePage extends StatelessWidget { Navigator.of(context).pushNamed('/profile/edit'), ), - const SizedBox(height: 12), - - _SettingCard( - icon: Icons.shield_outlined, - title: 'Manage Network', - subtitle: 'gdms', - onTap: - onManageNetworkTap ?? - () => Navigator.of(context).pushNamed('/gdms'), - ), + if (showManageNetwork) ...[ + const SizedBox(height: 12), + _SettingCard( + icon: Icons.shield_outlined, + title: 'Manage Network', + subtitle: 'gdms', + onTap: + onManageNetworkTap ?? + () => Navigator.of(context).pushNamed('/gdms'), + ), + ], const SizedBox(height: 12), diff --git a/lib/features/tickets/service/issue_api_service.dart b/lib/features/tickets/service/issue_api_service.dart index 551be64..220e6ef 100644 --- a/lib/features/tickets/service/issue_api_service.dart +++ b/lib/features/tickets/service/issue_api_service.dart @@ -165,15 +165,12 @@ class IssueApiService { } /// Assign technician to issue - Future assignTechnician(int issueId, int technicianId) async { + Future assignTechnician(int issueId, int technicianId) async { try { - final response = await _apiService.post( + await _apiService.post( '/issues/$issueId/assign-technician', {'technician_id': technicianId}, ); - - final issueData = response['data'] as Map; - return IssueModel.fromJson(issueData); } catch (e) { throw Exception('Failed to assign technician: ${e.toString()}'); } @@ -185,13 +182,13 @@ class IssueApiService { int executiveId, ) async { try { - final response = await _apiService.post( + await _apiService.post( '/issues/$issueId/assign-maintenance-executive', - {'executive_id': executiveId}, + {'maintenance_executive_id': executiveId}, ); - - final issueData = response['data'] as Map; - return IssueModel.fromJson(issueData); + // Backend returns a partial issue object missing required fields like + // branch_id and manager_id, so fetch the full issue separately. + return getIssueById(issueId); } catch (e) { throw Exception( 'Failed to assign maintenance executive: ${e.toString()}', diff --git a/lib/features/tickets/view/report_new_issue_page.dart b/lib/features/tickets/view/report_new_issue_page.dart index 6eb6b94..599e2d3 100644 --- a/lib/features/tickets/view/report_new_issue_page.dart +++ b/lib/features/tickets/view/report_new_issue_page.dart @@ -6,6 +6,10 @@ import 'package:intl/intl.dart'; import 'package:image_picker/image_picker.dart'; import '../controller/issue_controller.dart'; import '../model/issue_model.dart'; +import '../../../core/models/branch.dart'; +import '../../../core/models/branch_manager.dart'; +import '../../../core/services/branch_manager_service.dart'; +import '../../../core/services/branch_service.dart'; import '../../../theme/app_colors.dart'; import '../../../core/services/auth_service.dart'; import '../../../core/services/upload_service.dart'; @@ -26,13 +30,20 @@ class _ReportNewIssuePageState extends State { final TextEditingController _descriptionController = TextEditingController(); final IssueController _issueController = IssueController(); final ImagePicker _imagePicker = ImagePicker(); + final BranchService _branchService = BranchService(); + final BranchManagerService _branchManagerService = BranchManagerService(); bool _isUploadingFiles = false; + bool _isLoadingBranches = false; + String? _branchLoadError; + List _branches = []; + Branch? _selectedBranch; @override void initState() { super.initState(); _selectedDate = DateTime.now(); // Use current date as default + _loadBranchesIfNeeded(); } @override @@ -43,9 +54,85 @@ class _ReportNewIssuePageState extends State { // _issueController.dispose(); super.dispose(); } + + void _loadBranchesIfNeeded() { + final currentUser = AuthService.instance.currentUser; + if (currentUser?.role == 'maintenance_executive') { + _loadBranches(); + } + } + + Future _loadBranches() async { + setState(() { + _isLoadingBranches = true; + _branchLoadError = null; + }); + + try { + final branches = await _branchService.getAllBranches(); + final managers = await _branchManagerService.getAllBranchManagers(); + final managerIdMap = {}; + for (final BranchManager manager in managers) { + if (manager.id != null) { + managerIdMap[manager.id!] = manager.profileId; + } + } + + final normalized = branches.map((branch) { + final originalManagerId = branch.managerId; + if (originalManagerId != null && + managerIdMap.containsKey(originalManagerId) && + managerIdMap[originalManagerId] != null) { + return Branch( + id: branch.id, + name: branch.name, + location: branch.location, + managerId: managerIdMap[originalManagerId], + managerName: branch.managerName, + createdAt: branch.createdAt, + updatedAt: branch.updatedAt, + ); + } + return branch; + }).toList(); + + final availableBranches = normalized + .where((branch) => branch.managerId != null) + .toList(); + final currentUser = AuthService.instance.currentUser; + Branch? selected; + + if (currentUser?.branchId != null) { + for (final branch in availableBranches) { + if (branch.id == currentUser!.branchId) { + selected = branch; + break; + } + } + } + + if (!mounted) return; + + setState(() { + _branches = availableBranches; + _selectedBranch = selected; + _isLoadingBranches = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _isLoadingBranches = false; + _branchLoadError = e.toString(); + }); + } + } @override Widget build(BuildContext context) { + final currentUser = AuthService.instance.currentUser; + final isMaintenanceExecutive = + currentUser?.role == 'maintenance_executive'; + return Scaffold( backgroundColor: Colors.white, appBar: AppBar( @@ -93,6 +180,78 @@ class _ReportNewIssuePageState extends State { ), const SizedBox(height: 24), + if (isMaintenanceExecutive) ...[ + _buildSectionLabel('Outlet'), + const SizedBox(height: 8), + if (_isLoadingBranches) + const Row( + children: [ + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + SizedBox(width: 8), + Text('Loading branches...', style: TextStyle(fontSize: 12)), + ], + ) + else if (_branchLoadError != null) + Row( + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Failed to load branches', + style: TextStyle(color: Colors.red[700], fontSize: 12), + ), + ), + TextButton( + onPressed: _loadBranches, + child: const Text('Retry'), + ), + ], + ) + else if (_branches.isEmpty) + const Text( + 'No outlets with managers assigned', + style: TextStyle(color: Colors.grey, fontSize: 12), + ) + else + DropdownButtonFormField( + value: _selectedBranch, + isExpanded: true, + items: _branches + .map( + (branch) => DropdownMenuItem( + value: branch, + child: Text('${branch.name} - ${branch.location}'), + ), + ) + .toList(), + onChanged: (branch) { + setState(() { + _selectedBranch = branch; + }); + }, + decoration: InputDecoration( + hintText: 'Select outlet', + hintStyle: TextStyle(color: Colors.grey[400]), + filled: true, + fillColor: Colors.grey[100], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + ), + ), + const SizedBox(height: 20), + ], + // Reported On _buildSectionLabel('Reported On'), const SizedBox(height: 8), @@ -644,21 +803,47 @@ class _ReportNewIssuePageState extends State { _showErrorDialog('User not logged in'); return; } - - // Use branch 1 as default if user doesn't have a branch assigned - final branchId = currentUser.branchId ?? 1; - - // Determine manager_id based on user role: - // - Branch managers use their own profile ID - // - All other roles (maintenance executive, etc.) send 0/null so backend auto-assigns - final int? managerId = currentUser.branchManagerProfileId; + final isExecutive = currentUser.role == 'maintenance_executive'; + int branchId; + int managerId; + + if (isExecutive) { + final selectedBranch = _selectedBranch; + if (selectedBranch == null) { + _showErrorDialog('Please select an outlet'); + return; + } + if (selectedBranch.id == null) { + _showErrorDialog('Selected outlet is invalid'); + return; + } + if (selectedBranch.managerId == null) { + _showErrorDialog('Selected outlet has no manager assigned'); + return; + } + branchId = selectedBranch.id!; + managerId = selectedBranch.managerId!; + } else { + final userBranchId = currentUser.branchId; + if (userBranchId == null) { + _showErrorDialog('No branch assigned to this user'); + return; + } + branchId = userBranchId; + final branchManagerId = currentUser.branchManagerProfileId; + if (branchManagerId == null) { + _showErrorDialog('No branch manager profile found'); + return; + } + managerId = branchManagerId; + } // Create new issue // Note: id, createdAt, updatedAt will be set by backend final newIssue = IssueModel( id: 0, // Backend will assign the real ID - branchId: currentUser.branchId!, // Use user's branch or default to 1 - managerId: currentUser.branchManagerProfileId!, // Use BranchManager profile ID + branchId: branchId, + managerId: managerId, title: _taskNameController.text.trim(), description: _descriptionController.text.trim(), status: IssueStatus.open, diff --git a/lib/features/user/view/add_gdm_page.dart b/lib/features/user/view/add_gdm_page.dart index d0c00c1..8916b8b 100644 --- a/lib/features/user/view/add_gdm_page.dart +++ b/lib/features/user/view/add_gdm_page.dart @@ -79,10 +79,30 @@ class _AddGDMPageState extends State { return; } + if (_selectedOutlet == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please select an outlet'), + backgroundColor: Colors.red, + ), + ); + return; + } + + if (_selectedOutlet?.id == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Selected outlet is invalid'), + backgroundColor: Colors.red, + ), + ); + return; + } + setState(() => _isLoading = true); try { - await _service.createBranchManager( + final createdManager = await _service.createBranchManager( name: '$firstName $lastName', email: email, phone: phone.isNotEmpty ? phone : null, @@ -90,6 +110,13 @@ class _AddGDMPageState extends State { branchId: _selectedOutlet?.id, ); + if (createdManager.profileId != null) { + await _branchService.updateBranch( + id: _selectedOutlet!.id!, + managerId: createdManager.profileId, + ); + } + if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/features/user/view/add_outlet_page.dart b/lib/features/user/view/add_outlet_page.dart index 9475f5a..4efc6d9 100644 --- a/lib/features/user/view/add_outlet_page.dart +++ b/lib/features/user/view/add_outlet_page.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import '../../../core/models/branch_manager.dart'; +import '../../../core/services/branch_manager_service.dart'; import '../../../core/services/branch_service.dart'; import '../../../theme/app_colors.dart'; @@ -11,12 +13,22 @@ class AddOutletPage extends StatefulWidget { class _AddOutletPageState extends State { final BranchService _service = BranchService(); + final BranchManagerService _managerService = BranchManagerService(); final TextEditingController _outletNameController = TextEditingController(); final TextEditingController _cityNameController = TextEditingController(); final TextEditingController _phoneController = TextEditingController(); final TextEditingController _addressController = TextEditingController(); + List _managers = []; + BranchManager? _selectedManager; + bool _isLoadingManagers = true; bool _isLoading = false; + @override + void initState() { + super.initState(); + _loadManagers(); + } + @override void dispose() { _outletNameController.dispose(); @@ -26,6 +38,31 @@ class _AddOutletPageState extends State { super.dispose(); } + Future _loadManagers() async { + try { + final managers = await _managerService.getAllBranchManagers(); + final available = managers + .where((m) => m.profileId != null) + .toList(); + if (!mounted) return; + setState(() { + _managers = available; + _isLoadingManagers = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _isLoadingManagers = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to load managers: ${e.toString()}'), + backgroundColor: Colors.orange, + ), + ); + } + } + Future _handleAddOutlet() async { // Validate inputs final outletName = _outletNameController.text.trim(); @@ -41,12 +78,52 @@ class _AddOutletPageState extends State { return; } + if (_selectedManager == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please select a branch manager'), + backgroundColor: Colors.red, + ), + ); + return; + } + + if (_selectedManager?.id == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Selected manager is invalid'), + backgroundColor: Colors.red, + ), + ); + return; + } + + if (_selectedManager?.profileId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Selected manager profile is missing'), + backgroundColor: Colors.red, + ), + ); + return; + } + setState(() => _isLoading = true); try { - await _service.createBranch( + final createdBranch = await _service.createBranch( name: outletName, location: address, + managerId: _selectedManager!.profileId, + ); + + if (createdBranch.id == null || _selectedManager?.id == null) { + throw Exception('Failed to assign branch manager'); + } + + await _managerService.updateBranchManager( + id: _selectedManager!.id!, + branchId: createdBranch.id, ); if (!mounted) return; @@ -217,6 +294,17 @@ class _AddOutletPageState extends State { hintText: 'Address', ), + const SizedBox(height: 12), + + _ManagerDropdown( + isLoading: _isLoadingManagers, + managers: _managers, + value: _selectedManager, + onChanged: (value) { + setState(() => _selectedManager = value); + }, + ), + const SizedBox(height: 32), // Add an Outlet button @@ -311,6 +399,74 @@ class _FilledField extends StatelessWidget { } } +/// Branch manager dropdown field +class _ManagerDropdown extends StatelessWidget { + const _ManagerDropdown({ + required this.isLoading, + required this.managers, + required this.value, + required this.onChanged, + }); + + final bool isLoading; + final List managers; + final BranchManager? value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Container( + height: 45, + decoration: BoxDecoration( + color: AppColors.primary100, + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: isLoading + ? const Center( + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + : DropdownButtonHideUnderline( + child: DropdownButton( + value: value, + hint: const Text( + 'Select Branch Manager', + style: TextStyle( + color: AppColors.textSecondary, + fontSize: 12, + fontFamily: 'Outfit', + fontWeight: FontWeight.w400, + ), + ), + isExpanded: true, + icon: const Icon( + Icons.keyboard_arrow_down, + color: AppColors.textTitle, + size: 20, + ), + style: const TextStyle( + fontSize: 12, + fontFamily: 'Outfit', + fontWeight: FontWeight.w400, + color: AppColors.textTitle, + ), + items: managers.map((manager) { + return DropdownMenuItem( + value: manager, + child: Text(manager.displayName), + ); + }).toList(), + onChanged: managers.isEmpty ? null : onChanged, + ), + ), + ); + } +} + /// Add an Outlet button class _GradientButton extends StatelessWidget { const _GradientButton({required this.child, this.onPressed}); diff --git a/lib/features/user/view/edit_gdm_details_page.dart b/lib/features/user/view/edit_gdm_details_page.dart index 2a36fd7..08955c9 100644 --- a/lib/features/user/view/edit_gdm_details_page.dart +++ b/lib/features/user/view/edit_gdm_details_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import '../../../core/models/branch.dart'; import '../../../core/models/branch_manager.dart'; +import '../../../core/services/branch_service.dart'; import '../../../core/services/branch_manager_service.dart'; import '../../../theme/app_colors.dart'; import '../../../shared/widgets/custom_text_field.dart'; @@ -24,11 +26,14 @@ class EditGDMDetailsPage extends StatefulWidget { class _EditGDMDetailsPageState extends State { final BranchManagerService _service = BranchManagerService(); + final BranchService _branchService = BranchService(); late final TextEditingController _firstNameController; late final TextEditingController _lastNameController; late final TextEditingController _phoneController; late final TextEditingController _emailController; - String? _selectedOutlet; + List _outlets = []; + Branch? _selectedOutlet; + bool _isLoadingOutlets = true; bool _isLoading = false; bool _isLoadingData = false; BranchManager? _gdm; @@ -46,12 +51,54 @@ class _EditGDMDetailsPageState extends State { ); _phoneController = TextEditingController(); _emailController = TextEditingController(); - _selectedOutlet = widget.gdmOutlet; + _selectedOutlet = null; // Load full data if ID is provided if (widget.gdmId != null) { _loadGDMData(); } + + _loadOutlets(); + } + + Future _loadOutlets() async { + try { + final outlets = await _branchService.getAllBranches(); + if (!mounted) return; + setState(() { + _outlets = outlets; + _isLoadingOutlets = false; + }); + _syncSelectedOutlet(); + } catch (e) { + if (!mounted) return; + setState(() { + _isLoadingOutlets = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to load outlets: ${e.toString()}'), + backgroundColor: Colors.orange, + ), + ); + } + } + + void _syncSelectedOutlet() { + final outletId = _gdm?.branchId; + if (outletId == null || _outlets.isEmpty) return; + for (final outlet in _outlets) { + if (outlet.id == outletId) { + if (mounted) { + setState(() { + _selectedOutlet = outlet; + }); + } else { + _selectedOutlet = outlet; + } + break; + } + } } Future _loadGDMData() async { @@ -67,9 +114,9 @@ class _EditGDMDetailsPageState extends State { nameParts.length > 1 ? nameParts.sublist(1).join(' ') : ''; _phoneController.text = gdm.phone ?? ''; _emailController.text = gdm.email; - _selectedOutlet = gdm.branchName; _isLoadingData = false; }); + _syncSelectedOutlet(); } catch (e) { if (mounted) { setState(() => _isLoadingData = false); @@ -118,16 +165,55 @@ class _EditGDMDetailsPageState extends State { return; } + if (_selectedOutlet == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please select an outlet'), + backgroundColor: Colors.red, + ), + ); + return; + } + + if (_selectedOutlet?.id == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Selected outlet is invalid'), + backgroundColor: Colors.red, + ), + ); + return; + } + setState(() => _isLoading = true); try { - await _service.updateBranchManager( + final previousBranchId = _gdm?.branchId; + + final updatedManager = await _service.updateBranchManager( id: widget.gdmId!, name: '$firstName $lastName', email: email, phone: phone.isNotEmpty ? phone : null, + branchId: _selectedOutlet!.id, + ); + + if (updatedManager.profileId == null) { + throw Exception('GDM profile is missing'); + } + + await _branchService.updateBranch( + id: _selectedOutlet!.id!, + managerId: updatedManager.profileId, ); + if (previousBranchId != null && previousBranchId != _selectedOutlet!.id) { + await _branchService.updateBranch( + id: previousBranchId, + clearManager: true, + ); + } + if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( @@ -389,6 +475,8 @@ class _EditGDMDetailsPageState extends State { // Outlet dropdown _OutletDropdown( + isLoading: _isLoadingOutlets, + outlets: _outlets, value: _selectedOutlet, onChanged: (value) { setState(() => _selectedOutlet = value); @@ -437,10 +525,17 @@ class _EditGDMDetailsPageState extends State { /// Outlet dropdown field class _OutletDropdown extends StatelessWidget { - const _OutletDropdown({required this.value, required this.onChanged}); + const _OutletDropdown({ + required this.isLoading, + required this.outlets, + required this.value, + required this.onChanged, + }); - final String? value; - final ValueChanged onChanged; + final bool isLoading; + final List outlets; + final Branch? value; + final ValueChanged onChanged; @override Widget build(BuildContext context) { @@ -451,41 +546,47 @@ class _OutletDropdown extends StatelessWidget { borderRadius: BorderRadius.circular(10), ), padding: const EdgeInsets.symmetric(horizontal: 16), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: value, - hint: const Text( - 'Name of the Outlet', - style: TextStyle( - color: AppColors.textSecondary, - fontSize: 12, - fontFamily: 'Outfit', - fontWeight: FontWeight.w400, - ), - ), - isExpanded: true, - icon: const Icon( - Icons.keyboard_arrow_down, - color: AppColors.textTitle, - size: 20, - ), - style: const TextStyle( - fontSize: 12, - fontFamily: 'Outfit', - fontWeight: FontWeight.w400, - color: AppColors.textTitle, - ), - items: const [ - DropdownMenuItem( - value: 'Kottawa Outlet', - child: Text('Kottawa Outlet'), + child: isLoading + ? const Center( + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + : DropdownButtonHideUnderline( + child: DropdownButton( + value: value, + hint: const Text( + 'Name of the Outlet', + style: TextStyle( + color: AppColors.textSecondary, + fontSize: 12, + fontFamily: 'Outfit', + fontWeight: FontWeight.w400, + ), + ), + isExpanded: true, + icon: const Icon( + Icons.keyboard_arrow_down, + color: AppColors.textTitle, + size: 20, + ), + style: const TextStyle( + fontSize: 12, + fontFamily: 'Outfit', + fontWeight: FontWeight.w400, + color: AppColors.textTitle, + ), + items: outlets.map((outlet) { + return DropdownMenuItem( + value: outlet, + child: Text(outlet.name), + ); + }).toList(), + onChanged: outlets.isEmpty ? null : onChanged, + ), ), - DropdownMenuItem(value: 'Outlet2', child: Text('Outlet2')), - DropdownMenuItem(value: 'Outlet3', child: Text('Outlet3')), - ], - onChanged: onChanged, - ), - ), ); } } diff --git a/lib/features/user/view/edit_outlet_page.dart b/lib/features/user/view/edit_outlet_page.dart index e4644bf..71074ee 100644 --- a/lib/features/user/view/edit_outlet_page.dart +++ b/lib/features/user/view/edit_outlet_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import '../../../core/models/branch.dart'; +import '../../../core/models/branch_manager.dart'; +import '../../../core/services/branch_manager_service.dart'; import '../../../core/services/branch_service.dart'; import '../../../theme/app_colors.dart'; @@ -21,10 +23,14 @@ class EditOutletPage extends StatefulWidget { class _EditOutletPageState extends State { final BranchService _service = BranchService(); + final BranchManagerService _managerService = BranchManagerService(); late final TextEditingController _outletNameController; late final TextEditingController _cityNameController; late final TextEditingController _phoneController; late final TextEditingController _addressController; + List _managers = []; + BranchManager? _selectedManager; + bool _isLoadingManagers = true; bool _isLoading = false; bool _isLoadingData = false; Branch? _outlet; @@ -40,6 +46,47 @@ class _EditOutletPageState extends State { if (widget.outletId != null) { _loadOutletData(); } + _loadManagers(); + } + + Future _loadManagers() async { + try { + final managers = await _managerService.getAllBranchManagers(); + if (!mounted) return; + setState(() { + _managers = managers; + _isLoadingManagers = false; + }); + _syncSelectedManager(); + } catch (e) { + if (!mounted) return; + setState(() { + _isLoadingManagers = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to load managers: ${e.toString()}'), + backgroundColor: Colors.orange, + ), + ); + } + } + + void _syncSelectedManager() { + final outletManagerId = _outlet?.managerId; + if (outletManagerId == null || _managers.isEmpty) return; + for (final manager in _managers) { + if (manager.profileId == outletManagerId || manager.id == outletManagerId) { + if (mounted) { + setState(() { + _selectedManager = manager; + }); + } else { + _selectedManager = manager; + } + break; + } + } } Future _loadOutletData() async { @@ -53,6 +100,7 @@ class _EditOutletPageState extends State { _addressController.text = outlet.location; _isLoadingData = false; }); + _syncSelectedManager(); } catch (e) { if (mounted) { setState(() => _isLoadingData = false); @@ -99,13 +147,66 @@ class _EditOutletPageState extends State { return; } + if (_selectedManager == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please select a branch manager'), + backgroundColor: Colors.red, + ), + ); + return; + } + + if (_selectedManager?.id == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Selected manager is invalid'), + backgroundColor: Colors.red, + ), + ); + return; + } + + if (_selectedManager?.profileId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Selected manager profile is missing'), + backgroundColor: Colors.red, + ), + ); + return; + } + setState(() => _isLoading = true); try { + BranchManager? previousManager; + for (final manager in _managers) { + if (manager.profileId == _outlet?.managerId || + manager.id == _outlet?.managerId) { + previousManager = manager; + break; + } + } + await _service.updateBranch( id: widget.outletId!, name: outletName, location: address, + managerId: _selectedManager!.profileId, + ); + + if (previousManager?.id != null && + previousManager!.id != _selectedManager!.id) { + await _managerService.updateBranchManager( + id: previousManager!.id!, + clearBranch: true, + ); + } + + await _managerService.updateBranchManager( + id: _selectedManager!.id!, + branchId: widget.outletId, ); if (!mounted) return; @@ -200,6 +301,10 @@ class _EditOutletPageState extends State { @override Widget build(BuildContext context) { + final availableManagers = _managers + .where((m) => m.profileId != null) + .toList(); + return Scaffold( appBar: AppBar( centerTitle: true, @@ -340,6 +445,17 @@ class _EditOutletPageState extends State { hintText: 'Address', ), + const SizedBox(height: 12), + + _ManagerDropdown( + isLoading: _isLoadingManagers, + managers: availableManagers, + value: _selectedManager, + onChanged: (value) { + setState(() => _selectedManager = value); + }, + ), + const SizedBox(height: 24), // Remove Outlet button @@ -439,6 +555,74 @@ class _FilledField extends StatelessWidget { } } +/// Branch manager dropdown field +class _ManagerDropdown extends StatelessWidget { + const _ManagerDropdown({ + required this.isLoading, + required this.managers, + required this.value, + required this.onChanged, + }); + + final bool isLoading; + final List managers; + final BranchManager? value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Container( + height: 45, + decoration: BoxDecoration( + color: AppColors.primary100, + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: isLoading + ? const Center( + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + : DropdownButtonHideUnderline( + child: DropdownButton( + value: value, + hint: const Text( + 'Select Branch Manager', + style: TextStyle( + color: AppColors.textSecondary, + fontSize: 12, + fontFamily: 'Outfit', + fontWeight: FontWeight.w400, + ), + ), + isExpanded: true, + icon: const Icon( + Icons.keyboard_arrow_down, + color: AppColors.textTitle, + size: 20, + ), + style: const TextStyle( + fontSize: 12, + fontFamily: 'Outfit', + fontWeight: FontWeight.w400, + color: AppColors.textTitle, + ), + items: managers.map((manager) { + return DropdownMenuItem( + value: manager, + child: Text(manager.displayName), + ); + }).toList(), + onChanged: managers.isEmpty ? null : onChanged, + ), + ), + ); + } +} + /// Remove Outlet button class _RemoveButton extends StatelessWidget { const _RemoveButton({required this.onPressed}); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 1771448..053954a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,14 @@ import Foundation import file_selector_macos import package_info_plus +import path_provider_foundation import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) }