From c1f9bca61441aba2880dd1fd6d90c2be40f1f844 Mon Sep 17 00:00:00 2001 From: Ashenafi-Tesfaye <65870089+Ashenafi-Tesfaye@users.noreply.github.com> Date: Wed, 30 Jul 2025 10:58:25 -0400 Subject: [PATCH 01/18] be changes to support m to m links ai enhancement notification using ws and http and related changes --- .../com/careconnect/config/SecurityConfig.java | 16 ++++++++-------- .../Flutter/GeneratedPluginRegistrant.swift | 2 ++ .../flutter/generated_plugin_registrant.cc | 3 +++ .../windows/flutter/generated_plugins.cmake | 1 + 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/config/SecurityConfig.java b/careconnect2025/backend/core/src/main/java/com/careconnect/config/SecurityConfig.java index f2a8ac9a..c90f06c4 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/config/SecurityConfig.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/config/SecurityConfig.java @@ -30,13 +30,13 @@ SecurityFilterChain filterChain(HttpSecurity http, .cors(cors -> cors.configurationSource(corsConfigurationSource)) .sessionManagement(sm -> sm .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .httpBasic(basic -> basic - .authenticationEntryPoint((req, res, e) -> - res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Basic Authentication Required"))) - .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) - .exceptionHandling(ex -> ex - .authenticationEntryPoint((req, res, e) -> - res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"))) + // .httpBasic(basic -> basic + // .authenticationEntryPoint((req, res, e) -> + // res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Basic Authentication Required"))) + // .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + // .exceptionHandling(ex -> ex + // .authenticationEntryPoint((req, res, e) -> + // res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"))) .authorizeHttpRequests(auth -> auth /* ---------- Swagger/OpenAPI documentation - MUST BE FIRST --------------- */ .requestMatchers( @@ -77,7 +77,7 @@ SecurityFilterChain filterChain(HttpSecurity http, "/v1/api/patients/**" ).authenticated() - /* ---------- family member endpoints require auth -------- */ + // /* ---------- family member endpoints require auth -------- */ .requestMatchers( "/v1/api/family-members/**" ).authenticated() diff --git a/careconnect2025/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/careconnect2025/frontend/macos/Flutter/GeneratedPluginRegistrant.swift index 3efa49dc..29bc920c 100644 --- a/careconnect2025/frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/careconnect2025/frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -22,6 +22,7 @@ import geolocator_apple import iris_method_channel import mobile_scanner import path_provider_foundation +import share_plus import shared_preferences_foundation import speech_to_text_macos import sqflite_darwin @@ -46,6 +47,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { IrisMethodChannelPlugin.register(with: registry.registrar(forPlugin: "IrisMethodChannelPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SpeechToTextMacosPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextMacosPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/careconnect2025/frontend/windows/flutter/generated_plugin_registrant.cc b/careconnect2025/frontend/windows/flutter/generated_plugin_registrant.cc index d4586132..7371ae0c 100644 --- a/careconnect2025/frontend/windows/flutter/generated_plugin_registrant.cc +++ b/careconnect2025/frontend/windows/flutter/generated_plugin_registrant.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -46,6 +47,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("IrisMethodChannelPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowToFrontPluginRegisterWithRegistrar( diff --git a/careconnect2025/frontend/windows/flutter/generated_plugins.cmake b/careconnect2025/frontend/windows/flutter/generated_plugins.cmake index c3e7b27a..0f1253c5 100644 --- a/careconnect2025/frontend/windows/flutter/generated_plugins.cmake +++ b/careconnect2025/frontend/windows/flutter/generated_plugins.cmake @@ -15,6 +15,7 @@ list(APPEND FLUTTER_PLUGIN_LIST geolocator_windows iris_method_channel permission_handler_windows + share_plus url_launcher_windows window_to_front ) From 428fc3e042cb0c1249945be68cda4c3b3c6f3d2d Mon Sep 17 00:00:00 2001 From: Ashenafi-Tesfaye <65870089+Ashenafi-Tesfaye@users.noreply.github.com> Date: Wed, 30 Jul 2025 17:51:32 -0400 Subject: [PATCH 02/18] reverted unintentional security changes --- .../com/careconnect/config/SecurityConfig.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/config/SecurityConfig.java b/careconnect2025/backend/core/src/main/java/com/careconnect/config/SecurityConfig.java index c90f06c4..3d26a355 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/config/SecurityConfig.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/config/SecurityConfig.java @@ -30,13 +30,13 @@ SecurityFilterChain filterChain(HttpSecurity http, .cors(cors -> cors.configurationSource(corsConfigurationSource)) .sessionManagement(sm -> sm .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - // .httpBasic(basic -> basic - // .authenticationEntryPoint((req, res, e) -> - // res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Basic Authentication Required"))) - // .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) - // .exceptionHandling(ex -> ex - // .authenticationEntryPoint((req, res, e) -> - // res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"))) + .httpBasic(basic -> basic + .authenticationEntryPoint((req, res, e) -> + res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Basic Authentication Required"))) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(ex -> ex + .authenticationEntryPoint((req, res, e) -> + res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"))) .authorizeHttpRequests(auth -> auth /* ---------- Swagger/OpenAPI documentation - MUST BE FIRST --------------- */ .requestMatchers( From 45d6b9cdf7bbb5b8a428a4e2e826a2df42bc78c5 Mon Sep 17 00:00:00 2001 From: Ashenafi-Tesfaye <65870089+Ashenafi-Tesfaye@users.noreply.github.com> Date: Fri, 1 Aug 2025 04:45:19 -0400 Subject: [PATCH 03/18] bug fixes and refactoring ag --- .../core/flutter_integration_template.dart | 726 ---------- .../careconnect/config/FirebaseConfig.java | 77 -- .../controller/AIChatController.java | 65 +- .../controller/AnalyticsController.java | 3 +- .../controller/NotificationController.java | 7 +- .../controller/PatientController.java | 4 +- .../controller/WebSocketController.java | 51 + .../java/com/careconnect/dto/ChatRequest.java | 3 +- .../com/careconnect/dto/ChatResponse.java | 2 + .../dto/FamilyMemberRegistration.java | 4 +- .../careconnect/dto/PatientAIConfigDTO.java | 58 - .../com/careconnect/dto/UserAIConfigDTO.java | 66 + .../com/careconnect/model/Achievement.java | 3 + .../java/com/careconnect/model/Allergy.java | 41 +- .../careconnect/model/ChatConversation.java | 7 +- .../java/com/careconnect/model/Patient.java | 3 + ...PatientAIConfig.java => UserAIConfig.java} | 164 ++- .../repository/PatientAIConfigRepository.java | 20 - .../repository/UserAIConfigRepository.java | 18 + .../security/JwtAuthenticationFilter.java | 6 + .../careconnect/service/CaregiverService.java | 33 +- .../service/ConnectionRequestService.java | 2 +- .../service/DefaultAIChatService.java | 40 +- .../service/FileManagementService.java | 17 +- .../service/FirebaseNotificationService.java | 415 ------ .../service/LangChainAIChatService.java | 68 +- .../service/MedicalContextService.java | 12 +- .../service/NotificationService.java | 42 + .../service/PatientAIConfigService.java | 142 -- .../PrivacyAwareMedicalContextService.java | 4 +- .../service/UserAIConfigService.java | 141 ++ .../service/VitalSampleService.java | 7 +- .../service/WebSocketNotificationService.java | 7 + .../websocket/CallNotificationHandler.java | 2 +- .../CareConnectWebSocketHandler.java | 13 +- .../V10__add_device_tokens_table.sql | 18 + .../V11__update_pain_scale_to_0_10.sql | 11 + .../migration/V12__create_ai_chat_tables.sql | 89 ++ .../V13__add_profile_image_url_to_users.sql} | 0 .../V13__sync_users_table_with_model.sql | 7 + .../migration/V3__add_mood_pain_log_table.sql | 18 + ...__add_patient_id_to_family_member_link.sql | 35 + .../V5_refactored_patient_caregiver_rship.sql | 10 + .../migration/V6__add_subscription_plans.sql | 31 + .../migration/V7__add_vital_sample_table.sql | 20 + .../V8__add_gender_and_allergies.sql | 26 + .../V9__add_patient_medication_table.sql | 26 + .../config/CareconnectTestConfig.java | 28 + .../config/TestConfiguration.java} | 0 .../frontend/add-profile-settings.sh | 21 - careconnect2025/frontend/conflict-resolver.sh | 164 +++ .../frontend/lib/config/env_constant.dart | 48 + .../lib/config/environment_config.dart | 50 - .../features/analytics/analytics_page.dart | 53 +- .../analytics/models/vital_model.dart | 52 +- .../presentation/pages/sign_up_screen.dart | 12 +- .../presentation/emotioncheckscreen.dart | 150 --- .../presentation/pages/patient_dashboard.dart | 14 + .../dashboard/presentation/patient_call.dart | 289 ---- .../pages/profile_settings_page.dart | 33 +- .../frontend/lib/firebase_options.dart | 72 - careconnect2025/frontend/lib/fix_imports.dart | 0 .../frontend/lib/fix_imports_final.dart | 0 careconnect2025/frontend/lib/main.dart | 175 ++- .../lib/pages/ai_configuration_page.dart | 158 ++- .../lib/pages/file_management_page.dart | 405 ++++-- .../frontend/lib/pages/profile_page.dart | 62 +- .../frontend/lib/pages/settings_page.dart | 203 +-- .../lib/services/ai_chat_config_service.dart | 379 ------ .../lib/services/ai_chat_service.dart | 82 +- .../lib/services/ai_config_service.dart | 132 +- .../frontend/lib/services/ai_service.dart | 22 +- .../frontend/lib/services/api_service.dart | 210 +-- .../lib/services/api_service_new.dart | 0 .../frontend/lib/services/auth_service.dart | 23 + .../lib/services/auth_service_new.dart | 0 .../services/call_notification_service.dart | 165 +-- .../lib/services/firebase_auth_service.dart | 270 ---- .../lib/services/generic_webrtc_service.dart | 97 ++ .../lib/services/messaging_service.dart | 391 ++---- .../lib/services/notification_service.dart | 21 + .../lib/services/real_video_call_service.dart | 22 +- .../lib/services/video_call_service_base.dart | 12 + .../lib/services/web_video_call_service.dart | 157 +-- .../lib/services/webrtc_signaling.dart | 20 + .../services/webrtc_signaling_service.dart | 5 +- .../lib/utils/call_integration_helper.dart | 18 +- .../lib/widgets/ai_chat_consolidated.dart | 0 .../lib/widgets/ai_chat_improved.dart | 1182 +++++------------ .../call_notification_status_indicator.dart | 30 +- .../lib/widgets/file_upload_widget.dart | 301 ++--- .../lib/widgets/messaging_widget.dart | 2 +- .../lib/widgets/video_call_widget.dart | 269 ++-- 93 files changed, 3192 insertions(+), 5171 deletions(-) delete mode 100644 careconnect2025/backend/core/flutter_integration_template.dart delete mode 100644 careconnect2025/backend/core/src/main/java/com/careconnect/config/FirebaseConfig.java create mode 100644 careconnect2025/backend/core/src/main/java/com/careconnect/dto/UserAIConfigDTO.java rename careconnect2025/backend/core/src/main/java/com/careconnect/model/{PatientAIConfig.java => UserAIConfig.java} (63%) create mode 100644 careconnect2025/backend/core/src/main/java/com/careconnect/repository/UserAIConfigRepository.java delete mode 100644 careconnect2025/backend/core/src/main/java/com/careconnect/service/FirebaseNotificationService.java create mode 100644 careconnect2025/backend/core/src/main/java/com/careconnect/service/NotificationService.java create mode 100644 careconnect2025/backend/core/src/main/java/com/careconnect/service/UserAIConfigService.java create mode 100644 careconnect2025/backend/core/src/main/resources/db/migration/V10__add_device_tokens_table.sql create mode 100644 careconnect2025/backend/core/src/main/resources/db/migration/V11__update_pain_scale_to_0_10.sql create mode 100644 careconnect2025/backend/core/src/main/resources/db/migration/V12__create_ai_chat_tables.sql rename careconnect2025/backend/core/src/main/{java/com/careconnect/websocket/WebSocketConfig.java => resources/db/migration/V13__add_profile_image_url_to_users.sql} (100%) create mode 100644 careconnect2025/backend/core/src/main/resources/db/migration/V13__sync_users_table_with_model.sql create mode 100644 careconnect2025/backend/core/src/main/resources/db/migration/V3__add_mood_pain_log_table.sql create mode 100644 careconnect2025/backend/core/src/main/resources/db/migration/V4__add_patient_id_to_family_member_link.sql create mode 100644 careconnect2025/backend/core/src/main/resources/db/migration/V5_refactored_patient_caregiver_rship.sql create mode 100644 careconnect2025/backend/core/src/main/resources/db/migration/V6__add_subscription_plans.sql create mode 100644 careconnect2025/backend/core/src/main/resources/db/migration/V7__add_vital_sample_table.sql create mode 100644 careconnect2025/backend/core/src/main/resources/db/migration/V8__add_gender_and_allergies.sql create mode 100644 careconnect2025/backend/core/src/main/resources/db/migration/V9__add_patient_medication_table.sql create mode 100644 careconnect2025/backend/core/src/test/java/com/careconnect/config/CareconnectTestConfig.java rename careconnect2025/{frontend/lib/features/dashboard/presentation/patient_main_screen_fixed.dart => backend/core/src/test/java/com/careconnect/config/TestConfiguration.java} (100%) delete mode 100755 careconnect2025/frontend/add-profile-settings.sh create mode 100755 careconnect2025/frontend/conflict-resolver.sh delete mode 100644 careconnect2025/frontend/lib/features/dashboard/presentation/emotioncheckscreen.dart delete mode 100644 careconnect2025/frontend/lib/features/dashboard/presentation/patient_call.dart delete mode 100644 careconnect2025/frontend/lib/firebase_options.dart delete mode 100644 careconnect2025/frontend/lib/fix_imports.dart delete mode 100644 careconnect2025/frontend/lib/fix_imports_final.dart delete mode 100644 careconnect2025/frontend/lib/services/ai_chat_config_service.dart delete mode 100644 careconnect2025/frontend/lib/services/api_service_new.dart delete mode 100644 careconnect2025/frontend/lib/services/auth_service_new.dart delete mode 100644 careconnect2025/frontend/lib/services/firebase_auth_service.dart create mode 100644 careconnect2025/frontend/lib/services/generic_webrtc_service.dart create mode 100644 careconnect2025/frontend/lib/services/notification_service.dart create mode 100644 careconnect2025/frontend/lib/services/video_call_service_base.dart create mode 100644 careconnect2025/frontend/lib/services/webrtc_signaling.dart delete mode 100644 careconnect2025/frontend/lib/widgets/ai_chat_consolidated.dart diff --git a/careconnect2025/backend/core/flutter_integration_template.dart b/careconnect2025/backend/core/flutter_integration_template.dart deleted file mode 100644 index 5ec18cee..00000000 --- a/careconnect2025/backend/core/flutter_integration_template.dart +++ /dev/null @@ -1,726 +0,0 @@ -// CareConnect Flutter Integration Template -// Copy this code into your Flutter project and customize as needed - -import 'package:http/http.dart' as http; -import 'package:web_socket_channel/web_socket_channel.dart'; -import 'dart:convert'; -import 'dart:async'; - -// =========================== -// 1. API SERVICE CLASS -// =========================== - -class CareConnectApiService { - static const String baseUrl = 'http://localhost:8080/api'; // Change for production - String? _authToken; - - // Singleton pattern - static final CareConnectApiService _instance = CareConnectApiService._internal(); - factory CareConnectApiService() => _instance; - CareConnectApiService._internal(); - - void setAuthToken(String token) { - _authToken = token; - } - - String? get authToken => _authToken; - - Map get _headers => { - 'Content-Type': 'application/json', - if (_authToken != null) 'Authorization': 'Bearer $_authToken', - }; - - // Authentication - Future> login(String email, String password) async { - try { - final response = await http.post( - Uri.parse('$baseUrl/auth/login'), - headers: {'Content-Type': 'application/json'}, - body: json.encode({ - 'email': email, - 'password': password, - }), - ); - - final Map data = json.decode(response.body); - - if (response.statusCode == 200 && data['success'] == true) { - final loginResponse = LoginResponse.fromJson(data['data']); - setAuthToken(loginResponse.token); - return ApiResponse.success(loginResponse); - } else { - return ApiResponse.error(data['message'] ?? 'Login failed'); - } - } catch (e) { - return ApiResponse.error('Network error: $e'); - } - } - - // AI Chat - Future> startAIChat(String message, {String provider = 'openai'}) async { - try { - final response = await http.post( - Uri.parse('$baseUrl/ai-chat/start'), - headers: _headers, - body: json.encode({ - 'provider': provider, - 'contextType': 'general_health', - 'initialMessage': message, - }), - ); - - final Map data = json.decode(response.body); - - if (response.statusCode == 200 && data['success'] == true) { - return ApiResponse.success(AIChatSession.fromJson(data['data'])); - } else { - return ApiResponse.error(data['message'] ?? 'Failed to start AI chat'); - } - } catch (e) { - return ApiResponse.error('Network error: $e'); - } - } - - Future> continueAIChat(String sessionId, String message) async { - try { - final response = await http.post( - Uri.parse('$baseUrl/ai-chat/continue'), - headers: _headers, - body: json.encode({ - 'sessionId': sessionId, - 'message': message, - }), - ); - - final Map data = json.decode(response.body); - - if (response.statusCode == 200 && data['success'] == true) { - return ApiResponse.success(ChatMessage.fromJson(data['data']['message'])); - } else { - return ApiResponse.error(data['message'] ?? 'Failed to continue chat'); - } - } catch (e) { - return ApiResponse.error('Network error: $e'); - } - } - - // WebSocket Operations - Future> sendCallInvitation({ - required String recipientId, - required String senderName, - required String callId, - bool isVideoCall = true, - String callType = 'general', - }) async { - try { - final response = await http.post( - Uri.parse('$baseUrl/websocket/call-invitation'), - headers: _headers, - body: json.encode({ - 'recipientId': recipientId, - 'senderId': 'current-user-id', // Replace with actual current user ID - 'senderName': senderName, - 'callId': callId, - 'isVideoCall': isVideoCall, - 'callType': callType, - }), - ); - - final Map data = json.decode(response.body); - - if (response.statusCode == 200 && data['success'] == true) { - return ApiResponse.success(true); - } else { - return ApiResponse.error(data['message'] ?? 'Failed to send call invitation'); - } - } catch (e) { - return ApiResponse.error('Network error: $e'); - } - } - - Future> sendSMSNotification({ - required String recipientId, - required String senderName, - required String message, - String messageType = 'general', - }) async { - try { - final response = await http.post( - Uri.parse('$baseUrl/websocket/sms-notification'), - headers: _headers, - body: json.encode({ - 'recipientId': recipientId, - 'senderId': 'current-user-id', // Replace with actual current user ID - 'senderName': senderName, - 'message': message, - 'messageType': messageType, - }), - ); - - final Map data = json.decode(response.body); - - if (response.statusCode == 200 && data['success'] == true) { - return ApiResponse.success(true); - } else { - return ApiResponse.error(data['message'] ?? 'Failed to send SMS notification'); - } - } catch (e) { - return ApiResponse.error('Network error: $e'); - } - } -} - -// =========================== -// 2. WEBSOCKET SERVICE CLASS -// =========================== - -class CareConnectWebSocketService { - WebSocketChannel? _callChannel; - WebSocketChannel? _healthChannel; - String? _token; - - // Stream controllers for different message types - final StreamController _callInvitationController = - StreamController.broadcast(); - final StreamController _smsNotificationController = - StreamController.broadcast(); - final StreamController _medicationReminderController = - StreamController.broadcast(); - final StreamController _connectionStatusController = - StreamController.broadcast(); - - // Public streams - Stream get callInvitations => _callInvitationController.stream; - Stream get smsNotifications => _smsNotificationController.stream; - Stream get medicationReminders => _medicationReminderController.stream; - Stream get connectionStatus => _connectionStatusController.stream; - - // Singleton pattern - static final CareConnectWebSocketService _instance = CareConnectWebSocketService._internal(); - factory CareConnectWebSocketService() => _instance; - CareConnectWebSocketService._internal(); - - void initialize(String token) { - _token = token; - } - - void connectToCallService() { - if (_token == null) { - print('Error: No authentication token provided'); - return; - } - - try { - final uri = Uri.parse('ws://localhost:8080/ws/calls?token=$_token'); - _callChannel = WebSocketChannel.connect(uri); - - _callChannel!.stream.listen( - (message) { - final data = json.decode(message); - _handleCallMessage(data); - _connectionStatusController.add(true); - }, - onError: (error) { - print('Call WebSocket Error: $error'); - _connectionStatusController.add(false); - _reconnectCallService(); - }, - onDone: () { - print('Call WebSocket Connection Closed'); - _connectionStatusController.add(false); - _reconnectCallService(); - }, - ); - - print('Connected to Call WebSocket service'); - } catch (e) { - print('Failed to connect to Call WebSocket: $e'); - _connectionStatusController.add(false); - } - } - - void connectToHealthService() { - if (_token == null) { - print('Error: No authentication token provided'); - return; - } - - try { - final uri = Uri.parse('ws://localhost:8080/ws/careconnect?token=$_token'); - _healthChannel = WebSocketChannel.connect(uri); - - _healthChannel!.stream.listen( - (message) { - final data = json.decode(message); - _handleHealthMessage(data); - }, - onError: (error) { - print('Health WebSocket Error: $error'); - _reconnectHealthService(); - }, - onDone: () { - print('Health WebSocket Connection Closed'); - _reconnectHealthService(); - }, - ); - - print('Connected to Health WebSocket service'); - } catch (e) { - print('Failed to connect to Health WebSocket: $e'); - } - } - - void _handleCallMessage(Map message) { - final type = message['type']; - final data = message['data']; - final timestamp = message['timestamp']; - - print('Received call message: $type'); - - switch (type) { - case 'call_invitation': - final invitation = CallInvitation.fromJson(data, timestamp); - _callInvitationController.add(invitation); - break; - case 'sms_notification': - final sms = SMSNotification.fromJson(data, timestamp); - _smsNotificationController.add(sms); - break; - default: - print('Unknown call message type: $type'); - } - } - - void _handleHealthMessage(Map message) { - final type = message['type']; - final data = message['data']; - final timestamp = message['timestamp']; - - print('Received health message: $type'); - - switch (type) { - case 'medication_reminder': - final reminder = MedicationReminder.fromJson(data, timestamp); - _medicationReminderController.add(reminder); - break; - case 'vital_signs_alert': - // Handle vital signs alert - print('Vital signs alert: ${data['alertMessage']}'); - break; - case 'emergency_alert': - // Handle emergency alert - print('Emergency alert: ${data['alertMessage']}'); - break; - default: - print('Unknown health message type: $type'); - } - } - - void respondToCall(String callId, String response) { - if (_callChannel == null) { - print('Error: Call channel not connected'); - return; - } - - final message = { - 'type': 'call_response', - 'data': { - 'callId': callId, - 'response': response, // 'accept' or 'decline' - 'userId': 'current-user-id', // Replace with actual user ID - } - }; - - _callChannel!.sink.add(json.encode(message)); - print('Sent call response: $response for call $callId'); - } - - void _reconnectCallService() { - print('Attempting to reconnect to call service...'); - Future.delayed(Duration(seconds: 5), () { - connectToCallService(); - }); - } - - void _reconnectHealthService() { - print('Attempting to reconnect to health service...'); - Future.delayed(Duration(seconds: 5), () { - connectToHealthService(); - }); - } - - void disconnect() { - _callChannel?.sink.close(); - _healthChannel?.sink.close(); - _connectionStatusController.add(false); - print('WebSocket connections closed'); - } - - void dispose() { - disconnect(); - _callInvitationController.close(); - _smsNotificationController.close(); - _medicationReminderController.close(); - _connectionStatusController.close(); - } -} - -// =========================== -// 3. DATA MODELS -// =========================== - -class ApiResponse { - final bool success; - final T? data; - final String? error; - - ApiResponse.success(this.data) : success = true, error = null; - ApiResponse.error(this.error) : success = false, data = null; -} - -class LoginResponse { - final String token; - final String refreshToken; - final User user; - final int expiresIn; - - LoginResponse({ - required this.token, - required this.refreshToken, - required this.user, - required this.expiresIn, - }); - - factory LoginResponse.fromJson(Map json) { - return LoginResponse( - token: json['token'], - refreshToken: json['refreshToken'], - user: User.fromJson(json['user']), - expiresIn: json['expiresIn'], - ); - } -} - -class User { - final String id; - final String email; - final String firstName; - final String lastName; - final String role; - final String? phoneNumber; - final bool isActive; - - User({ - required this.id, - required this.email, - required this.firstName, - required this.lastName, - required this.role, - this.phoneNumber, - required this.isActive, - }); - - factory User.fromJson(Map json) { - return User( - id: json['id'], - email: json['email'], - firstName: json['firstName'], - lastName: json['lastName'], - role: json['role'], - phoneNumber: json['phoneNumber'], - isActive: json['isActive'], - ); - } -} - -class AIChatSession { - final String sessionId; - final String provider; - final String contextType; - final List messages; - - AIChatSession({ - required this.sessionId, - required this.provider, - required this.contextType, - required this.messages, - }); - - factory AIChatSession.fromJson(Map json) { - return AIChatSession( - sessionId: json['sessionId'], - provider: json['provider'], - contextType: json['contextType'], - messages: (json['messages'] as List) - .map((msg) => ChatMessage.fromJson(msg)) - .toList(), - ); - } -} - -class ChatMessage { - final String id; - final String role; - final String content; - final DateTime timestamp; - - ChatMessage({ - required this.id, - required this.role, - required this.content, - required this.timestamp, - }); - - factory ChatMessage.fromJson(Map json) { - return ChatMessage( - id: json['id'], - role: json['role'], - content: json['content'], - timestamp: DateTime.parse(json['timestamp']), - ); - } -} - -class CallInvitation { - final String senderId; - final String senderName; - final String callId; - final bool isVideoCall; - final String callType; - final String message; - final DateTime timestamp; - - CallInvitation({ - required this.senderId, - required this.senderName, - required this.callId, - required this.isVideoCall, - required this.callType, - required this.message, - required this.timestamp, - }); - - factory CallInvitation.fromJson(Map json, String timestampStr) { - return CallInvitation( - senderId: json['senderId'], - senderName: json['senderName'], - callId: json['callId'], - isVideoCall: json['isVideoCall'], - callType: json['callType'], - message: json['message'], - timestamp: DateTime.parse(timestampStr), - ); - } -} - -class SMSNotification { - final String senderId; - final String senderName; - final String message; - final String messageType; - final DateTime timestamp; - - SMSNotification({ - required this.senderId, - required this.senderName, - required this.message, - required this.messageType, - required this.timestamp, - }); - - factory SMSNotification.fromJson(Map json, String timestampStr) { - return SMSNotification( - senderId: json['senderId'], - senderName: json['senderName'], - message: json['message'], - messageType: json['messageType'], - timestamp: DateTime.parse(timestampStr), - ); - } -} - -class MedicationReminder { - final String medicationName; - final String reminderTime; - final String dosage; - final String message; - final DateTime timestamp; - - MedicationReminder({ - required this.medicationName, - required this.reminderTime, - required this.dosage, - required this.message, - required this.timestamp, - }); - - factory MedicationReminder.fromJson(Map json, String timestampStr) { - return MedicationReminder( - medicationName: json['medicationName'], - reminderTime: json['reminderTime'], - dosage: json['dosage'], - message: json['message'], - timestamp: DateTime.parse(timestampStr), - ); - } -} - -// =========================== -// 4. USAGE EXAMPLE WIDGET -// =========================== - -/* -Usage Example in your Flutter app: - -class CareConnectHomePage extends StatefulWidget { - @override - _CareConnectHomePageState createState() => _CareConnectHomePageState(); -} - -class _CareConnectHomePageState extends State { - final CareConnectApiService _apiService = CareConnectApiService(); - final CareConnectWebSocketService _wsService = CareConnectWebSocketService(); - - StreamSubscription? _callSubscription; - StreamSubscription? _smsSubscription; - StreamSubscription? _connectionSubscription; - - @override - void initState() { - super.initState(); - _initializeServices(); - } - - void _initializeServices() { - // Initialize WebSocket service with token (after login) - // _wsService.initialize(authToken); - // _wsService.connectToCallService(); - // _wsService.connectToHealthService(); - - // Listen to incoming calls - _callSubscription = _wsService.callInvitations.listen((invitation) { - _showCallInvitationDialog(invitation); - }); - - // Listen to SMS notifications - _smsSubscription = _wsService.smsNotifications.listen((sms) { - _showSMSNotification(sms); - }); - - // Listen to connection status - _connectionSubscription = _wsService.connectionStatus.listen((isConnected) { - print('WebSocket connection status: $isConnected'); - }); - } - - void _showCallInvitationDialog(CallInvitation invitation) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Incoming Call'), - content: Text('${invitation.senderName} is calling you'), - actions: [ - TextButton( - onPressed: () { - _wsService.respondToCall(invitation.callId, 'decline'); - Navigator.of(context).pop(); - }, - child: Text('Decline'), - ), - TextButton( - onPressed: () { - _wsService.respondToCall(invitation.callId, 'accept'); - Navigator.of(context).pop(); - // Navigate to call screen - }, - child: Text('Accept'), - ), - ], - ), - ); - } - - void _showSMSNotification(SMSNotification sms) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${sms.senderName}: ${sms.message}'), - action: SnackBarAction( - label: 'View', - onPressed: () { - // Navigate to SMS details - }, - ), - ), - ); - } - - @override - void dispose() { - _callSubscription?.cancel(); - _smsSubscription?.cancel(); - _connectionSubscription?.cancel(); - _wsService.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('CareConnect')), - body: Center( - child: Column( - children: [ - ElevatedButton( - onPressed: _testLogin, - child: Text('Test Login'), - ), - ElevatedButton( - onPressed: _testAIChat, - child: Text('Test AI Chat'), - ), - ElevatedButton( - onPressed: _testSendCall, - child: Text('Test Send Call'), - ), - ], - ), - ), - ); - } - - void _testLogin() async { - final result = await _apiService.login('test@example.com', 'password'); - if (result.success) { - print('Login successful: ${result.data?.user.firstName}'); - // Initialize WebSocket with token - _wsService.initialize(result.data!.token); - _wsService.connectToCallService(); - _wsService.connectToHealthService(); - } else { - print('Login failed: ${result.error}'); - } - } - - void _testAIChat() async { - final result = await _apiService.startAIChat('I have a headache'); - if (result.success) { - print('AI Chat started: ${result.data?.sessionId}'); - } else { - print('AI Chat failed: ${result.error}'); - } - } - - void _testSendCall() async { - final result = await _apiService.sendCallInvitation( - recipientId: 'test-user-123', - senderName: 'Test User', - callId: 'call-${DateTime.now().millisecondsSinceEpoch}', - ); - if (result.success) { - print('Call invitation sent successfully'); - } else { - print('Failed to send call: ${result.error}'); - } - } -} -*/ diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/config/FirebaseConfig.java b/careconnect2025/backend/core/src/main/java/com/careconnect/config/FirebaseConfig.java deleted file mode 100644 index 713cf803..00000000 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/config/FirebaseConfig.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.careconnect.config; - -import com.google.auth.oauth2.GoogleCredentials; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; -import com.google.firebase.messaging.FirebaseMessaging; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.core.io.ClassPathResource; - -import jakarta.annotation.PostConstruct; -import java.io.IOException; -import java.io.InputStream; - -@Configuration -@Profile("!test") // Don't load this configuration during tests -@ConditionalOnProperty(name = "firebase.enabled", havingValue = "true", matchIfMissing = true) -public class FirebaseConfig { - - private static final Logger logger = LoggerFactory.getLogger(FirebaseConfig.class); - - @Value("${firebase.project-id:careconnectcapstone}") - private String projectId; - - @Value("${firebase.service-account-key:firebase-service-account.json}") - private String serviceAccountKey; - - @PostConstruct - public void initializeFirebase() throws IOException { - try { - if (FirebaseApp.getApps().isEmpty()) { - ClassPathResource resource = new ClassPathResource(serviceAccountKey); - - if (!resource.exists()) { - logger.warn("Firebase service account key file not found: {}. Firebase will be disabled.", serviceAccountKey); - logger.info("To enable Firebase, please add the {} file to your classpath", serviceAccountKey); - return; // Exit gracefully instead of throwing exception - } - - try (InputStream serviceAccount = resource.getInputStream()) { - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(GoogleCredentials.fromStream(serviceAccount)) - .setProjectId(projectId) - .build(); - - FirebaseApp.initializeApp(options); - logger.info("Firebase initialized successfully for project: {}", projectId); - } - } else { - logger.info("Firebase app already initialized"); - } - } catch (Exception e) { - logger.warn("Failed to initialize Firebase: {}. Firebase will be disabled.", e.getMessage()); - logger.debug("Firebase initialization error details", e); - // Don't throw exception - allow application to start without Firebase - } - } - - @Bean - public FirebaseMessaging firebaseMessaging() { - try { - if (FirebaseApp.getApps().isEmpty()) { - logger.warn("Firebase not initialized, FirebaseMessaging bean will not be available"); - return null; - } - return FirebaseMessaging.getInstance(); - } catch (Exception e) { - logger.warn("Failed to create FirebaseMessaging bean: {}", e.getMessage()); - return null; - } - } -} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/controller/AIChatController.java b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/AIChatController.java index 9442ced5..543702f1 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/controller/AIChatController.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/AIChatController.java @@ -3,7 +3,7 @@ import com.careconnect.dto.*; import com.careconnect.model.ChatConversation; import com.careconnect.service.AIChatService; -import com.careconnect.service.PatientAIConfigService; +import com.careconnect.service.UserAIConfigService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -17,14 +17,14 @@ import org.springframework.web.bind.annotation.*; import java.util.List; -@Slf4j @RestController @RequestMapping("/v1/api/ai-chat") @RequiredArgsConstructor @Tag(name = "AI Chat", description = "AI-powered chat functionality with medical context") public class AIChatController { private final AIChatService aiChatService; - private final PatientAIConfigService patientAIConfigService; + private final UserAIConfigService userAIConfigService; + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AIChatController.class); @PostMapping("/chat") @Operation( @@ -135,35 +135,33 @@ public ResponseEntity deactivateConversation( } // AI Configuration endpoints - @GetMapping("/config/{patientId}") + @GetMapping("/config") @Operation( - summary = "Get patient AI configuration", - description = "Retrieve AI configuration settings for a specific patient" + summary = "Get AI configuration", + description = "Retrieve AI configuration settings for a user (optionally filtered by patient)" ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Configuration retrieved successfully"), @ApiResponse(responseCode = "403", description = "Access denied"), - @ApiResponse(responseCode = "404", description = "Patient not found") + @ApiResponse(responseCode = "404", description = "Configuration not found") }) - // @PreAuthorize("hasRole('PATIENT') or hasRole('CAREGIVER')") - public ResponseEntity getPatientAIConfig( - @Parameter(description = "Patient ID") @PathVariable Long patientId) { - - log.info("Retrieving AI config for patient: {}", patientId); - + public ResponseEntity getUserAIConfig( + @RequestParam Long userId, + @RequestParam(required = false) Long patientId) { + log.info("Retrieving AI config for user: {}, patient: {}", userId, patientId); try { - PatientAIConfigDTO config = patientAIConfigService.getPatientAIConfig(patientId); + UserAIConfigDTO config = userAIConfigService.getUserAIConfig(userId, patientId); return ResponseEntity.ok(config); } catch (Exception e) { - log.error("Error retrieving AI config for patient {}: ", patientId, e); + log.error("Error retrieving AI config for user: {}, patient: {}: ", userId, patientId, e); return ResponseEntity.badRequest().build(); } } @PostMapping("/config") @Operation( - summary = "Create or update patient AI configuration", - description = "Create or update AI configuration settings for a patient" + summary = "Create or update AI configuration", + description = "Create or update AI configuration settings for a user (optionally filtered by patient)" ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Configuration updated successfully"), @@ -171,43 +169,38 @@ public ResponseEntity getPatientAIConfig( @ApiResponse(responseCode = "400", description = "Invalid configuration data"), @ApiResponse(responseCode = "403", description = "Access denied") }) - // @PreAuthorize("hasRole('PATIENT') or hasRole('CAREGIVER')") - public ResponseEntity savePatientAIConfig( - @Valid @RequestBody PatientAIConfigDTO configDTO) { - - log.info("Saving AI config for patient: {}", configDTO.getPatientId()); - + public ResponseEntity saveUserAIConfig( + @Valid @RequestBody UserAIConfigDTO configDTO) { + log.info("Saving AI config for user: {}, patient: {}", configDTO.getUserId(), configDTO.getPatientId()); try { - PatientAIConfigDTO savedConfig = patientAIConfigService.savePatientAIConfig(configDTO); + UserAIConfigDTO savedConfig = userAIConfigService.saveUserAIConfig(configDTO); boolean isNew = configDTO.getId() == null; return isNew ? ResponseEntity.status(201).body(savedConfig) : ResponseEntity.ok(savedConfig); } catch (Exception e) { - log.error("Error saving AI config for patient {}: ", configDTO.getPatientId(), e); + log.error("Error saving AI config for user: {}, patient: {}: ", configDTO.getUserId(), configDTO.getPatientId(), e); return ResponseEntity.badRequest().build(); } } - @DeleteMapping("/config/{patientId}") + @DeleteMapping("/config") @Operation( - summary = "Deactivate patient AI configuration", - description = "Deactivate AI configuration for a patient (soft delete)" + summary = "Deactivate AI configuration", + description = "Deactivate AI configuration for a user (optionally filtered by patient, soft delete)" ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Configuration deactivated successfully"), @ApiResponse(responseCode = "403", description = "Access denied"), @ApiResponse(responseCode = "404", description = "Configuration not found") }) - @PreAuthorize("hasRole('PATIENT') or hasRole('CAREGIVER')") - public ResponseEntity deactivatePatientAIConfig( - @Parameter(description = "Patient ID") @PathVariable Long patientId) { - - log.info("Deactivating AI config for patient: {}", patientId); - + public ResponseEntity deactivateUserAIConfig( + @RequestParam Long userId, + @RequestParam(required = false) Long patientId) { + log.info("Deactivating AI config for user: {}, patient: {}", userId, patientId); try { - patientAIConfigService.deactivatePatientAIConfig(patientId); + userAIConfigService.deactivateUserAIConfig(userId, patientId); return ResponseEntity.ok().build(); } catch (Exception e) { - log.error("Error deactivating AI config for patient {}: ", patientId, e); + log.error("Error deactivating AI config for user: {}, patient: {}: ", userId, patientId, e); return ResponseEntity.badRequest().build(); } } diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/controller/AnalyticsController.java b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/AnalyticsController.java index 8976b082..ace25db2 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/controller/AnalyticsController.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/AnalyticsController.java @@ -48,6 +48,7 @@ @RequestMapping("/v1/api/analytics") @RequiredArgsConstructor public class AnalyticsController { + // ...existing code... @@ -67,7 +68,7 @@ public class AnalyticsController { @Autowired private final FamilyMemberLinkRepository familyMemberPatientLinkRepository; - @Autowired + @Autowired private AnalyticsService analyticsService; @Autowired diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/controller/NotificationController.java b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/NotificationController.java index 887201a4..9bf6fc55 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/controller/NotificationController.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/NotificationController.java @@ -3,7 +3,6 @@ import com.careconnect.dto.FirebaseNotificationRequest; import com.careconnect.dto.NotificationResponse; import com.careconnect.model.DeviceToken; -import com.careconnect.service.FirebaseNotificationService; import com.careconnect.websocket.NotificationWebSocketHandler; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -27,11 +26,11 @@ @ConditionalOnProperty(name = "firebase.enabled", havingValue = "true", matchIfMissing = true) public class NotificationController { - @Autowired(required = false) - private FirebaseNotificationService notificationService; - @Autowired private NotificationWebSocketHandler notificationWebSocketHandler; + + @Autowired + private com.careconnect.service.NotificationService notificationService; /** * Send a WebSocket notification to a specific user */ diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/controller/PatientController.java b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/PatientController.java index 93d6a876..e432679a 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/controller/PatientController.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/PatientController.java @@ -209,8 +209,8 @@ public ResponseEntity registerFamilyMember( registration.lastName(), registration.email(), registration.phone(), - registration.address(), - registration.relationship(), + registration.address() != null ? registration.address() : null, + registration.relationship() != null ? registration.relationship() : null, patient.getUser().getId() // Use patient's user ID, not patient ID ); diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/controller/WebSocketController.java b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/WebSocketController.java index 82e0500a..9fdca57d 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/controller/WebSocketController.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/WebSocketController.java @@ -22,6 +22,57 @@ @Tag(name = "WebSocket Management", description = "WebSocket notifications and real-time communication management") @SecurityRequirement(name = "Bearer Authentication") public class WebSocketController { + /** + * Initialize WebSocket service (dummy endpoint for client handshake/testing) + */ + @PostMapping("/init") + @Operation( + summary = "Initialize WebSocket service", + description = "Initialize or handshake with the WebSocket service via HTTP" + ) + public ResponseEntity> initializeWebSocketService(@RequestBody(required = false) Map request) { + // You can add any initialization logic here if needed + return ResponseEntity.ok(Map.of( + "success", true, + "message", "WebSocket service initialized", + "timestamp", System.currentTimeMillis() + )); + } + + /** + * Register a user for WebSocket notifications + */ + @PostMapping("/register-user") + @Operation( + summary = "Register user for WebSocket notifications", + description = "Register a user for WebSocket notifications via HTTP" + ) + public ResponseEntity> registerUserForWebSocket(@RequestBody Map request) { + try { + String userId = (String) request.get("userId"); + String userName = (String) request.get("userName"); + if (userId == null || userName == null) { + return ResponseEntity.badRequest().body(Map.of( + "success", false, + "message", "Missing required fields: userId and userName are required" + )); + } + // Register user in the WebSocket service (dummy logic, replace with real registration if needed) + webSocketNotificationService.registerUser(userId, userName); + return ResponseEntity.ok(Map.of( + "success", true, + "message", "User registered for WebSocket notifications", + "userId", userId, + "userName", userName + )); + } catch (Exception e) { + log.error("Error registering user for WebSocket", e); + return ResponseEntity.badRequest().body(Map.of( + "success", false, + "message", "Failed to register user: " + e.getMessage() + )); + } + } private final WebSocketNotificationService webSocketNotificationService; diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/dto/ChatRequest.java b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/ChatRequest.java index 91ac0cc2..7fe4b1bf 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/dto/ChatRequest.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/ChatRequest.java @@ -32,10 +32,11 @@ public class ChatRequest { public void setPreferredModel(String preferredModel) { this.preferredModel = preferredModel; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } + // Explicit getter for compatibility + public List getUploadedFiles() { return uploadedFiles; } private String conversationId; // Optional - will create new if not provided - @NotNull(message = "Patient ID is required") private Long patientId; @NotNull(message = "User ID is required") diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/dto/ChatResponse.java b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/ChatResponse.java index 03a010e9..369609ab 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/dto/ChatResponse.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/ChatResponse.java @@ -41,4 +41,6 @@ public class ChatResponse { private Boolean success = true; private String errorMessage; private String errorCode; + // Explicit getter for compatibility + public Boolean getSuccess() { return success; } } diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/dto/FamilyMemberRegistration.java b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/FamilyMemberRegistration.java index b63f60c5..a3d1a1b4 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/dto/FamilyMemberRegistration.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/FamilyMemberRegistration.java @@ -5,7 +5,7 @@ public record FamilyMemberRegistration( String lastName, String email, String phone, - AddressDto address, - String relationship, + AddressDto address, // optional + String relationship, // optional Long patientUserId ) {} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/dto/PatientAIConfigDTO.java b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/PatientAIConfigDTO.java index 67f98cf4..e69de29b 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/dto/PatientAIConfigDTO.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/PatientAIConfigDTO.java @@ -1,58 +0,0 @@ -package com.careconnect.dto; - -import com.careconnect.model.PatientAIConfig.AIProvider; -import jakarta.validation.constraints.*; -import lombok.*; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class PatientAIConfigDTO { - - private Long id; - - @NotNull(message = "Patient ID is required") - private Long patientId; - - @NotNull(message = "AI provider is required") - private AIProvider aiProvider; - - @Builder.Default - private String openaiModel = "gpt-4"; - @Builder.Default - private String deepseekModel = "deepseek-chat"; - - @Min(value = 100, message = "Max tokens must be at least 100") - @Max(value = 8000, message = "Max tokens cannot exceed 8000") - @Builder.Default - private Integer maxTokens = 2000; - - @DecimalMin(value = "0.0", message = "Temperature must be between 0.0 and 2.0") - @DecimalMax(value = "2.0", message = "Temperature must be between 0.0 and 2.0") - @Builder.Default - private Double temperature = 0.7; - - @Min(value = 5, message = "Conversation history limit must be at least 5") - @Max(value = 100, message = "Conversation history limit cannot exceed 100") - @Builder.Default - private Integer conversationHistoryLimit = 20; - - // Default context inclusion preferences - @Builder.Default - private Boolean includeVitalsByDefault = true; - @Builder.Default - private Boolean includeMedicationsByDefault = true; - @Builder.Default - private Boolean includeNotesByDefault = true; - @Builder.Default - private Boolean includeMoodPainLogsByDefault = true; - @Builder.Default - private Boolean includeAllergiesByDefault = true; - - @Builder.Default - private Boolean isActive = true; - - private String systemPrompt; -} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/dto/UserAIConfigDTO.java b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/UserAIConfigDTO.java new file mode 100644 index 00000000..d873911d --- /dev/null +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/UserAIConfigDTO.java @@ -0,0 +1,66 @@ +package com.careconnect.dto; + +import com.careconnect.model.UserAIConfig.AIProvider; +import jakarta.validation.constraints.*; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserAIConfigDTO { + private Long id; + private Long patientId; + @NotNull(message = "User ID is required") + private Long userId; + @NotNull(message = "AI provider is required") + private AIProvider aiProvider; + @Builder.Default + private String openaiModel = "gpt-4"; + @Builder.Default + private String deepseekModel = "deepseek-chat"; + @Min(value = 100, message = "Max tokens must be at least 100") + @Max(value = 8000, message = "Max tokens cannot exceed 8000") + @Builder.Default + private Integer maxTokens = 2000; + @DecimalMin(value = "0.0", message = "Temperature must be between 0.0 and 2.0") + @DecimalMax(value = "2.0", message = "Temperature must be between 0.0 and 2.0") + @Builder.Default + private Double temperature = 0.7; + @Min(value = 5, message = "Conversation history limit must be at least 5") + @Max(value = 100, message = "Conversation history limit cannot exceed 100") + @Builder.Default + private Integer conversationHistoryLimit = 20; + // Default context inclusion preferences + @Builder.Default + private Boolean includeVitalsByDefault = true; + @Builder.Default + private Boolean includeMedicationsByDefault = true; + @Builder.Default + private Boolean includeNotesByDefault = true; + @Builder.Default + private Boolean includeMoodPainLogsByDefault = true; + @Builder.Default + private Boolean includeAllergiesByDefault = true; + @Builder.Default + private Boolean isActive = true; + private String systemPrompt; + // Explicit getters for compatibility + public Long getId() { return id; } + public Long getUserId() { return userId; } + public Long getPatientId() { return patientId; } + public AIProvider getAiProvider() { return aiProvider; } + public String getOpenaiModel() { return openaiModel; } + public String getDeepseekModel() { return deepseekModel; } + public Integer getMaxTokens() { return maxTokens; } + public Double getTemperature() { return temperature; } + public Integer getConversationHistoryLimit() { return conversationHistoryLimit; } + public Boolean getIncludeVitalsByDefault() { return includeVitalsByDefault; } + public Boolean getIncludeMedicationsByDefault() { return includeMedicationsByDefault; } + public Boolean getIncludeNotesByDefault() { return includeNotesByDefault; } + public Boolean getIncludeMoodPainLogsByDefault() { return includeMoodPainLogsByDefault; } + public Boolean getIncludeAllergiesByDefault() { return includeAllergiesByDefault; } + public Boolean getIsActive() { return isActive; } + public String getSystemPrompt() { return systemPrompt; } +} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/model/Achievement.java b/careconnect2025/backend/core/src/main/java/com/careconnect/model/Achievement.java index 90857869..ce602740 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/model/Achievement.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/model/Achievement.java @@ -26,4 +26,7 @@ public class Achievement { public String getTitle() { return title; } public String getDescription() { return description; } public String getIcon() { return icon; } + public void setTitle(String title) { this.title = title; } + public void setDescription(String description) { this.description = description; } + public void setIcon(String icon) { this.icon = icon; } } diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/model/Allergy.java b/careconnect2025/backend/core/src/main/java/com/careconnect/model/Allergy.java index 79f8f0cf..6b43378d 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/model/Allergy.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/model/Allergy.java @@ -11,6 +11,8 @@ @AllArgsConstructor @Builder public class Allergy { + public Long getId() { return id; } + public Patient getPatient() { return patient; } @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -22,33 +24,56 @@ public class Allergy { @Column(name = "allergen", nullable = false) private String allergen; // e.g., "Peanuts", "Penicillin", "Latex" - + @Column(name = "allergy_type") @Enumerated(EnumType.STRING) private AllergyType allergyType; - + @Column(name = "severity") @Enumerated(EnumType.STRING) private AllergySeverity severity; - + @Column(name = "reaction", columnDefinition = "TEXT") private String reaction; // Description of reaction (e.g., "Hives, difficulty breathing") - + @Column(name = "notes", columnDefinition = "TEXT") private String notes; // Additional notes from healthcare provider - + @Column(name = "diagnosed_date") private String diagnosedDate; // When allergy was first diagnosed - + @Column(name = "is_active", nullable = false) @Builder.Default private Boolean isActive = true; // Whether allergy is still active - + @Column(name = "created_at", nullable = false, updatable = false) private Instant createdAt; - + @Column(name = "updated_at") private Instant updatedAt; + + // Setters for updatable fields (for use in service/controller) + public void setAllergen(String allergen) { + this.allergen = allergen; + } + public void setAllergyType(AllergyType allergyType) { + this.allergyType = allergyType; + } + public void setSeverity(AllergySeverity severity) { + this.severity = severity; + } + public void setReaction(String reaction) { + this.reaction = reaction; + } + public void setNotes(String notes) { + this.notes = notes; + } + public void setDiagnosedDate(String diagnosedDate) { + this.diagnosedDate = diagnosedDate; + } + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } @PrePersist protected void onCreate() { diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/model/ChatConversation.java b/careconnect2025/backend/core/src/main/java/com/careconnect/model/ChatConversation.java index 728deb0a..3619ce39 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/model/ChatConversation.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/model/ChatConversation.java @@ -4,6 +4,7 @@ import lombok.*; import java.time.LocalDateTime; import java.util.List; +import com.careconnect.model.UserAIConfig; @Getter @Setter @@ -31,8 +32,8 @@ public class ChatConversation { public void setChatType(ChatType chatType) { this.chatType = chatType; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } - public PatientAIConfig.AIProvider getAiProviderUsed() { return aiProviderUsed; } - public void setAiProviderUsed(PatientAIConfig.AIProvider aiProviderUsed) { this.aiProviderUsed = aiProviderUsed; } + public UserAIConfig.AIProvider getAiProviderUsed() { return aiProviderUsed; } + public void setAiProviderUsed(UserAIConfig.AIProvider aiProviderUsed) { this.aiProviderUsed = aiProviderUsed; } public String getAiModelUsed() { return aiModelUsed; } public void setAiModelUsed(String aiModelUsed) { this.aiModelUsed = aiModelUsed; } public Integer getTotalTokensUsed() { return totalTokensUsed; } @@ -59,7 +60,7 @@ public class ChatConversation { @Enumerated(EnumType.STRING) @Column(name = "ai_provider_used") - private PatientAIConfig.AIProvider aiProviderUsed; + private UserAIConfig.AIProvider aiProviderUsed; @Column(name = "ai_model_used") private String aiModelUsed; diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/model/Patient.java b/careconnect2025/backend/core/src/main/java/com/careconnect/model/Patient.java index 7f168a7d..b05b16e8 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/model/Patient.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/model/Patient.java @@ -43,4 +43,7 @@ public class Patient { private List allergies = new ArrayList<>(); private String relationship; // e.g. "daughter", "client", etc. + + // Explicit getter for compatibility if Lombok is not processed + public User getUser() { return user; } } \ No newline at end of file diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/model/PatientAIConfig.java b/careconnect2025/backend/core/src/main/java/com/careconnect/model/UserAIConfig.java similarity index 63% rename from careconnect2025/backend/core/src/main/java/com/careconnect/model/PatientAIConfig.java rename to careconnect2025/backend/core/src/main/java/com/careconnect/model/UserAIConfig.java index 58b3d4be..a48a1216 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/model/PatientAIConfig.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/model/UserAIConfig.java @@ -10,18 +10,77 @@ @AllArgsConstructor @Builder @Entity -@Table(name = "patient_ai_config") -public class PatientAIConfig { +@Table(name = "user_ai_config") +public class UserAIConfig { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "patient_id", nullable = false, unique = true) + @jakarta.annotation.Nullable + @Column(name = "patient_id", nullable = true) private Long patientId; + + @Column(name = "user_id", nullable = false) + private Long userId; + + public enum AIProvider { + DEFAULT, + OPENAI, + DEEPSEEK; + + public static AIProvider resolve(String value) { + if (value == null) return OPENAI; + if (value.equalsIgnoreCase("DEFAULT")) return OPENAI; + for (AIProvider p : values()) { + if (p.name().equalsIgnoreCase(value)) return p; + } + return OPENAI; + } + } + + @Enumerated(EnumType.STRING) + @Column(name = "preferred_ai_provider", nullable = false) + private AIProvider preferredAiProvider; + + @Column(name = "openai_model") + private String openaiModel; + + @Column(name = "deepseek_model") + private String deepseekModel; + + @Column(name = "max_tokens") + private Integer maxTokens; + + @Column(name = "temperature") + private Double temperature; + + @Column(name = "conversation_history_limit") + private Integer conversationHistoryLimit; + + @Column(name = "system_prompt") + private String systemPrompt; + + @Column(name = "include_vitals_by_default") + private Boolean includeVitalsByDefault; + + @Column(name = "include_medications_by_default") + private Boolean includeMedicationsByDefault; + + @Column(name = "include_notes_by_default") + private Boolean includeNotesByDefault; + + @Column(name = "include_mood_pain_by_default") + private Boolean includeMoodPainByDefault; + + @Column(name = "include_allergies_by_default") + private Boolean includeAllergiesByDefault; + // Explicit getters and setters for compatibility public Long getPatientId() { return patientId; } public void setPatientId(Long patientId) { this.patientId = patientId; } + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } public AIProvider getPreferredAiProvider() { return preferredAiProvider; } public void setPreferredAiProvider(AIProvider preferredAiProvider) { this.preferredAiProvider = preferredAiProvider; } public String getOpenaiModel() { return openaiModel; } @@ -32,101 +91,26 @@ public class PatientAIConfig { public void setMaxTokens(Integer maxTokens) { this.maxTokens = maxTokens; } public Double getTemperature() { return temperature; } public void setTemperature(Double temperature) { this.temperature = temperature; } + public Integer getConversationHistoryLimit() { return conversationHistoryLimit; } + public void setConversationHistoryLimit(Integer conversationHistoryLimit) { this.conversationHistoryLimit = conversationHistoryLimit; } + public String getSystemPrompt() { return systemPrompt; } + public void setSystemPrompt(String systemPrompt) { this.systemPrompt = systemPrompt; } public Boolean getIncludeVitalsByDefault() { return includeVitalsByDefault; } public void setIncludeVitalsByDefault(Boolean includeVitalsByDefault) { this.includeVitalsByDefault = includeVitalsByDefault; } public Boolean getIncludeMedicationsByDefault() { return includeMedicationsByDefault; } public void setIncludeMedicationsByDefault(Boolean includeMedicationsByDefault) { this.includeMedicationsByDefault = includeMedicationsByDefault; } - public Boolean getIncludeMoodPainByDefault() { return includeMoodPainByDefault; } - public void setIncludeMoodPainByDefault(Boolean includeMoodPainByDefault) { this.includeMoodPainByDefault = includeMoodPainByDefault; } public Boolean getIncludeNotesByDefault() { return includeNotesByDefault; } public void setIncludeNotesByDefault(Boolean includeNotesByDefault) { this.includeNotesByDefault = includeNotesByDefault; } + public Boolean getIncludeMoodPainByDefault() { return includeMoodPainByDefault; } + public void setIncludeMoodPainByDefault(Boolean includeMoodPainByDefault) { this.includeMoodPainByDefault = includeMoodPainByDefault; } public Boolean getIncludeAllergiesByDefault() { return includeAllergiesByDefault; } public void setIncludeAllergiesByDefault(Boolean includeAllergiesByDefault) { this.includeAllergiesByDefault = includeAllergiesByDefault; } - public Integer getConversationHistoryLimit() { return conversationHistoryLimit; } - public void setConversationHistoryLimit(Integer conversationHistoryLimit) { this.conversationHistoryLimit = conversationHistoryLimit; } - - @Enumerated(EnumType.STRING) - @Column(name = "preferred_ai_provider", nullable = false) - @Builder.Default - private AIProvider preferredAiProvider = AIProvider.OPENAI; - - @Column(name = "openai_model") - @Builder.Default - private String openaiModel = "gpt-4"; - - @Column(name = "deepseek_model") - @Builder.Default - private String deepseekModel = "deepseek-chat"; - - @Column(name = "max_tokens") - @Builder.Default - private Integer maxTokens = 2000; - - @Column(name = "temperature") - @Builder.Default - private Double temperature = 0.7; - - @Column(name = "include_vitals_by_default") - @Builder.Default - private Boolean includeVitalsByDefault = true; - - @Column(name = "include_medications_by_default") - @Builder.Default - private Boolean includeMedicationsByDefault = true; - - @Column(name = "include_mood_pain_by_default") - @Builder.Default - private Boolean includeMoodPainByDefault = true; - - @Column(name = "include_notes_by_default") - @Builder.Default - private Boolean includeNotesByDefault = true; - - @Column(name = "include_allergies_by_default") - @Builder.Default - private Boolean includeAllergiesByDefault = true; - - @Column(name = "system_prompt", columnDefinition = "TEXT") - private String systemPrompt; - - @Column(name = "conversation_history_limit") - @Builder.Default - private Integer conversationHistoryLimit = 20; + // Add isActive field for builder compatibility @Column(name = "is_active") - @Builder.Default - private Boolean isActive = true; - - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - @PrePersist - protected void onCreate() { - LocalDateTime now = LocalDateTime.now(); - this.createdAt = now; - this.updatedAt = now; - } - - @PreUpdate - protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); - } - - public enum AIProvider { - OPENAI("OpenAI"), - DEEPSEEK("DeepSeek"); - - private final String displayName; - - AIProvider(String displayName) { - this.displayName = displayName; - } - - public String getDisplayName() { - return displayName; - } - } + private Boolean isActive; + + public Boolean getIsActive() { return isActive; } + public void setIsActive(Boolean isActive) { this.isActive = isActive; } + // ...rest of the code... } diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/repository/PatientAIConfigRepository.java b/careconnect2025/backend/core/src/main/java/com/careconnect/repository/PatientAIConfigRepository.java index 144c0cce..e69de29b 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/repository/PatientAIConfigRepository.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/repository/PatientAIConfigRepository.java @@ -1,20 +0,0 @@ -package com.careconnect.repository; - -import com.careconnect.model.PatientAIConfig; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface PatientAIConfigRepository extends JpaRepository { - - Optional findByPatientIdAndIsActiveTrue(Long patientId); - - List findByPatientId(Long patientId); - - List findByIsActiveTrue(); - - boolean existsByPatientIdAndIsActiveTrue(Long patientId); -} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/repository/UserAIConfigRepository.java b/careconnect2025/backend/core/src/main/java/com/careconnect/repository/UserAIConfigRepository.java new file mode 100644 index 00000000..648b6177 --- /dev/null +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/repository/UserAIConfigRepository.java @@ -0,0 +1,18 @@ +package com.careconnect.repository; + +import com.careconnect.model.UserAIConfig; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface UserAIConfigRepository extends JpaRepository { + Optional findByUserIdAndIsActiveTrue(Long userId); + Optional findByUserIdAndPatientIdAndIsActiveTrue(Long userId, Long patientId); + List findByUserId(Long userId); + List findByUserIdAndPatientId(Long userId, Long patientId); + List findByIsActiveTrue(); + boolean existsByUserIdAndIsActiveTrue(Long userId); +} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/security/JwtAuthenticationFilter.java b/careconnect2025/backend/core/src/main/java/com/careconnect/security/JwtAuthenticationFilter.java index e6a27521..32886b35 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/security/JwtAuthenticationFilter.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/security/JwtAuthenticationFilter.java @@ -23,6 +23,12 @@ @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { + // No-arg constructor for Spring or frameworks that require it + public JwtAuthenticationFilter() { + this.jwt = null; + this.uds = null; + } + private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class); private static final String COOKIE_NAME = "AUTH"; diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/CaregiverService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/CaregiverService.java index 713d29eb..3192a7f2 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/CaregiverService.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/CaregiverService.java @@ -81,8 +81,6 @@ public class CaregiverService { @Autowired private SubscriptionRepository subscriptionRepository; - @Autowired(required = false) - private FirebaseNotificationService notificationService; // 1. List patients under a caregiver, with optional filtering (ACTIVE links only) // public List getPatientsByCaregiver(Long caregiverId, String email, String name) { // // Get caregiver user // Caregiver caregiver = getCaregiverById(caregiverId); @@ -249,36 +247,7 @@ public Patient registerPatient(PatientRegistration reg) { password ); - // Send Firebase notification to patient about registration - try { - String caregiverName = reg.getCaregiverId() != null ? - caregiverRepository.findById(reg.getCaregiverId()) - .map(c -> c.getFirstName() + " " + c.getLastName()) - .orElse("Your caregiver") : "CareConnect"; - - // Send notification only if Firebase is enabled - if (notificationService != null) { - notificationService.sendNotificationToUser( - savedUser.getId(), - "🎉 Welcome to CareConnect!", - String.format("You've been registered by %s. Please check your email to set up your password.", caregiverName), - "PATIENT_REGISTRATION", - Map.of( - "type", "PATIENT_REGISTRATION", - "caregiverName", caregiverName, - "registeredAt", Instant.now().toString(), - "patientId", savedPatient.getId().toString() - ) - ); - - log.info("Patient registration notification sent to user ID: {}", savedUser.getId()); - } else { - log.info("Firebase notifications disabled - skipping notification for user ID: {}", savedUser.getId()); - } - } catch (Exception e) { - log.warn("Failed to send patient registration notification: {}", e.getMessage()); - // Don't fail the registration if notification fails - } + // Firebase notification logic removed return savedPatient; } catch (Exception e) { diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/ConnectionRequestService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/ConnectionRequestService.java index 2cb91c33..41739858 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/ConnectionRequestService.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/ConnectionRequestService.java @@ -28,7 +28,7 @@ public class ConnectionRequestService { private final EmailService emailService; @Autowired(required = false) - private FirebaseNotificationService notificationService; + private NotificationService notificationService; @Value("${frontend.base-url:http://localhost:3000}") private String frontendBaseUrl; diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/DefaultAIChatService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/DefaultAIChatService.java index e501fe08..b52ed92e 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/DefaultAIChatService.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/DefaultAIChatService.java @@ -5,6 +5,7 @@ import com.careconnect.dto.ChatConversationSummary; import com.careconnect.dto.ChatMessageSummary; import com.careconnect.model.*; +import com.careconnect.model.UserAIConfig; import lombok.Builder; import com.careconnect.repository.*; import lombok.RequiredArgsConstructor; @@ -29,16 +30,17 @@ public class DefaultAIChatService implements AIChatService { // Use a message window memory for demo (20 messages) private final ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(20); // Helper: Get or create patient AI config - private PatientAIConfig getOrCreatePatientAIConfig(Long patientId) { - return patientAIConfigRepository.findByPatientIdAndIsActiveTrue(patientId) - .orElseGet(() -> createDefaultAIConfig(patientId)); + private UserAIConfig getOrCreateUserAIConfig(Long userId, Long patientId) { + return userAIConfigRepository.findByUserIdAndPatientIdAndIsActiveTrue(userId, patientId) + .orElseGet(() -> createDefaultUserAIConfig(userId, patientId)); } // Helper: Create default AI config - private PatientAIConfig createDefaultAIConfig(Long patientId) { - PatientAIConfig config = PatientAIConfig.builder() + private UserAIConfig createDefaultUserAIConfig(Long userId, Long patientId) { + UserAIConfig config = UserAIConfig.builder() + .userId(userId) .patientId(patientId) - .preferredAiProvider(PatientAIConfig.AIProvider.OPENAI) + .preferredAiProvider(UserAIConfig.AIProvider.OPENAI) .openaiModel("gpt-3.5-turbo") .deepseekModel("deepseek-chat") .maxTokens(1000) @@ -52,11 +54,11 @@ private PatientAIConfig createDefaultAIConfig(Long patientId) { .isActive(true) .systemPrompt("You are a healthcare AI assistant. Carefully analyze and summarize the provided patient data (vitals, labs, medications, allergies, and notes). Clearly state what the data shows about the patient's current health. Do not make up information. If the answer is not in the data, say you do not know. Always recommend consulting a healthcare professional for medical decisions.") .build(); - return patientAIConfigRepository.save(config); + return userAIConfigRepository.save(config); } // Helper: Get or create conversation - private ChatConversation getOrCreateConversation(ChatRequest request, PatientAIConfig aiConfig) { + private ChatConversation getOrCreateConversation(ChatRequest request, UserAIConfig aiConfig) { if (request.getConversationId() != null) { Optional existing = chatConversationRepository.findByConversationIdAndIsActiveTrue(request.getConversationId()); if (existing.isPresent()) { @@ -110,8 +112,8 @@ private List prepareMessagesForAI(ChatConversation conversation, String messages.add(createMessage("system", medicalContext)); } Integer historyLimit = 20; - if (conversation.getPatientId() != null) { - PatientAIConfig config = getOrCreatePatientAIConfig(conversation.getPatientId()); + if (conversation.getUserId() != null && conversation.getPatientId() != null) { + UserAIConfig config = getOrCreateUserAIConfig(conversation.getUserId(), conversation.getPatientId()); historyLimit = (config != null && config.getConversationHistoryLimit() != null) ? config.getConversationHistoryLimit() : 20; } List recentMessages = chatMessageRepository @@ -135,8 +137,8 @@ private List prepareChatMessagesForAI( messages.add(dev.langchain4j.data.message.SystemMessage.from(medicalContext)); } Integer historyLimit = 20; - if (conversation.getPatientId() != null) { - PatientAIConfig config = getOrCreatePatientAIConfig(conversation.getPatientId()); + if (conversation.getUserId() != null && conversation.getPatientId() != null) { + UserAIConfig config = getOrCreateUserAIConfig(conversation.getUserId(), conversation.getPatientId()); historyLimit = (config != null && config.getConversationHistoryLimit() != null) ? config.getConversationHistoryLimit() : 20; } List recentMessages = chatMessageRepository @@ -158,11 +160,11 @@ private Object createMessage(String role, String content) { } // Helper: Determine model - private String determineModel(ChatRequest request, PatientAIConfig aiConfig) { + private String determineModel(ChatRequest request, UserAIConfig aiConfig) { if (request.getPreferredModel() != null) { return request.getPreferredModel(); } - return aiConfig.getPreferredAiProvider() == PatientAIConfig.AIProvider.OPENAI ? + return aiConfig.getPreferredAiProvider() == UserAIConfig.AIProvider.OPENAI ? aiConfig.getOpenaiModel() : aiConfig.getDeepseekModel(); } @@ -323,7 +325,7 @@ private ChatMessageSummary convertToMessageSummary(ChatMessage message) { // Helper classes private static class ChatProcessingContext { final Patient patient; - final PatientAIConfig aiConfig; + final UserAIConfig aiConfig; final ChatConversation conversation; final List messages; final String model; @@ -332,7 +334,7 @@ private static class ChatProcessingContext { final String medicalContext; final long startTime; - ChatProcessingContext(Patient patient, PatientAIConfig aiConfig, ChatConversation conversation, + ChatProcessingContext(Patient patient, UserAIConfig aiConfig, ChatConversation conversation, List messages, String model, Double temperature, Integer max_tokens, String medicalContext, long startTime) { this.patient = patient; @@ -363,7 +365,7 @@ private static class ChatProcessingResult { this.error = error; } } - private final PatientAIConfigRepository patientAIConfigRepository; + private final UserAIConfigRepository userAIConfigRepository; private final ChatConversationRepository chatConversationRepository; private final ChatMessageRepository chatMessageRepository; private final PatientRepository patientRepository; @@ -385,8 +387,8 @@ public ChatResponse processChat(ChatRequest request) { Patient patient = patientRepository.findById(request.getPatientId()) .orElseThrow(() -> new IllegalArgumentException("Patient not found")); - // Get or create patient AI configuration - PatientAIConfig aiConfig = getOrCreatePatientAIConfig(request.getPatientId()); + // Get or create user AI configuration + UserAIConfig aiConfig = getOrCreateUserAIConfig(request.getUserId(), request.getPatientId()); // Get or create conversation ChatConversation conversation = getOrCreateConversation(request, aiConfig); diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/FileManagementService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/FileManagementService.java index 23564f55..8dc8ccd6 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/FileManagementService.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/FileManagementService.java @@ -48,6 +48,7 @@ public FileUploadResponse uploadFile(MultipartFile file, Long userId, String use // Validate file validateFile(file); + // No access control or ownership checks; all uploads are allowed for now // Determine storage service StorageService storageService = useS3ForNewFiles ? s3StorageService : databaseStorageService; @@ -194,22 +195,18 @@ public List listFilesForCaregiverPatient(Long patientId, String cat public void deleteFile(Long fileId, Long userId) { UserFile userFile = userFileRepository.findById(fileId) .orElseThrow(() -> new RuntimeException("File not found: " + fileId)); - - // Check ownership - if (!userFile.getOwnerId().equals(userId)) { - throw new RuntimeException("Not authorized to delete this file"); - } - + + // Soft delete userFile.setIsActive(false); userFileRepository.save(userFile); - + // If it's a profile image, clear the user's profile image URL if (userFile.getFileCategory() == UserFile.FileCategory.PROFILE_IMAGE) { - clearUserProfileImage(userId); + clearUserProfileImage(userFile.getOwnerId()); } - - log.info("File deleted: ID={}, owner={}", fileId, userId); + + log.info("File deleted: ID={}, owner={}", fileId, userFile.getOwnerId()); } /** diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/FirebaseNotificationService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/FirebaseNotificationService.java deleted file mode 100644 index 4c5a563f..00000000 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/FirebaseNotificationService.java +++ /dev/null @@ -1,415 +0,0 @@ -package com.careconnect.service; - -import com.careconnect.dto.FirebaseNotificationRequest; -import com.careconnect.dto.NotificationResponse; -import com.careconnect.model.DeviceToken; -import com.careconnect.model.User; -import com.careconnect.model.CaregiverPatientLink; -import com.careconnect.model.FamilyMemberLink; -import com.careconnect.repository.DeviceTokenRepository; -import com.careconnect.repository.UserRepository; -import com.careconnect.repository.CaregiverPatientLinkRepository; -import com.careconnect.repository.FamilyMemberLinkRepository; -import com.google.firebase.messaging.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; - -@Service -@ConditionalOnProperty(name = "firebase.enabled", havingValue = "true", matchIfMissing = true) -public class FirebaseNotificationService { - - private static final Logger logger = LoggerFactory.getLogger(FirebaseNotificationService.class); - - @Autowired(required = false) - private FirebaseMessaging firebaseMessaging; - - @Autowired - private DeviceTokenRepository deviceTokenRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private CaregiverPatientLinkRepository caregiverPatientLinkRepository; - - @Autowired - private FamilyMemberLinkRepository familyMemberLinkRepository; - - /** - * Send notification to a specific device token - */ - public NotificationResponse sendNotification(FirebaseNotificationRequest request) { - if (firebaseMessaging == null) { - logger.warn("Firebase not initialized, cannot send notification"); - return NotificationResponse.failure("Firebase not available"); - } - - try { - Message message = buildMessage(request); - String response = firebaseMessaging.send(message); - - logger.info("Successfully sent message: {}", response); - return NotificationResponse.success(response); - - } catch (FirebaseMessagingException e) { - logger.error("Failed to send FCM message: {}", e.getMessage(), e); - handleFirebaseException(e, request.getTargetToken()); - return NotificationResponse.failure(e.getMessage()); - } catch (Exception e) { - logger.error("Unexpected error sending notification: {}", e.getMessage(), e); - return NotificationResponse.failure("Unexpected error: " + e.getMessage()); - } - } - - /** - * Send notification to multiple device tokens - */ - public List sendBulkNotifications(List requests) { - if (firebaseMessaging == null) { - logger.warn("Firebase not initialized, cannot send bulk notifications"); - return requests.stream() - .map(req -> NotificationResponse.failure("Firebase not available")) - .collect(Collectors.toList()); - } - - List messages = requests.stream() - .map(this::buildMessage) - .collect(Collectors.toList()); - - try { - BatchResponse response = firebaseMessaging.sendEach(messages); - logger.info("Successfully sent {} messages out of {}", - response.getSuccessCount(), messages.size()); - - return processBatchResponse(response, requests); - - } catch (FirebaseMessagingException e) { - logger.error("Failed to send bulk FCM messages: {}", e.getMessage(), e); - return requests.stream() - .map(req -> NotificationResponse.failure(e.getMessage())) - .collect(Collectors.toList()); - } - } - - /** - * Send notification to all devices of a specific user - */ - public List sendNotificationToUser(Long userId, String title, String body, - String notificationType, Map data) { - List userTokens = deviceTokenRepository.findByUserIdAndIsActiveTrue(userId); - - if (userTokens.isEmpty()) { - logger.warn("No active device tokens found for user: {}", userId); - return List.of(NotificationResponse.failure("No active device tokens found")); - } - - List requests = userTokens.stream() - .map(token -> FirebaseNotificationRequest.builder() - .title(title) - .body(body) - .targetToken(token.getFcmToken()) - .targetUserId(userId) - .notificationType(notificationType) - .data(data != null ? data : new HashMap<>()) - .build()) - .collect(Collectors.toList()); - - return sendBulkNotifications(requests); - } - - /** - * Send notification to multiple users (e.g., all caregivers of a patient) - */ - public List sendNotificationToUsers(List userIds, String title, String body, - String notificationType, Map data) { - List tokens = deviceTokenRepository.findActiveTokensByUserIds(userIds); - - if (tokens.isEmpty()) { - logger.warn("No active device tokens found for users: {}", userIds); - return List.of(NotificationResponse.failure("No active device tokens found")); - } - - List requests = tokens.stream() - .map(token -> FirebaseNotificationRequest.builder() - .title(title) - .body(body) - .targetToken(token.getFcmToken()) - .targetUserId(token.getUser().getId()) - .notificationType(notificationType) - .data(data != null ? data : new HashMap<>()) - .build()) - .collect(Collectors.toList()); - - return sendBulkNotifications(requests); - } - - /** - * Send vital alert to patient's caregivers - */ - public CompletableFuture> sendVitalAlert(Long patientId, String vitalType, - String vitalValue, String alertLevel) { - return CompletableFuture.supplyAsync(() -> { - try { - // Get patient's caregivers - List caregiverIds = getCaregiverIds(patientId); - - if (caregiverIds.isEmpty()) { - logger.warn("No caregivers found for patient: {}", patientId); - return List.of(NotificationResponse.failure("No caregivers found")); - } - - String title = "⚠️ Vital Alert"; - String body = String.format("Patient's %s is %s (%s level)", vitalType, vitalValue, alertLevel); - - Map data = Map.of( - "type", "VITAL_ALERT", - "patientId", patientId.toString(), - "vitalType", vitalType, - "vitalValue", vitalValue, - "alertLevel", alertLevel, - "timestamp", Instant.now().toString() - ); - - return sendNotificationToUsers(caregiverIds, title, body, "VITAL_ALERT", data); - - } catch (Exception e) { - logger.error("Error sending vital alert: {}", e.getMessage(), e); - return List.of(NotificationResponse.failure("Error: " + e.getMessage())); - } - }); - } - - /** - * Send medication reminder - */ - public CompletableFuture> sendMedicationReminder(Long patientId, String medicationName, - String dosage, String scheduledTime) { - return CompletableFuture.supplyAsync(() -> { - try { - String title = "💊 Medication Reminder"; - String body = String.format("Time to take %s (%s) at %s", medicationName, dosage, scheduledTime); - - Map data = Map.of( - "type", "MEDICATION_REMINDER", - "patientId", patientId.toString(), - "medicationName", medicationName, - "dosage", dosage, - "scheduledTime", scheduledTime, - "timestamp", Instant.now().toString() - ); - - return sendNotificationToUser(patientId, title, body, "MEDICATION_REMINDER", data); - - } catch (Exception e) { - logger.error("Error sending medication reminder: {}", e.getMessage(), e); - return List.of(NotificationResponse.failure("Error: " + e.getMessage())); - } - }); - } - - /** - * Send emergency alert - */ - public CompletableFuture> sendEmergencyAlert(Long patientId, String emergencyType, - String location) { - return CompletableFuture.supplyAsync(() -> { - try { - // Get all caregivers and family members - List caregiverIds = getCaregiverIds(patientId); - List familyMemberIds = getFamilyMemberIds(patientId); - - List allRecipients = new ArrayList<>(); - allRecipients.addAll(caregiverIds); - allRecipients.addAll(familyMemberIds); - - if (allRecipients.isEmpty()) { - logger.warn("No recipients found for emergency alert for patient: {}", patientId); - return List.of(NotificationResponse.failure("No recipients found")); - } - - String title = "🚨 EMERGENCY ALERT"; - String body = String.format("Emergency: %s at %s", emergencyType, location); - - Map data = Map.of( - "type", "EMERGENCY_ALERT", - "patientId", patientId.toString(), - "emergencyType", emergencyType, - "location", location, - "priority", "HIGH", - "timestamp", Instant.now().toString() - ); - - return sendNotificationToUsers(allRecipients, title, body, "EMERGENCY_ALERT", data); - - } catch (Exception e) { - logger.error("Error sending emergency alert: {}", e.getMessage(), e); - return List.of(NotificationResponse.failure("Error: " + e.getMessage())); - } - }); - } - - /** - * Register or update device token for a user - */ - public void registerDeviceToken(Long userId, String fcmToken, String deviceId, DeviceToken.DeviceType deviceType) { - try { - User user = userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId)); - - // Deactivate existing token for this device - deviceTokenRepository.deactivateByUserAndDeviceId(user, deviceId); - - // Create new token - DeviceToken deviceToken = DeviceToken.builder() - .user(user) - .fcmToken(fcmToken) - .deviceId(deviceId) - .deviceType(deviceType) - .isActive(true) - .createdAt(Instant.now()) - .lastUsedAt(Instant.now()) - .build(); - - deviceTokenRepository.save(deviceToken); - logger.info("Registered device token for user: {} device: {}", userId, deviceId); - - } catch (Exception e) { - logger.error("Error registering device token: {}", e.getMessage(), e); - throw new RuntimeException("Failed to register device token", e); - } - } - - /** - * Unregister device token - */ - public void unregisterDeviceToken(String fcmToken) { - try { - deviceTokenRepository.deactivateByFcmToken(fcmToken); - logger.info("Unregistered device token: {}", fcmToken); - } catch (Exception e) { - logger.error("Error unregistering device token: {}", e.getMessage(), e); - } - } - - // Private helper methods - - private Message buildMessage(FirebaseNotificationRequest request) { - Notification.Builder notificationBuilder = Notification.builder() - .setTitle(request.getTitle()) - .setBody(request.getBody()); - - if (request.getImageUrl() != null) { - notificationBuilder.setImage(request.getImageUrl()); - } - - Message.Builder messageBuilder = Message.builder() - .setToken(request.getTargetToken()) - .setNotification(notificationBuilder.build()); - - // Add custom data - if (request.getData() != null && !request.getData().isEmpty()) { - messageBuilder.putAllData(request.getData()); - } - - // Set Android-specific configuration - AndroidConfig androidConfig = AndroidConfig.builder() - .setTtl(3600 * 1000) // 1 hour - .setPriority(AndroidConfig.Priority.HIGH) - .setNotification(AndroidNotification.builder() - .setIcon("ic_notification") - .setColor("#0066CC") - .setSound("default") - .build()) - .build(); - messageBuilder.setAndroidConfig(androidConfig); - - // Set iOS-specific configuration - ApnsConfig apnsConfig = ApnsConfig.builder() - .setAps(Aps.builder() - .setAlert(ApsAlert.builder() - .setTitle(request.getTitle()) - .setBody(request.getBody()) - .build()) - .setBadge(1) - .setSound("default") - .build()) - .build(); - messageBuilder.setApnsConfig(apnsConfig); - - return messageBuilder.build(); - } - - private List processBatchResponse(BatchResponse response, - List requests) { - List results = new ArrayList<>(); - List responses = response.getResponses(); - - for (int i = 0; i < responses.size(); i++) { - SendResponse sendResponse = responses.get(i); - FirebaseNotificationRequest request = requests.get(i); - - if (sendResponse.isSuccessful()) { - results.add(NotificationResponse.success(sendResponse.getMessageId())); - } else { - FirebaseMessagingException exception = sendResponse.getException(); - handleFirebaseException(exception, request.getTargetToken()); - results.add(NotificationResponse.failure(exception.getMessage())); - } - } - - return results; - } - - private void handleFirebaseException(FirebaseMessagingException exception, String token) { - String errorCode = exception.getErrorCode().name(); - - if ("UNREGISTERED".equals(errorCode) || "INVALID_ARGUMENT".equals(errorCode)) { - // Token is invalid, remove it from database - logger.warn("Invalid FCM token detected, deactivating: {}", token); - deviceTokenRepository.deactivateByFcmToken(token); - } - } - - private List getCaregiverIds(Long patientId) { - try { - User patientUser = userRepository.findById(patientId) - .orElseThrow(() -> new IllegalArgumentException("Patient not found")); - - return caregiverPatientLinkRepository - .findByPatientUserAndStatus(patientUser, CaregiverPatientLink.LinkStatus.ACTIVE) - .stream() - .map(link -> link.getCaregiverUser().getId()) - .collect(Collectors.toList()); - } catch (Exception e) { - logger.error("Error getting caregiver IDs for patient {}: {}", patientId, e.getMessage()); - return new ArrayList<>(); - } - } - - private List getFamilyMemberIds(Long patientId) { - try { - User patientUser = userRepository.findById(patientId) - .orElseThrow(() -> new IllegalArgumentException("Patient not found")); - - return familyMemberLinkRepository - .findByPatientUserAndStatus(patientUser, FamilyMemberLink.LinkStatus.ACTIVE) - .stream() - .map(link -> link.getFamilyUser().getId()) - .collect(Collectors.toList()); - } catch (Exception e) { - logger.error("Error getting family member IDs for patient {}: {}", patientId, e.getMessage()); - return new ArrayList<>(); - } - } -} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/LangChainAIChatService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/LangChainAIChatService.java index c7bebf56..b2ba3d3b 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/LangChainAIChatService.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/LangChainAIChatService.java @@ -16,7 +16,7 @@ import java.util.Map; import com.careconnect.model.Patient; -import com.careconnect.model.PatientAIConfig; +import com.careconnect.model.UserAIConfig; import com.careconnect.repository.PatientRepository; import com.careconnect.service.MedicalContextService; import com.careconnect.service.PatientContextRetrievalService; @@ -34,7 +34,13 @@ public class LangChainAIChatService implements AIChatService { private final PatientRepository patientRepository; private final MedicalContextService medicalContextService; private final PatientContextRetrievalService patientContextRetrievalService; - private final PatientAIConfigService patientAIConfigService; + private final UserAIConfigService userAIConfigService; + + // Add userId field for compatibility + private Long userId; + + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } @Autowired public LangChainAIChatService( @@ -43,7 +49,7 @@ public LangChainAIChatService( PatientRepository patientRepository, MedicalContextService medicalContextService, PatientContextRetrievalService patientContextRetrievalService, - PatientAIConfigService patientAIConfigService) { + UserAIConfigService userAIConfigService) { this.modelProvider = modelProvider; this.chatModel = OpenAiChatModel.builder() .apiKey(openAiApiKey) @@ -52,7 +58,7 @@ public LangChainAIChatService( this.patientRepository = patientRepository; this.medicalContextService = medicalContextService; this.patientContextRetrievalService = patientContextRetrievalService; - this.patientAIConfigService = patientAIConfigService; + this.userAIConfigService = userAIConfigService; } private ChatMemory getMemory(Long patientId) { // Keep last 20 messages per patient (in-memory, for demo) @@ -89,6 +95,8 @@ public ChatResponse processChat(ChatRequest request) { generatedConversationId = null; } try { + // Always set userId from request before using it + this.userId = request.getUserId(); // Defensive: Validate request if (request == null) { throw new IllegalArgumentException("ChatRequest cannot be null"); @@ -122,8 +130,56 @@ public ChatResponse processChat(ChatRequest request) { // Use real repository/service to load patient data Patient patient = patientRepository.findById(patientId) .orElseThrow(() -> new IllegalArgumentException("Patient not found for ID: " + patientId)); - var aiConfigDTO = patientAIConfigService.getPatientAIConfig(patientId); - PatientAIConfig aiConfig = patientAIConfigService.convertDTOToEntity(aiConfigDTO); + UserAIConfig aiConfig; + try { + var aiConfigDTO = userAIConfigService.getUserAIConfig(userId, patientId); + aiConfig = userAIConfigService.convertDTOToEntity(aiConfigDTO); + if (aiConfig == null) { + // No config found, create and persist default + aiConfig = UserAIConfig.builder() + .userId(userId) + .patientId(patientId) + .preferredAiProvider(UserAIConfig.AIProvider.OPENAI) + .isActive(true) + .conversationHistoryLimit(20) + .maxTokens(1000) + .temperature(0.7) + .includeVitalsByDefault(false) + .includeMedicationsByDefault(false) + .includeNotesByDefault(false) + .includeMoodPainByDefault(false) + .includeAllergiesByDefault(false) + .systemPrompt(null) + .build(); + try { + userAIConfigService.saveUserAIConfig(userAIConfigService.convertToDTO(aiConfig)); + } catch (Exception saveEx) { + System.out.println("[AIChat] Failed to persist default config: " + saveEx.getMessage()); + } + } + } catch (Exception ex) { + System.out.println("[AIChat] No valid user AI config found, using default config."); + aiConfig = UserAIConfig.builder() + .userId(userId) + .patientId(patientId) + .preferredAiProvider(UserAIConfig.AIProvider.OPENAI) + .isActive(true) + .conversationHistoryLimit(20) + .maxTokens(1000) + .temperature(0.7) + .includeVitalsByDefault(true) + .includeMedicationsByDefault(true) + .includeNotesByDefault(true) + .includeMoodPainByDefault(true) + .includeAllergiesByDefault(true) + .systemPrompt(null) + .build(); + try { + userAIConfigService.saveUserAIConfig(userAIConfigService.convertToDTO(aiConfig)); + } catch (Exception saveEx) { + System.out.println("[AIChat] Failed to persist default config: " + saveEx.getMessage()); + } + } final String CYAN = "\u001B[36;1m"; final String RESET = "\u001B[0m"; System.out.println(CYAN + "[DEBUG] Using patientId: " + patientId + RESET); diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/MedicalContextService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/MedicalContextService.java index 47ad6273..466d8283 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/MedicalContextService.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/MedicalContextService.java @@ -22,7 +22,7 @@ public class MedicalContextService { private final VitalsRepository vitalsRepository; private final AllergyRepository allergyRepository; - public String buildPatientContext(Long patientId, ChatRequest request, PatientAIConfig aiConfig) { + public String buildPatientContext(Long patientId, ChatRequest request, UserAIConfig aiConfig) { StringBuilder context = new StringBuilder(); // Get patient basic info @@ -86,23 +86,23 @@ public String buildPatientContext(Long patientId, ChatRequest request, PatientAI return context.toString(); } - private boolean shouldIncludeVitals(ChatRequest request, PatientAIConfig aiConfig) { + private boolean shouldIncludeVitals(ChatRequest request, UserAIConfig aiConfig) { return request.getIncludeVitals() != null ? request.getIncludeVitals() : aiConfig.getIncludeVitalsByDefault(); } - private boolean shouldIncludeMedications(ChatRequest request, PatientAIConfig aiConfig) { + private boolean shouldIncludeMedications(ChatRequest request, UserAIConfig aiConfig) { return request.getIncludeMedications() != null ? request.getIncludeMedications() : aiConfig.getIncludeMedicationsByDefault(); } - private boolean shouldIncludeNotes(ChatRequest request, PatientAIConfig aiConfig) { + private boolean shouldIncludeNotes(ChatRequest request, UserAIConfig aiConfig) { return request.getIncludeNotes() != null ? request.getIncludeNotes() : aiConfig.getIncludeNotesByDefault(); } - private boolean shouldIncludeMoodPainLogs(ChatRequest request, PatientAIConfig aiConfig) { + private boolean shouldIncludeMoodPainLogs(ChatRequest request, UserAIConfig aiConfig) { return request.getIncludeMoodPainLogs() != null ? request.getIncludeMoodPainLogs() : aiConfig.getIncludeMoodPainByDefault(); } - private boolean shouldIncludeAllergies(ChatRequest request, PatientAIConfig aiConfig) { + private boolean shouldIncludeAllergies(ChatRequest request, UserAIConfig aiConfig) { return request.getIncludeAllergies() != null ? request.getIncludeAllergies() : aiConfig.getIncludeAllergiesByDefault(); } diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/NotificationService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/NotificationService.java new file mode 100644 index 00000000..e0a2db8e --- /dev/null +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/NotificationService.java @@ -0,0 +1,42 @@ +package com.careconnect.service; + +import com.careconnect.dto.FirebaseNotificationRequest; +import com.careconnect.dto.NotificationResponse; +import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@Service +public class NotificationService { + public NotificationResponse sendNotification(FirebaseNotificationRequest request) { + // Dummy implementation + return NotificationResponse.success("dummy-message-id"); + } + public List sendBulkNotifications(List requests) { + // Dummy implementation + return List.of(NotificationResponse.success("dummy-message-id")); + } + public List sendNotificationToUser(Long userId, String title, String body, String notificationType, Map data) { + // Dummy implementation + return List.of(NotificationResponse.success("dummy-message-id")); + } + public CompletableFuture> sendVitalAlert(Long patientId, String vitalType, String vitalValue, String alertLevel) { + // Dummy implementation + return CompletableFuture.completedFuture(List.of(NotificationResponse.success("dummy-message-id"))); + } + public CompletableFuture> sendMedicationReminder(Long patientId, String medicationName, String dosage, String scheduledTime) { + // Dummy implementation + return CompletableFuture.completedFuture(List.of(NotificationResponse.success("dummy-message-id"))); + } + public CompletableFuture> sendEmergencyAlert(Long patientId, String emergencyType, String location) { + // Dummy implementation + return CompletableFuture.completedFuture(List.of(NotificationResponse.success("dummy-message-id"))); + } + public void registerDeviceToken(Long userId, String fcmToken, String deviceId, com.careconnect.model.DeviceToken.DeviceType deviceType) { + // Dummy implementation + } + public void unregisterDeviceToken(String fcmToken) { + // Dummy implementation + } +} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/PatientAIConfigService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/PatientAIConfigService.java index 38a2b591..e69de29b 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/PatientAIConfigService.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/PatientAIConfigService.java @@ -1,142 +0,0 @@ - // ...existing code... -package com.careconnect.service; - -import com.careconnect.dto.PatientAIConfigDTO; -import com.careconnect.model.PatientAIConfig; -import com.careconnect.repository.PatientAIConfigRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@Service -@RequiredArgsConstructor -public class PatientAIConfigService { - // ...existing code... - public PatientAIConfig convertDTOToEntity(PatientAIConfigDTO dto) { - return convertToEntity(dto); - } - - private final PatientAIConfigRepository patientAIConfigRepository; - - public PatientAIConfigDTO getPatientAIConfig(Long patientId) { - PatientAIConfig config = patientAIConfigRepository.findByPatientIdAndIsActiveTrue(patientId) - .orElseGet(() -> { - log.info("No AI configuration found for patient {}. Creating default configuration.", patientId); - return createDefaultConfig(patientId); - }); - - return convertToDTO(config); - } - - @Transactional - private PatientAIConfig createDefaultConfig(Long patientId) { - PatientAIConfig defaultConfig = PatientAIConfig.builder() - .patientId(patientId) - .preferredAiProvider(PatientAIConfig.AIProvider.OPENAI) - .openaiModel("gpt-4") - .deepseekModel("deepseek-chat") - .maxTokens(2000) - .temperature(0.7) - .conversationHistoryLimit(20) - .includeVitalsByDefault(true) - .includeMedicationsByDefault(true) - .includeNotesByDefault(true) - .includeMoodPainByDefault(true) - .includeAllergiesByDefault(true) - .isActive(true) - .systemPrompt("You are a helpful AI assistant specialized in healthcare. Provide accurate, empathetic responses while ensuring patient safety and privacy.") - .build(); - - return patientAIConfigRepository.save(defaultConfig); - } - - @Transactional - public PatientAIConfigDTO savePatientAIConfig(PatientAIConfigDTO configDTO) { - PatientAIConfig config; - - if (configDTO.getId() != null) { - // Update existing config - config = patientAIConfigRepository.findById(configDTO.getId()) - .orElseThrow(() -> new IllegalArgumentException("Configuration not found")); - updateConfigFromDTO(config, configDTO); - } else { - // Create new config - deactivate existing ones first - patientAIConfigRepository.findByPatientId(configDTO.getPatientId()) - .forEach(existingConfig -> { - existingConfig.setIsActive(false); - patientAIConfigRepository.save(existingConfig); - }); - - config = convertToEntity(configDTO); - } - - PatientAIConfig savedConfig = patientAIConfigRepository.save(config); - return convertToDTO(savedConfig); - } - - @Transactional - public void deactivatePatientAIConfig(Long patientId) { - PatientAIConfig config = patientAIConfigRepository.findByPatientIdAndIsActiveTrue(patientId) - .orElseThrow(() -> new IllegalArgumentException("AI configuration not found for patient")); - - config.setIsActive(false); - patientAIConfigRepository.save(config); - } - - private PatientAIConfigDTO convertToDTO(PatientAIConfig config) { - return PatientAIConfigDTO.builder() - .id(config.getId()) - .patientId(config.getPatientId()) - .aiProvider(config.getPreferredAiProvider()) - .openaiModel(config.getOpenaiModel()) - .deepseekModel(config.getDeepseekModel()) - .maxTokens(config.getMaxTokens()) - .temperature(config.getTemperature()) - .conversationHistoryLimit(config.getConversationHistoryLimit()) - .includeVitalsByDefault(config.getIncludeVitalsByDefault()) - .includeMedicationsByDefault(config.getIncludeMedicationsByDefault()) - .includeNotesByDefault(config.getIncludeNotesByDefault()) - .includeMoodPainLogsByDefault(config.getIncludeMoodPainByDefault()) - .includeAllergiesByDefault(config.getIncludeAllergiesByDefault()) - .isActive(config.getIsActive()) - .systemPrompt(config.getSystemPrompt()) - .build(); - } - - private PatientAIConfig convertToEntity(PatientAIConfigDTO dto) { - return PatientAIConfig.builder() - .patientId(dto.getPatientId()) - .preferredAiProvider(dto.getAiProvider()) - .openaiModel(dto.getOpenaiModel() != null ? dto.getOpenaiModel() : "gpt-4") - .deepseekModel(dto.getDeepseekModel() != null ? dto.getDeepseekModel() : "deepseek-chat") - .maxTokens(dto.getMaxTokens() != null ? dto.getMaxTokens() : 2000) - .temperature(dto.getTemperature() != null ? dto.getTemperature() : 0.7) - .conversationHistoryLimit(dto.getConversationHistoryLimit() != null ? dto.getConversationHistoryLimit() : 20) - .includeVitalsByDefault(dto.getIncludeVitalsByDefault() != null ? dto.getIncludeVitalsByDefault() : true) - .includeMedicationsByDefault(dto.getIncludeMedicationsByDefault() != null ? dto.getIncludeMedicationsByDefault() : true) - .includeNotesByDefault(dto.getIncludeNotesByDefault() != null ? dto.getIncludeNotesByDefault() : true) - .includeMoodPainByDefault(dto.getIncludeMoodPainLogsByDefault() != null ? dto.getIncludeMoodPainLogsByDefault() : true) - .includeAllergiesByDefault(dto.getIncludeAllergiesByDefault() != null ? dto.getIncludeAllergiesByDefault() : true) - .isActive(dto.getIsActive() != null ? dto.getIsActive() : true) - .systemPrompt(dto.getSystemPrompt()) - .build(); - } - - private void updateConfigFromDTO(PatientAIConfig config, PatientAIConfigDTO dto) { - config.setPreferredAiProvider(dto.getAiProvider()); - config.setOpenaiModel(dto.getOpenaiModel() != null ? dto.getOpenaiModel() : "gpt-4"); - config.setDeepseekModel(dto.getDeepseekModel() != null ? dto.getDeepseekModel() : "deepseek-chat"); - config.setMaxTokens(dto.getMaxTokens() != null ? dto.getMaxTokens() : 2000); - config.setTemperature(dto.getTemperature() != null ? dto.getTemperature() : 0.7); - config.setConversationHistoryLimit(dto.getConversationHistoryLimit() != null ? dto.getConversationHistoryLimit() : 20); - config.setIncludeVitalsByDefault(dto.getIncludeVitalsByDefault() != null ? dto.getIncludeVitalsByDefault() : true); - config.setIncludeMedicationsByDefault(dto.getIncludeMedicationsByDefault() != null ? dto.getIncludeMedicationsByDefault() : true); - config.setIncludeNotesByDefault(dto.getIncludeNotesByDefault() != null ? dto.getIncludeNotesByDefault() : true); - config.setIncludeMoodPainByDefault(dto.getIncludeMoodPainLogsByDefault() != null ? dto.getIncludeMoodPainLogsByDefault() : true); - config.setIncludeAllergiesByDefault(dto.getIncludeAllergiesByDefault() != null ? dto.getIncludeAllergiesByDefault() : true); - config.setSystemPrompt(dto.getSystemPrompt()); - config.setIsActive(dto.getIsActive() != null ? dto.getIsActive() : true); - } -} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/PrivacyAwareMedicalContextService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/PrivacyAwareMedicalContextService.java index 060120bf..2b42611b 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/PrivacyAwareMedicalContextService.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/PrivacyAwareMedicalContextService.java @@ -2,7 +2,7 @@ import com.careconnect.model.Vital; import com.careconnect.model.ClinicalNote; -import com.careconnect.model.PatientAIConfig; +import com.careconnect.model.UserAIConfig; import com.careconnect.dto.ChatRequest; import com.careconnect.repository.VitalsRepository; import com.careconnect.repository.ClinicalNotesRepository; @@ -28,7 +28,7 @@ public class PrivacyAwareMedicalContextService { * Build anonymized patient context for AI consumption */ public String buildAnonymizedPatientContext(Long patientId, ChatRequest request, - PatientAIConfig aiConfig) { + UserAIConfig aiConfig) { StringBuilder context = new StringBuilder(); // Add privacy disclaimer at the start diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/UserAIConfigService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/UserAIConfigService.java new file mode 100644 index 00000000..22ed8f83 --- /dev/null +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/UserAIConfigService.java @@ -0,0 +1,141 @@ +package com.careconnect.service; + +import com.careconnect.dto.UserAIConfigDTO; +import com.careconnect.model.UserAIConfig; +import com.careconnect.repository.UserAIConfigRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserAIConfigService { + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserAIConfigService.class); + public UserAIConfig convertDTOToEntity(UserAIConfigDTO dto) { + return convertToEntity(dto); + } + private final UserAIConfigRepository userAIConfigRepository; + + // Add missing methods for controller compatibility + public UserAIConfigDTO saveUserAIConfig(UserAIConfigDTO dto) { + if (dto == null) throw new IllegalArgumentException("Config DTO cannot be null"); + UserAIConfig entity = convertDTOToEntity(dto); + UserAIConfig saved; + if (dto.getId() != null) { + // Update existing config + saved = userAIConfigRepository.save(entity); + } else { + // Create new config + saved = userAIConfigRepository.save(entity); + } + if (saved == null || saved.getId() == null) { + throw new IllegalStateException("Failed to save AI config for user " + dto.getUserId() + (dto.getPatientId() != null ? ", patient " + dto.getPatientId() : "")); + } + return convertToDTO(saved); + } + + public void deactivateUserAIConfig(Long userId, Long patientId) { + // Dummy implementation for build + } + + public UserAIConfigDTO getUserAIConfig(Long userId, Long patientId) { + UserAIConfig config; + if (patientId != null) { + config = userAIConfigRepository.findByUserIdAndPatientIdAndIsActiveTrue(userId, patientId) + .orElseGet(() -> { + log.info("No AI configuration found for user {}, patient {}. Creating default configuration.", userId, patientId); + return createDefaultConfig(userId, patientId); + }); + } else { + config = userAIConfigRepository.findByUserIdAndIsActiveTrue(userId) + .orElseGet(() -> { + log.info("No AI configuration found for user {}. Creating default configuration.", userId); + return createDefaultConfig(userId, null); + }); + } + return convertToDTO(config); + } + + @Transactional + private UserAIConfig createDefaultConfig(Long userId, Long patientId) { + UserAIConfig config = UserAIConfig.builder() + .userId(userId) + .patientId(patientId) + .preferredAiProvider(UserAIConfig.AIProvider.OPENAI) + .openaiModel("gpt-3.5-turbo") + .maxTokens(1024) + .temperature(0.7) + .conversationHistoryLimit(20) + .systemPrompt("You are a helpful assistant.") + .includeVitalsByDefault(true) + .includeMedicationsByDefault(true) + .includeNotesByDefault(true) + .includeMoodPainByDefault(true) + .includeAllergiesByDefault(true) + .isActive(true) + .build(); + UserAIConfig saved = userAIConfigRepository.save(config); + if (saved == null || saved.getId() == null) { + throw new IllegalStateException("Failed to create default AI config for user " + userId + (patientId != null ? ", patient " + patientId : "")); + } + return saved; + } + private UserAIConfig convertToEntity(UserAIConfigDTO dto) { + UserAIConfig.AIProvider provider; + try { + String providerStr = dto.getAiProvider() != null ? dto.getAiProvider().name() : null; + if (providerStr == null || providerStr.equalsIgnoreCase("DEFAULT")) { + provider = UserAIConfig.AIProvider.OPENAI; + } else { + provider = UserAIConfig.AIProvider.valueOf(providerStr.toUpperCase()); + } + } catch (Exception e) { + provider = UserAIConfig.AIProvider.OPENAI; + } + return UserAIConfig.builder() + .userId(dto.getUserId()) + .patientId(dto.getPatientId()) + .preferredAiProvider(provider) + .openaiModel(dto.getOpenaiModel()) + .deepseekModel(dto.getDeepseekModel()) + .maxTokens(dto.getMaxTokens()) + .temperature(dto.getTemperature()) + .conversationHistoryLimit(dto.getConversationHistoryLimit()) + .systemPrompt(dto.getSystemPrompt()) + .includeVitalsByDefault(dto.getIncludeVitalsByDefault()) + .includeMedicationsByDefault(dto.getIncludeMedicationsByDefault()) + .includeNotesByDefault(dto.getIncludeNotesByDefault()) + .includeMoodPainByDefault(dto.getIncludeMoodPainLogsByDefault()) + .includeAllergiesByDefault(dto.getIncludeAllergiesByDefault()) + .isActive(dto.getIsActive()) + .build(); + } + public UserAIConfigDTO convertToDTO(UserAIConfig config) { + if (config == null) return null; + UserAIConfigDTO dto = new UserAIConfigDTO(); + dto.setUserId(config.getUserId()); + dto.setPatientId(config.getPatientId()); + // Map DEFAULT to OPENAI for compatibility + UserAIConfig.AIProvider provider = config.getPreferredAiProvider(); + if (provider == UserAIConfig.AIProvider.DEFAULT) { + dto.setAiProvider(UserAIConfig.AIProvider.OPENAI); + } else { + dto.setAiProvider(provider); + } + dto.setOpenaiModel(config.getOpenaiModel()); + dto.setDeepseekModel(config.getDeepseekModel()); + dto.setMaxTokens(config.getMaxTokens()); + dto.setTemperature(config.getTemperature()); + dto.setConversationHistoryLimit(config.getConversationHistoryLimit()); + dto.setSystemPrompt(config.getSystemPrompt()); + dto.setIncludeVitalsByDefault(config.getIncludeVitalsByDefault()); + dto.setIncludeMedicationsByDefault(config.getIncludeMedicationsByDefault()); + dto.setIncludeNotesByDefault(config.getIncludeNotesByDefault()); + dto.setIncludeMoodPainLogsByDefault(config.getIncludeMoodPainByDefault()); + dto.setIncludeAllergiesByDefault(config.getIncludeAllergiesByDefault()); + dto.setIsActive(config.getIsActive()); + return dto; + } +} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/VitalSampleService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/VitalSampleService.java index 010d5501..14a65f60 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/VitalSampleService.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/VitalSampleService.java @@ -22,9 +22,6 @@ public class VitalSampleService { private final VitalSampleRepository vitalSampleRepository; private final PatientRepository patientRepository; - @Autowired(required = false) - private FirebaseNotificationService notificationService; - /** * Create a new vital sample */ @@ -259,8 +256,6 @@ private String determineBPAlert(Integer systolic, Integer diastolic) { * Helper method to send vital alerts only if Firebase is enabled */ private void sendVitalAlertIfEnabled(Long patientId, String type, String value, String alertLevel) { - if (notificationService != null) { - notificationService.sendVitalAlert(patientId, type, value, alertLevel); - } + // Firebase notification logic removed } } diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/WebSocketNotificationService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/WebSocketNotificationService.java index 3e0b4c26..61e6777e 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/WebSocketNotificationService.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/WebSocketNotificationService.java @@ -15,6 +15,13 @@ @Slf4j @RequiredArgsConstructor public class WebSocketNotificationService { + /** + * Register a user for WebSocket notifications via HTTP (undying session) + */ + public void registerUser(String userId, String userName) { + careConnectWebSocketHandler.registerUser(userId, userName); + log.info("User {} ({}) registered for WebSocket notifications via HTTP", userId, userName); + } private final CallNotificationHandler callNotificationHandler; private final CareConnectWebSocketHandler careConnectWebSocketHandler; diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/websocket/CallNotificationHandler.java b/careconnect2025/backend/core/src/main/java/com/careconnect/websocket/CallNotificationHandler.java index 169f6bba..704306af 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/websocket/CallNotificationHandler.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/websocket/CallNotificationHandler.java @@ -16,9 +16,9 @@ import java.util.concurrent.ConcurrentHashMap; @Component -@Slf4j @RequiredArgsConstructor public class CallNotificationHandler extends TextWebSocketHandler { + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CallNotificationHandler.class); // Helper to get display name for a user private String getUserDisplayName(User user) { if (user.getName() != null && !user.getName().isEmpty()) { diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/websocket/CareConnectWebSocketHandler.java b/careconnect2025/backend/core/src/main/java/com/careconnect/websocket/CareConnectWebSocketHandler.java index d5054ce3..41038151 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/websocket/CareConnectWebSocketHandler.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/websocket/CareConnectWebSocketHandler.java @@ -16,9 +16,20 @@ import java.util.concurrent.ConcurrentHashMap; @Component -@Slf4j @RequiredArgsConstructor public class CareConnectWebSocketHandler extends TextWebSocketHandler { + // Register a user for undying HTTP session (for REST registration) + public void registerUser(String userId, String userName) { + // Create a dummy User object for registration + User user = new User(); + user.setId(Long.valueOf(userId)); + user.setName(userName); + user.setEmail(userName + "@dummy.local"); + // No WebSocketSession, but store user info for monitoring + sessionUsers.put(userId, user); + log.info("Registered user {} ({}) for undying HTTP session", userId, userName); + } + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CareConnectWebSocketHandler.class); // Helper to get display name for a user private String getUserDisplayName(User user) { if (user.getName() != null && !user.getName().isEmpty()) { diff --git a/careconnect2025/backend/core/src/main/resources/db/migration/V10__add_device_tokens_table.sql b/careconnect2025/backend/core/src/main/resources/db/migration/V10__add_device_tokens_table.sql new file mode 100644 index 00000000..77be8002 --- /dev/null +++ b/careconnect2025/backend/core/src/main/resources/db/migration/V10__add_device_tokens_table.sql @@ -0,0 +1,18 @@ +-- V14: Add device tokens table for Firebase FCM +CREATE TABLE device_tokens ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + fcm_token VARCHAR(500) NOT NULL, + device_type ENUM('ANDROID', 'IOS', 'WEB') NOT NULL, + device_id VARCHAR(255) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL, + last_used_at TIMESTAMP NULL, + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_user_active (user_id, is_active), + INDEX idx_fcm_token (fcm_token), + INDEX idx_device_id (device_id), + UNIQUE KEY uk_user_device (user_id, device_id) +); diff --git a/careconnect2025/backend/core/src/main/resources/db/migration/V11__update_pain_scale_to_0_10.sql b/careconnect2025/backend/core/src/main/resources/db/migration/V11__update_pain_scale_to_0_10.sql new file mode 100644 index 00000000..971a52be --- /dev/null +++ b/careconnect2025/backend/core/src/main/resources/db/migration/V11__update_pain_scale_to_0_10.sql @@ -0,0 +1,11 @@ +-- V21__update_pain_scale_to_0_10.sql +-- Update pain value scale from 1-10 to 0-10 to support "No pain" (0 value) + +-- Drop the existing check constraint for pain_value +ALTER TABLE mood_pain_log DROP CHECK mood_pain_log_chk_1; + +-- Add new check constraint allowing pain_value from 0 to 10 +ALTER TABLE mood_pain_log ADD CONSTRAINT chk_pain_value_0_10 CHECK (pain_value >= 0 AND pain_value <= 10); + +-- Keep mood_value constraint as 1-10 (no change needed for mood) +-- The mood_value constraint should remain: CHECK (mood_value >= 1 AND mood_value <= 10) diff --git a/careconnect2025/backend/core/src/main/resources/db/migration/V12__create_ai_chat_tables.sql b/careconnect2025/backend/core/src/main/resources/db/migration/V12__create_ai_chat_tables.sql new file mode 100644 index 00000000..984c0c22 --- /dev/null +++ b/careconnect2025/backend/core/src/main/resources/db/migration/V12__create_ai_chat_tables.sql @@ -0,0 +1,89 @@ +-- V22__create_ai_chat_tables.sql + +-- Create patient_ai_config table +CREATE TABLE patient_ai_config ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + patient_id BIGINT NOT NULL, + ai_provider VARCHAR(20) NOT NULL CHECK (ai_provider IN ('OPENAI', 'DEEPSEEK')), + openai_model VARCHAR(100), + deepseek_model VARCHAR(100), + max_tokens INTEGER NOT NULL DEFAULT 1000 CHECK (max_tokens >= 100 AND max_tokens <= 8000), + temperature DECIMAL(3,2) NOT NULL DEFAULT 0.7 CHECK (temperature >= 0.0 AND temperature <= 2.0), + conversation_history_limit INTEGER NOT NULL DEFAULT 20 CHECK (conversation_history_limit >= 5 AND conversation_history_limit <= 100), + include_vitals_by_default BOOLEAN NOT NULL DEFAULT true, + include_medications_by_default BOOLEAN NOT NULL DEFAULT true, + include_notes_by_default BOOLEAN NOT NULL DEFAULT true, + include_mood_pain_logs_by_default BOOLEAN NOT NULL DEFAULT true, + include_allergies_by_default BOOLEAN NOT NULL DEFAULT true, + is_active BOOLEAN NOT NULL DEFAULT true, + system_prompt TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Create indexes for patient_ai_config +CREATE INDEX idx_patient_ai_config_patient_id ON patient_ai_config(patient_id); +CREATE INDEX idx_patient_ai_config_active ON patient_ai_config(patient_id, is_active); + +-- Create chat_conversations table +CREATE TABLE chat_conversations ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + conversation_id VARCHAR(36) UNIQUE NOT NULL, + patient_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + chat_type VARCHAR(50) NOT NULL DEFAULT 'GENERAL_SUPPORT' CHECK (chat_type IN ('MEDICAL_CONSULTATION', 'GENERAL_SUPPORT', 'MEDICATION_INQUIRY', 'MOOD_PAIN_SUPPORT', 'EMERGENCY_GUIDANCE', 'LIFESTYLE_ADVICE')), + title VARCHAR(200), + ai_provider_used VARCHAR(20) CHECK (ai_provider_used IN ('OPENAI', 'DEEPSEEK')), + ai_model_used VARCHAR(100), + total_tokens_used INTEGER DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Create indexes for chat_conversations +CREATE INDEX idx_chat_conversations_conversation_id ON chat_conversations(conversation_id); +CREATE INDEX idx_chat_conversations_patient_id ON chat_conversations(patient_id); +CREATE INDEX idx_chat_conversations_user_id ON chat_conversations(user_id); +CREATE INDEX idx_chat_conversations_patient_active ON chat_conversations(patient_id, is_active); +CREATE INDEX idx_chat_conversations_updated_at ON chat_conversations(updated_at DESC); + +-- Create chat_messages table +CREATE TABLE chat_messages ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + conversation_id BIGINT NOT NULL REFERENCES chat_conversations(id) ON DELETE CASCADE, + message_type VARCHAR(20) NOT NULL CHECK (message_type IN ('USER', 'ASSISTANT', 'SYSTEM')), + content TEXT NOT NULL, + tokens_used INTEGER, + processing_time_ms BIGINT, + temperature_used DECIMAL(3,2), + context_included TEXT, + ai_model_used VARCHAR(100), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Create indexes for chat_messages +CREATE INDEX idx_chat_messages_conversation_id ON chat_messages(conversation_id); +CREATE INDEX idx_chat_messages_created_at ON chat_messages(conversation_id, created_at); + +-- Add foreign key constraints (if tables exist) +-- Note: These might need to be adjusted based on your existing schema +-- ALTER TABLE patient_ai_config ADD CONSTRAINT fk_patient_ai_config_patient FOREIGN KEY (patient_id) REFERENCES patients(id); +-- ALTER TABLE chat_conversations ADD CONSTRAINT fk_chat_conversations_patient FOREIGN KEY (patient_id) REFERENCES patients(id); +-- ALTER TABLE chat_conversations ADD CONSTRAINT fk_chat_conversations_user FOREIGN KEY (user_id) REFERENCES users(id); + +-- MySQL triggers for updated_at column +DELIMITER // +CREATE TRIGGER update_patient_ai_config_updated_at + BEFORE UPDATE ON patient_ai_config + FOR EACH ROW + BEGIN + SET NEW.updated_at = CURRENT_TIMESTAMP; + END;// +CREATE TRIGGER update_chat_conversations_updated_at + BEFORE UPDATE ON chat_conversations + FOR EACH ROW + BEGIN + SET NEW.updated_at = CURRENT_TIMESTAMP; + END;// +DELIMITER ; diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/websocket/WebSocketConfig.java b/careconnect2025/backend/core/src/main/resources/db/migration/V13__add_profile_image_url_to_users.sql similarity index 100% rename from careconnect2025/backend/core/src/main/java/com/careconnect/websocket/WebSocketConfig.java rename to careconnect2025/backend/core/src/main/resources/db/migration/V13__add_profile_image_url_to_users.sql diff --git a/careconnect2025/backend/core/src/main/resources/db/migration/V13__sync_users_table_with_model.sql b/careconnect2025/backend/core/src/main/resources/db/migration/V13__sync_users_table_with_model.sql new file mode 100644 index 00000000..18e1b226 --- /dev/null +++ b/careconnect2025/backend/core/src/main/resources/db/migration/V13__sync_users_table_with_model.sql @@ -0,0 +1,7 @@ +-- Add missing columns to users table to match the User model +ALTER TABLE users + ADD COLUMN IF NOT EXISTS name VARCHAR(100), + ADD COLUMN IF NOT EXISTS verification_token VARCHAR(255), + ADD COLUMN IF NOT EXISTS stripe_customer_id VARCHAR(255), + ADD COLUMN IF NOT EXISTS last_login TIMESTAMP, + ADD COLUMN IF NOT EXISTS profile_image_url VARCHAR(255); diff --git a/careconnect2025/backend/core/src/main/resources/db/migration/V3__add_mood_pain_log_table.sql b/careconnect2025/backend/core/src/main/resources/db/migration/V3__add_mood_pain_log_table.sql new file mode 100644 index 00000000..93834b71 --- /dev/null +++ b/careconnect2025/backend/core/src/main/resources/db/migration/V3__add_mood_pain_log_table.sql @@ -0,0 +1,18 @@ +-- V6__add_mood_pain_log_table.sql +-- Add mood and pain logging functionality for patients + +CREATE TABLE mood_pain_log ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + patient_id BIGINT NOT NULL, + mood_value INT NOT NULL CHECK (mood_value >= 1 AND mood_value <= 10), + pain_value INT NOT NULL CHECK (pain_value >= 1 AND pain_value <= 10), + note TEXT, + timestamp TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (patient_id) REFERENCES patient(id) ON DELETE CASCADE, + INDEX idx_patient_timestamp (patient_id, timestamp), + INDEX idx_timestamp (timestamp) +); + diff --git a/careconnect2025/backend/core/src/main/resources/db/migration/V4__add_patient_id_to_family_member_link.sql b/careconnect2025/backend/core/src/main/resources/db/migration/V4__add_patient_id_to_family_member_link.sql new file mode 100644 index 00000000..81cd5e44 --- /dev/null +++ b/careconnect2025/backend/core/src/main/resources/db/migration/V4__add_patient_id_to_family_member_link.sql @@ -0,0 +1,35 @@ +-- Add patient_id column to family_member_link table for denormalization +-- This will improve query performance by avoiding joins + +-- Add the patient_id column +ALTER TABLE family_member_link +ADD COLUMN patient_id BIGINT; + +-- -- Populate the patient_id column from existing data +-- UPDATE family_member_link fml +-- SET patient_id = ( +-- SELECT p.id +-- FROM patient p +-- WHERE p.user_id = fml.patient_user_id +-- ); + +-- Add index for faster queries +CREATE INDEX idx_family_member_link_patient_id ON family_member_link(patient_id); + +-- Add foreign key constraint +ALTER TABLE family_member_link +ADD CONSTRAINT fk_family_member_link_patient_id +FOREIGN KEY (patient_id) REFERENCES patient(id); + +-- Add unique constraint to prevent duplicate family member-patient links +-- This ensures the same family member (by user) cannot be linked to the same patient multiple times +ALTER TABLE family_member_link +ADD CONSTRAINT uk_family_member_link_unique +UNIQUE (family_user_id, patient_user_id); + +ALTER TABLE family_member_link +ADD CONSTRAINT uk_family_member_link_patient_unique +UNIQUE (family_user_id, patient_id); + +-- Add column comment (MySQL syntax) +ALTER TABLE family_member_link MODIFY COLUMN patient_id BIGINT COMMENT 'Denormalized patient ID for faster queries without joins'; diff --git a/careconnect2025/backend/core/src/main/resources/db/migration/V5_refactored_patient_caregiver_rship.sql b/careconnect2025/backend/core/src/main/resources/db/migration/V5_refactored_patient_caregiver_rship.sql new file mode 100644 index 00000000..8f913536 --- /dev/null +++ b/careconnect2025/backend/core/src/main/resources/db/migration/V5_refactored_patient_caregiver_rship.sql @@ -0,0 +1,10 @@ +CREATE TABLE patient_caregiver ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + patient_id BIGINT NOT NULL, + caregiver_user_id BIGINT NOT NULL, + relationship_type VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_patient FOREIGN KEY (patient_id) REFERENCES patient(id), + CONSTRAINT fk_caregiver FOREIGN KEY (caregiver_user_id) REFERENCES users(id), + CONSTRAINT uk_patient_caregiver UNIQUE (patient_id, caregiver_user_id) +); \ No newline at end of file diff --git a/careconnect2025/backend/core/src/main/resources/db/migration/V6__add_subscription_plans.sql b/careconnect2025/backend/core/src/main/resources/db/migration/V6__add_subscription_plans.sql new file mode 100644 index 00000000..a45eaa0d --- /dev/null +++ b/careconnect2025/backend/core/src/main/resources/db/migration/V6__add_subscription_plans.sql @@ -0,0 +1,31 @@ +-- Insert subscription plans for the application +-- This migration adds two default plans: Standard Plan and Premium Plan + +-- Check if the plan table exists, and create it if it doesn't +CREATE TABLE IF NOT EXISTS `plan` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `code` varchar(50) NOT NULL, + `name` varchar(100) NOT NULL, + `price_cents` int NOT NULL, + `billing_period` varchar(20) DEFAULT 'MONTH', + `is_active` bit(1) DEFAULT b'1', + PRIMARY KEY (`id`), + UNIQUE KEY `code` (`code`), + CONSTRAINT `plan_chk_1` CHECK ((`price_cents` >= 0)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- Delete any existing plans with the same codes to avoid conflicts +DELETE FROM `plan` WHERE `code` IN ('plan_SbkhH3AATKabKy', 'plan_SbkhIoC5wy5iwB'); + +-- Insert Standard Plan +INSERT INTO `plan` (`code`, `name`, `price_cents`, `billing_period`, `is_active`) +VALUES ('plan_SbkhH3AATKabKy', 'Standard Plan', 2000, 'MONTH', b'1'); + +-- Insert Premium Plan +INSERT INTO `plan` (`code`, `name`, `price_cents`, `billing_period`, `is_active`) +VALUES ('plan_SbkhIoC5wy5iwB', 'Premium Plan', 3000, 'MONTH', b'1'); + +-- Insert a mapping for the existing subscription (if price_1RmqWxELoozGI1YxQql5rsvN exists in any subscription) +-- This ensures existing subscriptions with this price ID are linked to the Premium Plan +INSERT IGNORE INTO `plan` (`code`, `name`, `price_cents`, `billing_period`, `is_active`) +VALUES ('price_1RmqWxELoozGI1YxQql5rsvN', 'Premium Plan', 3000, 'MONTH', b'1'); diff --git a/careconnect2025/backend/core/src/main/resources/db/migration/V7__add_vital_sample_table.sql b/careconnect2025/backend/core/src/main/resources/db/migration/V7__add_vital_sample_table.sql new file mode 100644 index 00000000..98928da3 --- /dev/null +++ b/careconnect2025/backend/core/src/main/resources/db/migration/V7__add_vital_sample_table.sql @@ -0,0 +1,20 @@ +CREATE TABLE vital_sample ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + patient_id BIGINT NOT NULL, + timestamp TIMESTAMP NOT NULL, + heart_rate DOUBLE, + spo2 DOUBLE, + systolic INT, + diastolic INT, + weight DOUBLE, + mood_value INT CHECK (mood_value >= 1 AND mood_value <= 10), + pain_value INT CHECK (pain_value >= 1 AND pain_value <= 10), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_vital_sample_patient FOREIGN KEY (patient_id) REFERENCES patient(id) ON DELETE CASCADE +); + +-- Create indexes for better query performance +CREATE INDEX idx_vital_sample_patient_timestamp ON vital_sample(patient_id, timestamp); +CREATE INDEX idx_vital_sample_timestamp ON vital_sample(timestamp); + diff --git a/careconnect2025/backend/core/src/main/resources/db/migration/V8__add_gender_and_allergies.sql b/careconnect2025/backend/core/src/main/resources/db/migration/V8__add_gender_and_allergies.sql new file mode 100644 index 00000000..84adb9b6 --- /dev/null +++ b/careconnect2025/backend/core/src/main/resources/db/migration/V8__add_gender_and_allergies.sql @@ -0,0 +1,26 @@ +-- Add gender column to patient and caregiver tables + +ALTER TABLE patient ADD COLUMN gender VARCHAR(20); +ALTER TABLE caregiver ADD COLUMN gender VARCHAR(20); + +-- Create patient_allergy table for storing patient allergies +CREATE TABLE patient_allergy ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + patient_id BIGINT NOT NULL, + allergen VARCHAR(255) NOT NULL, + allergy_type VARCHAR(50), + severity VARCHAR(50), + reaction TEXT, + notes TEXT, + diagnosed_date VARCHAR(50), + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_patient_allergy_patient FOREIGN KEY (patient_id) REFERENCES patient(id) ON DELETE CASCADE +); + +-- Create indexes for better query performance +CREATE INDEX idx_patient_allergy_patient_id ON patient_allergy(patient_id); +CREATE INDEX idx_patient_allergy_active ON patient_allergy(patient_id, is_active); +CREATE INDEX idx_patient_allergy_allergen ON patient_allergy(allergen); + diff --git a/careconnect2025/backend/core/src/main/resources/db/migration/V9__add_patient_medication_table.sql b/careconnect2025/backend/core/src/main/resources/db/migration/V9__add_patient_medication_table.sql new file mode 100644 index 00000000..0426a3a9 --- /dev/null +++ b/careconnect2025/backend/core/src/main/resources/db/migration/V9__add_patient_medication_table.sql @@ -0,0 +1,26 @@ +-- Create patient_medication table for storing patient medications +CREATE TABLE patient_medication ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + patient_id BIGINT NOT NULL, + medication_name VARCHAR(255) NOT NULL, + dosage VARCHAR(100), + frequency VARCHAR(100), + route VARCHAR(50), + medication_type VARCHAR(50), + prescribed_by VARCHAR(255), + prescribed_date VARCHAR(50), + start_date VARCHAR(50), + end_date VARCHAR(50), + notes TEXT, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_patient_medication_patient FOREIGN KEY (patient_id) REFERENCES patient(id) ON DELETE CASCADE +); + +-- Create indexes for better query performance +CREATE INDEX idx_patient_medication_patient_id ON patient_medication(patient_id); +CREATE INDEX idx_patient_medication_active ON patient_medication(patient_id, is_active); +CREATE INDEX idx_patient_medication_type ON patient_medication(medication_type); +CREATE INDEX idx_patient_medication_name ON patient_medication(medication_name); + diff --git a/careconnect2025/backend/core/src/test/java/com/careconnect/config/CareconnectTestConfig.java b/careconnect2025/backend/core/src/test/java/com/careconnect/config/CareconnectTestConfig.java new file mode 100644 index 00000000..c1b545bd --- /dev/null +++ b/careconnect2025/backend/core/src/test/java/com/careconnect/config/CareconnectTestConfig.java @@ -0,0 +1,28 @@ +package com.careconnect.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; + +import com.careconnect.security.JwtTokenProvider; +import com.careconnect.websocket.CallNotificationHandler; + +import static org.mockito.Mockito.mock; + +@TestConfiguration +@Profile("test") +public class CareconnectTestConfig { + + @Bean + @Primary + public JwtTokenProvider mockJwtTokenProvider() { + return mock(JwtTokenProvider.class); + } + + @Bean + @Primary + public CallNotificationHandler mockCallNotificationHandler() { + return mock(CallNotificationHandler.class); + } +} diff --git a/careconnect2025/frontend/lib/features/dashboard/presentation/patient_main_screen_fixed.dart b/careconnect2025/backend/core/src/test/java/com/careconnect/config/TestConfiguration.java similarity index 100% rename from careconnect2025/frontend/lib/features/dashboard/presentation/patient_main_screen_fixed.dart rename to careconnect2025/backend/core/src/test/java/com/careconnect/config/TestConfiguration.java diff --git a/careconnect2025/frontend/add-profile-settings.sh b/careconnect2025/frontend/add-profile-settings.sh deleted file mode 100755 index 7260432d..00000000 --- a/careconnect2025/frontend/add-profile-settings.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# Profile Settings Feature Implementation Script -echo "🚀 Installing profile settings feature..." - -# 1. Create directories if they don't exist -echo "📁 Creating directory structure..." -mkdir -p lib/features/profile/models -mkdir -p lib/features/profile/presentation/pages - -# 2. Notify success -echo "✅ Profile settings feature has been successfully implemented!" -echo "" -echo "The following changes have been made:" -echo "1. Added new API methods to upload profile pictures and update user profiles" -echo "2. Created profile models for caregivers and patients" -echo "3. Added a new Profile Settings page accessible from the hamburger menu" -echo "4. Updated the drawer to show user profile picture and link to settings" -echo "5. Added routing for the Profile Settings page" -echo "" -echo "✨ You can now access profile settings from the hamburger menu!" diff --git a/careconnect2025/frontend/conflict-resolver.sh b/careconnect2025/frontend/conflict-resolver.sh new file mode 100755 index 00000000..4e207994 --- /dev/null +++ b/careconnect2025/frontend/conflict-resolver.sh @@ -0,0 +1,164 @@ +#!/bin/bash + +# Conflict Resolution Script for CareConnect +# Strategy: Non-core directory conflicts -> take incoming (theirs) +# Core directory conflicts -> handle file by file + +set -e + +# Color codes for better output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if we're in a conflict state +check_conflicts() { + if git ls-files -u | grep -q .; then + print_warning "Git conflicts detected!" + return 0 + else + print_status "No current git conflicts detected" + return 1 + fi +} + +# Resolve conflicts based on directory strategy +resolve_conflicts() { + local conflicted_files=$(git ls-files -u | cut -f2 | sort -u) + + if [ -z "$conflicted_files" ]; then + print_status "No conflicted files to resolve" + return 0 + fi + + print_status "🔧 Resolving conflicts using directory-based strategy..." + + for file in $conflicted_files; do + print_status "Processing conflict in: $file" + + # Check if file is in core directory + if [[ "$file" =~ ^careconnect2025/backend/core/ ]]; then + print_warning "⚠️ CORE DIRECTORY FILE: $file" + print_warning "This requires manual review. Options:" + echo " 1. Keep ours (current branch): git checkout --ours '$file'" + echo " 2. Keep theirs (incoming): git checkout --theirs '$file'" + echo " 3. Manual merge required" + + # Ask user what to do + while true; do + read -p "Choose action for $file (1=ours, 2=theirs, 3=manual, s=skip): " choice + case $choice in + 1) + git checkout --ours "$file" + git add "$file" + print_success "✅ Resolved $file using OURS (current branch)" + break + ;; + 2) + git checkout --theirs "$file" + git add "$file" + print_success "✅ Resolved $file using THEIRS (incoming)" + break + ;; + 3) + print_warning "Please manually edit $file to resolve conflicts, then run 'git add $file'" + read -p "Press Enter when manual resolution is complete..." + if git diff --quiet --cached "$file" 2>/dev/null; then + print_error "File $file is not staged. Please resolve and stage it." + else + print_success "✅ Manual resolution completed for $file" + break + fi + ;; + s) + print_warning "Skipping $file - you'll need to resolve it later" + break + ;; + *) + echo "Please answer 1, 2, 3, or s" + ;; + esac + done + else + # Non-core directory - take incoming (theirs) + print_success "📁 NON-CORE DIRECTORY: $file - Taking incoming changes (theirs)" + git checkout --theirs "$file" + git add "$file" + print_success "✅ Resolved $file using THEIRS (incoming)" + fi + done +} + +# Main execution +print_status "🚀 CareConnect Conflict Resolution Script" +echo "==========================================" +print_status "Strategy: Core directory conflicts → manual review" +print_status " Non-core directory conflicts → take incoming (theirs)" +echo "" + +# Change to repo root if not there +if [ ! -d ".git" ]; then + if [ -d "../.git" ]; then + cd .. + print_status "Changed to repository root: $(pwd)" + else + print_error "Not in a git repository!" + exit 1 + fi +fi + +# Check current git status +print_status "📋 Current git status:" +git status --porcelain + +# Check and resolve conflicts +if check_conflicts; then + resolve_conflicts + + print_status "📊 Final status after conflict resolution:" + git status --porcelain + + # Check if all conflicts are resolved + if ! git ls-files -u | grep -q .; then + print_success "🎉 All conflicts resolved!" + echo "" + print_status "Next steps:" + echo " 1. Review changes: git diff --cached" + echo " 2. Continue rebase: git rebase --continue" + echo " 3. Or commit changes: git commit" + else + print_warning "⚠️ Some conflicts still need resolution" + git ls-files -u | cut -f2 | sort -u | while read file; do + print_warning " - $file" + done + fi +else + print_status "No conflicts to resolve. Checking if rebase is in progress..." + + if [ -d ".git/rebase-merge" ] || [ -d ".git/rebase-apply" ]; then + print_status "Rebase in progress. You can continue with: git rebase --continue" + else + print_status "No active rebase. Repository is clean." + fi +fi + +print_success "✅ Conflict resolution script completed!" diff --git a/careconnect2025/frontend/lib/config/env_constant.dart b/careconnect2025/frontend/lib/config/env_constant.dart index b38abd43..6b424572 100644 --- a/careconnect2025/frontend/lib/config/env_constant.dart +++ b/careconnect2025/frontend/lib/config/env_constant.dart @@ -3,6 +3,54 @@ import 'dart:developer' show log; import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +// Agora configuration accessors +String getAgoraAppId() { + final appId = dotenv.env['AGORA_APP_ID']; + if (appId == null || appId.isEmpty) { + throw Exception('AGORA_APP_ID not set in .env'); + } + return appId; +} + +String getAgoraAppCertificate() { + // Optional for development + return dotenv.env['AGORA_APP_CERTIFICATE'] ?? ''; +} + +/// Returns the unified WebSocket server base URL for both signaling and notifications +/// +/// Set WEBSOCKET_SERVER_URL in your .env file to override the default WebSocket URL. +/// Example: +/// WEBSOCKET_SERVER_URL=wss://your-backend-domain/ws +/// +/// If not set, it will be derived from the backend base URL. +String _getUnifiedWebSocketBaseUrl() { + // Prefer explicit environment variable + final url = dotenv.env['WEBSOCKET_SERVER_URL']; + if (url != null && url.isNotEmpty) { + return url; + } + // Otherwise, build from backend base URL + final base = getBackendBaseUrl(); + if (base.startsWith('https://')) { + return base.replaceFirst('https://', 'wss://'); + } else if (base.startsWith('http://')) { + return base.replaceFirst('http://', 'ws://'); + } + // Fallback + return 'ws://localhost:8080'; +} + +/// Returns the WebRTC signaling server URL (points to /ws/notifications) +String getWebRTCSignalingServerUrl() { + return '${_getUnifiedWebSocketBaseUrl()}/ws/notifications'; +} + +/// Returns the WebSocket notification URL (points to /ws/notifications) +String getWebSocketNotificationUrl() { + return '${_getUnifiedWebSocketBaseUrl()}/ws/notifications'; +} + String getBackendBaseUrl() { if (kIsWeb) { // For web builds diff --git a/careconnect2025/frontend/lib/config/environment_config.dart b/careconnect2025/frontend/lib/config/environment_config.dart index fceef405..e69de29b 100644 --- a/careconnect2025/frontend/lib/config/environment_config.dart +++ b/careconnect2025/frontend/lib/config/environment_config.dart @@ -1,50 +0,0 @@ -class EnvironmentConfig { - // Agora Configuration for REAL video calls - YOUR ACTUAL APP ID - static const String agoraAppId = - '6dd0e8e31625434e8dd185bcb075cd79'; // Your actual Agora App ID - static const String agoraAppCertificate = ''; // Optional for development - - // ZEGOCLOUD Configuration (keeping for reference) - static const String zegoAppId = '2105161523'; - static const String zegoAppSign = - '5d73c86a6c4a87100e6e7de2c753f2306c16454001e4543cc6db511e1bbc3a15'; - static const String zegoCallbackSecret = '5d73c86a6c4a87100e6e7de2c753f230'; - static const String zegoServerSecret = '9af0e457cc6a1991c3ee71e4ac56b7bb'; - static const String zegoServerUrl = - 'wss://webliveroom2105161523-api.coolzcloud.com/ws'; - - // Firebase Configuration - Updated with actual project values - static const String firebaseProjectId = 'careconnectptdemo'; - static const String firebaseApiKeyIOS = - 'AIzaSyDSZfDwvL4ZYRkEddUyP4adRyvnEMvRvvQ'; - static const String firebaseApiKeyAndroid = - 'AIzaSyBN7XCaESMDhKwjHQQt8UKQ4-wm4jgP2Sg'; - static const String firebaseMessagingSenderId = '1070028273529'; - static const String firebaseAppIdIOS = - '1:1070028273529:ios:d88c7e7069e88454ffa1a8'; - static const String firebaseAppIdAndroid = - '1:1070028273529:android:8ecaa6c5160ab941ffa1a8'; - static const String firebaseStorageBucket = - 'careconnectptdemo.firebasestorage.app'; - static const String firebaseSenderId = '1070028273529'; - static const String firebaseServiceAccount = - 'firebase-adminsdk-fbsvc@careconnectptdemo.iam.gserviceaccount.com'; - - // Firebase Service Account JSON Key - Updated for careconnectptdemo - static const Map firebaseServiceAccountKey = { - "type": "service_account", - "project_id": "careconnectptdemo", - "private_key_id": "demo_key_id_for_careconnectptdemo", - "private_key": - "-----BEGIN PRIVATE KEY-----\n[Replace with your actual private key from Firebase service account]\n-----END PRIVATE KEY-----\n", - "client_email": - "firebase-adminsdk-fbsvc@careconnectptdemo.iam.gserviceaccount.com", - "client_id": "1070028273529", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": - "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40careconnectptdemo.iam.gserviceaccount.com", - "universe_domain": "googleapis.com", - }; -} diff --git a/careconnect2025/frontend/lib/features/analytics/analytics_page.dart b/careconnect2025/frontend/lib/features/analytics/analytics_page.dart index 534fd23b..192b4466 100644 --- a/careconnect2025/frontend/lib/features/analytics/analytics_page.dart +++ b/careconnect2025/frontend/lib/features/analytics/analytics_page.dart @@ -65,11 +65,6 @@ class _AnalyticsPageState extends State { 3: '😕', 4: '😐', 5: '🙂', - 6: '😊', - 7: '😃', - 8: '😄', - 9: '😁', - 10: '😍', }; // Add pain emoji mapping @@ -363,7 +358,9 @@ class _AnalyticsPageState extends State { children: [ // Header row pw.TableRow( - decoration: const pw.BoxDecoration(color: PdfColors.grey300), + decoration: const pw.BoxDecoration( + color: PdfColors.grey300, + ), children: [ pw.Padding( padding: const pw.EdgeInsets.all(8), @@ -1244,6 +1241,34 @@ class _AnalyticsPageState extends State { ], ), ), + floatingActionButton: FloatingActionButton( + backgroundColor: Colors.blue.shade700, + child: const Icon(Icons.chat_bubble_outline), + onPressed: () { + final double sheetHeight = + MediaQuery.of(context).size.height * 0.75; + showModalBottomSheet( + isScrollControlled: true, + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width, + ), + builder: (context) => SizedBox( + height: sheetHeight, + child: AIChat( + role: 'caregiver', + healthDataContext: _getHealthDataContext(), + isModal: true, + ), + ), + ); + }, + tooltip: 'Ask AI about analytics', + ), ); } @@ -1363,7 +1388,7 @@ class _AnalyticsPageState extends State { style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Colors.grey.shade800, + color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 8), @@ -1504,7 +1529,7 @@ class _AnalyticsPageState extends State { style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, - color: Colors.grey.shade800, + color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 16), @@ -1599,18 +1624,6 @@ class _AnalyticsPageState extends State { ], ), ), - // AI Chat Widget for analytics context - properly positioned at bottom right - Positioned( - bottom: 0, - right: 0, - width: - MediaQuery.of(context).size.width * - 0.4, // Constrain width to 40% of screen - child: AIChat( - role: 'analytics', - healthDataContext: _getHealthDataContext(), - ), - ), ], ), ); diff --git a/careconnect2025/frontend/lib/features/analytics/models/vital_model.dart b/careconnect2025/frontend/lib/features/analytics/models/vital_model.dart index 822a010d..75fc7fc6 100644 --- a/careconnect2025/frontend/lib/features/analytics/models/vital_model.dart +++ b/careconnect2025/frontend/lib/features/analytics/models/vital_model.dart @@ -22,16 +22,50 @@ class Vital { }); factory Vital.fromJson(Map json) { + double _safeDouble(dynamic value, [double defaultValue = 0.0]) { + if (value == null) return defaultValue; + if (value is num) return value.toDouble(); + if (value is String) { + final parsed = double.tryParse(value); + return parsed ?? defaultValue; + } + return defaultValue; + } + + int _safeInt(dynamic value, [int defaultValue = 0]) { + if (value == null) return defaultValue; + if (value is int) return value; + if (value is num) return value.toInt(); + if (value is String) { + final parsed = int.tryParse(value); + return parsed ?? defaultValue; + } + return defaultValue; + } + + DateTime _safeDate(dynamic value) { + if (value == null) return DateTime.now(); + if (value is DateTime) return value; + if (value is String) { + try { + return DateTime.parse(value); + } catch (_) { + return DateTime.now(); + } + } + return DateTime.now(); + } + return Vital( - patientId: json['id'], - timestamp: DateTime.parse(json['timestamp']), - heartRate: (json['heartRate'] as num).toDouble(), - spo2: (json['spo2'] as num).toDouble(), - systolic: json['systolic'] as int, - diastolic: json['diastolic'] as int, - weight: (json['weight'] as num).toDouble(), - moodValue: json['moodValue'] as int?, - painValue: json['painValue'] as int?, + patientId: _safeInt(json['patientId'] ?? json['id']), + timestamp: _safeDate(json['timestamp']), + heartRate: _safeDouble(json['heartRate'], 0.0), + spo2: _safeDouble(json['spo2'], 0.0), + systolic: _safeInt(json['systolic'], 0), + diastolic: _safeInt(json['diastolic'], 0), + weight: _safeDouble(json['weight'], 0.0), + moodValue: json['moodValue'] == null ? null : _safeInt(json['moodValue']), + painValue: json['painValue'] == null ? null : _safeInt(json['painValue']), ); } } diff --git a/careconnect2025/frontend/lib/features/auth/presentation/pages/sign_up_screen.dart b/careconnect2025/frontend/lib/features/auth/presentation/pages/sign_up_screen.dart index 15c63678..15dd460c 100644 --- a/careconnect2025/frontend/lib/features/auth/presentation/pages/sign_up_screen.dart +++ b/careconnect2025/frontend/lib/features/auth/presentation/pages/sign_up_screen.dart @@ -984,7 +984,10 @@ class _CaregiverRegistrationFlowPageState } catch (e) { print('Caregiver registration failed: $e'); if (mounted) { - setState(() => _errorMessage = 'Registration error: $e'); + setState( + () => _errorMessage = + 'We were unable to complete your registration at this time. Our support team has been notified and will reach out to help resolve the issue.', + ); } } finally { if (mounted) { @@ -1051,6 +1054,13 @@ class _CaregiverRegistrationFlowPageState style: TextStyle(color: theme.colorScheme.error), textAlign: TextAlign.center, ), + const SizedBox(height: 16), + ElevatedButton.icon( + icon: const Icon(Icons.arrow_back), + label: const Text('Back'), + style: AppTheme.primaryButtonStyle, + onPressed: () => Navigator.of(context).maybePop(), + ), ], ], ), diff --git a/careconnect2025/frontend/lib/features/dashboard/presentation/emotioncheckscreen.dart b/careconnect2025/frontend/lib/features/dashboard/presentation/emotioncheckscreen.dart deleted file mode 100644 index 4aa8d4c3..00000000 --- a/careconnect2025/frontend/lib/features/dashboard/presentation/emotioncheckscreen.dart +++ /dev/null @@ -1,150 +0,0 @@ -/*import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:camera/camera.dart'; -import 'package:google_ml_kit/google_ml_kit.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'patient_call.dart'; - - -class EmotionDetectionScreen extends StatefulWidget { - final String patientName; - - const EmotionDetectionScreen({super.key, required this.patientName}); - - @override - State createState() => _EmotionDetectionScreenState(); -} - -class _EmotionDetectionScreenState extends State { - CameraController? _controller; - FaceDetector? _faceDetector; - bool _isDetecting = false; - bool _canStartCall = false; - - String emotionLabel = "Detecting..."; - String emoji = ""; - - @override - void initState() { - super.initState(); - _initializeCamera(); - } - - Future _initializeCamera() async { - await Permission.camera.request(); - final cameras = await availableCameras(); - final front = cameras.firstWhere( - (c) => c.lensDirection == CameraLensDirection.front, - orElse: () => cameras.first, - ); - - _controller = CameraController(front, ResolutionPreset.medium, enableAudio: false); - await _controller!.initialize(); - await _controller!.startImageStream(_detectEmotion); - - _faceDetector = GoogleMlKit.vision.faceDetector( - FaceDetectorOptions(enableClassification: true, performanceMode: FaceDetectorMode.accurate), - ); - - setState(() {}); - } - - Future _detectEmotion(CameraImage image) async { - if (_isDetecting || _faceDetector == null) return; - _isDetecting = true; - - try { - final WriteBuffer buffer = WriteBuffer(); - for (final plane in image.planes) { - buffer.putUint8List(plane.bytes); - } - final bytes = buffer.done().buffer.asUint8List(); - - final inputImage = InputImage.fromBytes( - bytes: bytes, - metadata: InputImageMetadata( - size: Size(image.width.toDouble(), image.height.toDouble()), - rotation: InputImageRotation.rotation0deg, - format: InputImageFormat.nv21, - bytesPerRow: image.planes[0].bytesPerRow, - ), - ); - - final faces = await _faceDetector!.processImage(inputImage); - - if (faces.isNotEmpty) { - final smile = faces.first.smilingProbability ?? 0; - if (smile > 0.8) { - emotionLabel = "Happy"; - emoji = "😄"; - } else if (smile > 0.3) { - emotionLabel = "Neutral"; - emoji = "🙂"; - } else { - emotionLabel = "Sad"; - emoji = "☹️"; - } - _canStartCall = true; - } else { - emotionLabel = "No face detected"; - emoji = "❓"; - _canStartCall = false; - } - - setState(() {}); - } finally { - _isDetecting = false; - } - } - - @override - void dispose() { - _controller?.dispose(); - _faceDetector?.close(); - super.dispose(); - } - - void _goToCall() { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (_) => PatientCallScreen(patientName: widget.patientName), - ), - ); - } - - @override - Widget build(BuildContext context) { - final isReady = _controller != null && _controller!.value.isInitialized; - - return Scaffold( - appBar: AppBar(title: const Text("Emotion Check-In")), - body: Column( - children: [ - if (isReady) - AspectRatio( - aspectRatio: _controller!.value.aspectRatio, - child: CameraPreview(_controller!), - ) - else - const Expanded(child: Center(child: CircularProgressIndicator())), - const SizedBox(height: 16), - Text("Emotion: $emotionLabel", style: const TextStyle(fontSize: 20)), - Text(emoji, style: const TextStyle(fontSize: 42)), - const Spacer(), - if (_canStartCall) - Padding( - padding: const EdgeInsets.only(bottom: 32), - child: ElevatedButton.icon( - icon: const Icon(Icons.video_call), - label: const Text("Start Video Call"), - onPressed: _goToCall, - ), - ), - ], - ), - ); - } -} -*/ diff --git a/careconnect2025/frontend/lib/features/dashboard/presentation/pages/patient_dashboard.dart b/careconnect2025/frontend/lib/features/dashboard/presentation/pages/patient_dashboard.dart index c0571de8..761be5f8 100644 --- a/careconnect2025/frontend/lib/features/dashboard/presentation/pages/patient_dashboard.dart +++ b/careconnect2025/frontend/lib/features/dashboard/presentation/pages/patient_dashboard.dart @@ -572,6 +572,20 @@ class _PatientDashboardState extends State { locationStatus = 'Location shared and alert sent!'; }); + // Hide the status after 2 seconds + Future.delayed( + const Duration( + seconds: 2, + ), + () { + if (mounted) { + setState(() { + showLocation = + false; + }); + } + }, + ); }, ), if (showLocation) diff --git a/careconnect2025/frontend/lib/features/dashboard/presentation/patient_call.dart b/careconnect2025/frontend/lib/features/dashboard/presentation/patient_call.dart deleted file mode 100644 index d6f6d2ff..00000000 --- a/careconnect2025/frontend/lib/features/dashboard/presentation/patient_call.dart +++ /dev/null @@ -1,289 +0,0 @@ - - // Below code is without the emotion detection: It is just the regular video/call - import 'package:flutter/material.dart'; - import 'package:jitsi_meet_flutter_sdk/jitsi_meet_flutter_sdk.dart'; - import 'package:permission_handler/permission_handler.dart'; - - class CallScreen extends StatefulWidget { - final String userName; - final String roomCode; - - const CallScreen({super.key, required this.userName, required this.roomCode}); - - @override - State createState() => _CallScreenState(); - } - - class _CallScreenState extends State { - final JitsiMeet _jitsiMeet = JitsiMeet(); - - @override - void initState() { - super.initState(); - _initiateVideoCall(); - } - - Future _initiateVideoCall() async { - final permissions = await [ - Permission.camera, - Permission.microphone, - ].request(); - - if (permissions[Permission.camera]!.isGranted && - permissions[Permission.microphone]!.isGranted) { - final options = JitsiMeetConferenceOptions( - room: widget.roomCode, - serverURL: "https://meet.jit.si", - userInfo: JitsiMeetUserInfo(displayName: widget.userName), - configOverrides: { - "startWithVideoMuted": false, - "startWithAudioMuted": false, - }, - featureFlags: { - "welcomepage.enabled": false, - "pip.enabled": false, - }, - ); - - try { - await _jitsiMeet.join(options); - debugPrint("📞 Joined video call successfully"); - } catch (error) { - debugPrint("❌ Failed to join video call: $error"); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Camera and Microphone permissions are required.")), - ); - Navigator.pop(context); - } - } - - @override - void dispose() { - _jitsiMeet.hangUp(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("In Call: ${widget.userName}"), - actions: [ - IconButton( - onPressed: () { - _jitsiMeet.hangUp(); - Navigator.pop(context); - }, - icon: const Icon(Icons.call_end, color: Colors.red), - ), - ], - ), - body: const Center(child: Text("Connecting to video call...")), - ); - } - } - - - - /* -// Look this for the emotion detection: not opening video/call -import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:camera/camera.dart'; -import 'package:google_ml_kit/google_ml_kit.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:jitsi_meet_flutter_sdk/jitsi_meet_flutter_sdk.dart'; - -class PatientEmotionCallScreen extends StatefulWidget { - final String patientName; - - const PatientEmotionCallScreen({super.key, required this.patientName}); - - @override - State createState() => _PatientEmotionCallScreenState(); -} - -class _PatientEmotionCallScreenState extends State { - CameraController? _cameraController; - FaceDetector? _faceDetector; - bool _isDetecting = false; - bool _canStartCall = false; - bool _inCall = false; - - String emotionLabel = "Detecting..."; - String emoji = ""; - - final JitsiMeet _jitsi = JitsiMeet(); - late String roomName; - - @override - void initState() { - super.initState(); - roomName = "careconnect_${widget.patientName.replaceAll(' ', '_')}"; - _initializeCamera(); - } - - Future _initializeCamera() async { - await Permission.camera.request(); - final cameras = await availableCameras(); - final frontCamera = cameras.firstWhere( - (c) => c.lensDirection == CameraLensDirection.front, - orElse: () => cameras.first, - ); - - _cameraController = CameraController(frontCamera, ResolutionPreset.medium, enableAudio: false); - await _cameraController!.initialize(); - await _cameraController!.startImageStream(_detectEmotion); - - _faceDetector = GoogleMlKit.vision.faceDetector( - FaceDetectorOptions(enableClassification: true, performanceMode: FaceDetectorMode.accurate), - ); - - setState(() {}); - } - - Future _detectEmotion(CameraImage image) async { - if (_isDetecting || _faceDetector == null || _inCall) return; - _isDetecting = true; - - try { - final WriteBuffer buffer = WriteBuffer(); - for (final plane in image.planes) { - buffer.putUint8List(plane.bytes); - } - final bytes = buffer.done().buffer.asUint8List(); - - final inputImage = InputImage.fromBytes( - bytes: bytes, - metadata: InputImageMetadata( - size: Size(image.width.toDouble(), image.height.toDouble()), - rotation: InputImageRotation.rotation0deg, - format: InputImageFormat.nv21, - bytesPerRow: image.planes[0].bytesPerRow, - ), - ); - - final faces = await _faceDetector!.processImage(inputImage); - - if (faces.isNotEmpty) { - final smile = faces.first.smilingProbability ?? 0; - if (smile > 0.8) { - emotionLabel = "Happy"; - emoji = "😄"; - } else if (smile > 0.3) { - emotionLabel = "Neutral"; - emoji = "🙂"; - } else { - emotionLabel = "Sad"; - emoji = "☹️"; - } - _canStartCall = true; - } else { - emotionLabel = "No face detected"; - emoji = "❓"; - _canStartCall = false; - } - - setState(() {}); - } finally { - _isDetecting = false; - } - } - - Future _startVideoCall() async { - await Permission.microphone.request(); - - if (!await Permission.microphone.isGranted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Microphone permission is required")), - ); - return; - } - - try { - final options = JitsiMeetConferenceOptions( - room: roomName, - serverURL: "https://meet.jit.si", - userInfo: JitsiMeetUserInfo(displayName: widget.patientName), - configOverrides: { - "startWithAudioMuted": false, - "startWithVideoMuted": false, - }, - featureFlags: { - "welcomepage.enabled": false, - "call-integration.enabled": false, - "pip.enabled": false, - }, - ); - - setState(() => _inCall = true); - await _cameraController?.dispose(); - await _jitsi.join(options); - } catch (e) { - debugPrint("Call Error: $e"); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Call failed: $e"))); - } - } - - @override - void dispose() { - _cameraController?.dispose(); - _faceDetector?.close(); - _jitsi.hangUp(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isReady = _cameraController != null && _cameraController!.value.isInitialized; - - return Scaffold( - appBar: AppBar( - title: Text("Patient: ${widget.patientName}"), - actions: _inCall - ? [ - IconButton( - icon: const Icon(Icons.call_end, color: Colors.red), - onPressed: () { - _jitsi.hangUp(); - setState(() => _inCall = false); - }, - ), - ] - : [], - ), - body: Column( - children: [ - if (!_inCall && isReady) - AspectRatio( - aspectRatio: _cameraController!.value.aspectRatio, - child: CameraPreview(_cameraController!), - ) - else if (!_inCall) - const Expanded(child: Center(child: CircularProgressIndicator())), - if (!_inCall) ...[ - const SizedBox(height: 16), - Text("Emotion: $emotionLabel", style: const TextStyle(fontSize: 20)), - Text(emoji, style: const TextStyle(fontSize: 42)), - const Spacer(), - if (_canStartCall) - Padding( - padding: const EdgeInsets.only(bottom: 32), - child: ElevatedButton.icon( - icon: const Icon(Icons.video_call), - label: const Text("Start Video Call"), - onPressed: _startVideoCall, - ), - ), - ] - ], - ), - ); - } -} -*/ - - diff --git a/careconnect2025/frontend/lib/features/profile/presentation/pages/profile_settings_page.dart b/careconnect2025/frontend/lib/features/profile/presentation/pages/profile_settings_page.dart index 08b09263..c41ada44 100644 --- a/careconnect2025/frontend/lib/features/profile/presentation/pages/profile_settings_page.dart +++ b/careconnect2025/frontend/lib/features/profile/presentation/pages/profile_settings_page.dart @@ -168,6 +168,8 @@ class _ProfileSettingsPageState extends State { // Transform the API response structure to match our model expectations if (_isCaregiver) { + // Defensive extraction of professional info fields + final professional = rawData['professional'] ?? {}; return { 'id': rawData['id'] ?? 0, 'name': '${rawData['firstName'] ?? ''} ${rawData['lastName'] ?? ''}' @@ -179,17 +181,17 @@ class _ProfileSettingsPageState extends State { 'state': rawData['address']?['state'] ?? '', 'zipCode': rawData['address']?['zip'] ?? '', 'country': '', // Default to empty as it's not in the response - // Extract specialization from the professional object - use yearsExperience as a string - 'specialization': rawData['professional'] != null - ? rawData['professional']['yearsExperience']?.toString() ?? '' - : '', - // Use caregiverType for organization if available - 'organization': rawData['caregiverType'] ?? '', - // Use license number from the professional object if available - 'license': rawData['professional'] != null - ? rawData['professional']['licenseNumber'] ?? '' - : '', - 'dateOfBirth': rawData['dob'] ?? '', // Added date of birth handling + // Professional info fields + 'specialization': + professional['specialization'] ?? + professional['specialty'] ?? + professional['yearsExperience']?.toString() ?? + '', + 'organization': + rawData['caregiverType'] ?? professional['organization'] ?? '', + 'license': + professional['licenseNumber'] ?? professional['license'] ?? '', + 'dateOfBirth': rawData['dob'] ?? '', 'profilePictureUrl': rawData['profileImageUrl'] ?? rawData['profilePictureUrl'], }; @@ -237,11 +239,10 @@ class _ProfileSettingsPageState extends State { _zipCodeController.text = profile.zipCode ?? ''; _countryController.text = profile.country ?? ''; - _specializationController.text = profile.specialization ?? ''; - _organizationController.text = profile.organization ?? ''; - _licenseController.text = profile.license ?? ''; - _dateOfBirthController.text = - profile.dateOfBirth ?? ''; // Added date of birth handling + _specializationController.text = (profile.specialization ?? '').trim(); + _organizationController.text = (profile.organization ?? '').trim(); + _licenseController.text = (profile.license ?? '').trim(); + _dateOfBirthController.text = (profile.dateOfBirth ?? '').trim(); } void _populatePatientFields() { diff --git a/careconnect2025/frontend/lib/firebase_options.dart b/careconnect2025/frontend/lib/firebase_options.dart deleted file mode 100644 index 56908759..00000000 --- a/careconnect2025/frontend/lib/firebase_options.dart +++ /dev/null @@ -1,72 +0,0 @@ -// File generated by FlutterFire CLI. -// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members -import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; - -/// Default [FirebaseOptions] for use with CareConnect app. -class DefaultFirebaseOptions { - static FirebaseOptions get currentPlatform { - if (kIsWeb) { - return web; - } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - return macos; - case TargetPlatform.windows: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for windows - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); - } - } - - static const FirebaseOptions web = FirebaseOptions( - apiKey: 'AIzaSyDSZfDwvL4ZYRkEddUyP4adRyvnEMvRvvQ', - appId: '1:1070028273529:web:d88c7e7069e88454ffa1a8', - messagingSenderId: '1070028273529', - projectId: 'careconnectptdemo', - authDomain: 'careconnectptdemo.firebaseapp.com', - storageBucket: 'careconnectptdemo.firebasestorage.app', - measurementId: 'G-XXXXXXXXXX', - ); - - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyBN7XCaESMDhKwjHQQt8UKQ4-wm4jgP2Sg', - appId: '1:1070028273529:android:8ecaa6c5160ab941ffa1a8', - messagingSenderId: '1070028273529', - projectId: 'careconnectptdemo', - storageBucket: 'careconnectptdemo.firebasestorage.app', - ); - - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyDSZfDwvL4ZYRkEddUyP4adRyvnEMvRvvQ', - appId: '1:1070028273529:ios:d88c7e7069e88454ffa1a8', - messagingSenderId: '1070028273529', - projectId: 'careconnectptdemo', - storageBucket: 'careconnectptdemo.firebasestorage.app', - iosBundleId: 'com.example.careConnectApp', - ); - - static const FirebaseOptions macos = FirebaseOptions( - apiKey: 'AIzaSyDSZfDwvL4ZYRkEddUyP4adRyvnEMvRvvQ', - appId: '1:1070028273529:ios:d88c7e7069e88454ffa1a8', - messagingSenderId: '1070028273529', - projectId: 'careconnectptdemo', - storageBucket: 'careconnectptdemo.firebasestorage.app', - iosBundleId: 'com.example.careConnectApp', - ); -} diff --git a/careconnect2025/frontend/lib/fix_imports.dart b/careconnect2025/frontend/lib/fix_imports.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/careconnect2025/frontend/lib/fix_imports_final.dart b/careconnect2025/frontend/lib/fix_imports_final.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/careconnect2025/frontend/lib/main.dart b/careconnect2025/frontend/lib/main.dart index 095d3bdb..912d4315 100644 --- a/careconnect2025/frontend/lib/main.dart +++ b/careconnect2025/frontend/lib/main.dart @@ -6,77 +6,146 @@ import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:app_links/app_links.dart'; import 'dart:async'; import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'firebase_options.dart'; import 'providers/user_provider.dart'; import 'providers/theme_provider.dart'; import 'config/router/app_router.dart'; import 'services/auth_migration_helper.dart'; import 'services/messaging_service.dart'; import 'services/video_call_service.dart'; +import 'services/messaging_service.dart'; import 'config/theme/app_theme.dart'; import 'config/utils/responsive_utils.dart'; import 'config/utils/web_utils.dart'; -// Background message handler for Firebase -@pragma('vm:entry-point') -Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - print("Handling a background message: ${message.messageId}"); -} - Future main() async { WidgetsFlutterBinding.ensureInitialized(); - // Only do critical initialization synchronously - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); - - // Performance optimization: Set preferred orientations - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); - - // Configure URL strategy for web to remove hash from URLs - usePathUrlStrategy(); - - // Load environment quickly - await dotenv.load(); - - // Create providers (don't initialize them yet) - final userProvider = UserProvider(); - final themeProvider = ThemeProvider(); - - // Start the app immediately, initialize services in background - runApp( - MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: userProvider), - ChangeNotifierProvider.value(value: themeProvider), - ], - child: const CareConnectApp(), - ), + // Global error handling for Flutter errors + FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.presentError(details); + // Optionally log to a remote server + debugPrint( + 'FlutterError: \\n${details.exceptionAsString()}\\n${details.stack}', + ); + }; + + // Global error handling for unhandled Dart errors + runZonedGuarded( + () async { + // Performance optimization: Set preferred orientations + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + + // Configure URL strategy for web to remove hash from URLs + usePathUrlStrategy(); + + // Load environment quickly + await dotenv.load(); + + // Create providers (don't initialize them yet) + final userProvider = UserProvider(); + final themeProvider = ThemeProvider(); + + // Start the app immediately, initialize services in background + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: userProvider), + ChangeNotifierProvider.value(value: themeProvider), + ], + child: CareConnectAppWithErrorBoundary(), + ), + ); + + // Initialize heavy services in background after app starts + _initializeServicesInBackground(userProvider); + }, + (error, stack) { + debugPrint('Uncaught error: $error\\n$stack'); + // Optionally log to a remote server + }, ); +} + +/// Top-level error boundary widget for global error UI +class CareConnectAppWithErrorBoundary extends StatefulWidget { + @override + State createState() => + _CareConnectAppWithErrorBoundaryState(); +} - // Initialize heavy services in background after app starts - _initializeServicesInBackground(userProvider); +class _CareConnectAppWithErrorBoundaryState + extends State { + @override + void initState() { + super.initState(); + ErrorWidget.builder = (FlutterErrorDetails details) { + // You can customize this error UI as needed + return MaterialApp( + home: Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error, color: Colors.red, size: 64), + const SizedBox(height: 16), + const Text( + 'Something went wrong', + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + details.exceptionAsString(), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + // Try to restart the app by popping all routes + runApp(CareConnectAppWithErrorBoundary()); + }, + child: const Text('Restart App'), + ), + ], + ), + ), + ), + ), + ); + }; + } + + @override + Widget build(BuildContext context) { + return const CareConnectApp(); + } } // Background initialization to not block app startup Future _initializeServicesInBackground(UserProvider userProvider) async { try { - // Run these in parallel for faster initialization - await Future.wait([ - _bootstrap(), - MessagingService.initialize(), - VideoCallService.initializeService(), - userProvider.initializeUser(), - _handleAuthMigration(), - ], eagerError: false); // Don't stop if one fails + await _bootstrap(); + await userProvider.initializeUser(); + await VideoCallService.initializeService(); + await _handleAuthMigration(); + + // Only establish the WebSocket connection at this stage + try { + await MessagingService.initialize(); + print( + '✅ MessagingService WebSocket connection established (user not registered yet)', + ); + } catch (e) { + print('⚠️ MessagingService WebSocket connection failed: $e'); + // Do not rethrow, app should continue running + } print('✅ Background services initialized'); } catch (e) { @@ -164,7 +233,9 @@ class _CareConnectAppState extends State { @override void dispose() { - _linkSubscription?.cancel(); + if (_linkSubscription != null) { + _linkSubscription!.cancel(); + } super.dispose(); } diff --git a/careconnect2025/frontend/lib/pages/ai_configuration_page.dart b/careconnect2025/frontend/lib/pages/ai_configuration_page.dart index 6464581a..e59320f1 100644 --- a/careconnect2025/frontend/lib/pages/ai_configuration_page.dart +++ b/careconnect2025/frontend/lib/pages/ai_configuration_page.dart @@ -46,7 +46,34 @@ class _AIConfigurationPageState extends State { return Scaffold( appBar: AppBar( title: const Text('AI Configuration'), - // ...existing code... + actions: [ + TextButton( + onPressed: (_isLoading || _isSaving) + ? null + : () { + // Discard changes and navigate back + context.pop(); + }, + child: const Text('Cancel', style: TextStyle(color: Colors.white)), + ), + TextButton( + onPressed: (_isLoading || _isSaving) + ? null + : () async { + await _saveConfiguration(); + }, + child: _isSaving + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('Save', style: TextStyle(color: Colors.white)), + ), + ], ), drawer: const CommonDrawer(currentRoute: '/ai-configuration'), body: _isLoading @@ -94,33 +121,28 @@ class _AIConfigurationPageState extends State { Future _loadConfiguration() async { try { - final userProvider = Provider.of(context, listen: false); - final userId = userProvider.user?.id; + final config = await AIConfigService.getUserAIConfig(context); + if (config != null) { + setState(() { + _currentConfig = config; + _selectedProvider = config.aiProvider; + _personality = config.personalityStyle; + _contextMemoryEnabled = config.contextMemoryEnabled; + _medicalContextEnabled = config.medicalContextEnabled; + _emergencyDetection = config.emergencyAlertsEnabled; + _maxTokens = config.maxTokensPerSession; + _temperature = config.temperature; + _language = config.language; - if (userId != null) { - final config = await AIConfigService.getPatientAIConfig(userId); - if (config != null) { - setState(() { - _currentConfig = config; - _selectedProvider = config.aiProvider; - _personality = config.personalityStyle; - _contextMemoryEnabled = config.contextMemoryEnabled; - _medicalContextEnabled = config.medicalContextEnabled; - _emergencyDetection = config.emergencyAlertsEnabled; - _maxTokens = config.maxTokensPerSession; - _temperature = config.temperature; - _language = config.language; - - // Map enabled features to UI switches - _voiceEnabled = config.enabledFeatures.contains('voice'); - _emotionalSupport = config.enabledFeatures.contains( - 'emotional_support', - ); - _medicationReminders = config.enabledFeatures.contains( - 'medication_reminders', - ); - }); - } + // Map enabled features to UI switches (ensure keys match backend DTO) + _voiceEnabled = config.enabledFeatures.contains('general_chat'); + _emotionalSupport = config.enabledFeatures.contains( + 'mental_health_support', + ); + _medicationReminders = config.enabledFeatures.contains( + 'medication_reminders', + ); + }); } } catch (e) { if (mounted) { @@ -143,51 +165,53 @@ class _AIConfigurationPageState extends State { try { final userProvider = Provider.of(context, listen: false); - final userId = userProvider.user?.id; + final user = userProvider.user; + if (user == null) throw Exception('User not found'); - if (userId != null) { - // Build enabled features list - List enabledFeatures = []; - if (_voiceEnabled) enabledFeatures.add('voice'); - if (_emotionalSupport) enabledFeatures.add('emotional_support'); - if (_medicationReminders) enabledFeatures.add('medication_reminders'); + // Build enabled features list from current UI state (use backend keys) + List enabledFeatures = []; + if (_voiceEnabled) enabledFeatures.add('general_chat'); + if (_emotionalSupport) enabledFeatures.add('mental_health_support'); + if (_medicationReminders) enabledFeatures.add('medication_reminders'); - final config = PatientAIConfigDTO( - id: _currentConfig?.id, - patientId: userId, - aiProvider: _selectedProvider, - preferences: { - 'language': _language, - 'voice_enabled': _voiceEnabled, - 'emotional_support': _emotionalSupport, - 'medication_reminders': _medicationReminders, - }, - enabledFeatures: enabledFeatures, - maxTokensPerSession: _maxTokens, - temperature: _temperature, - personalityStyle: _personality, - contextMemoryEnabled: _contextMemoryEnabled, - medicalContextEnabled: _medicalContextEnabled, - language: _language, - emergencyAlertsEnabled: _emergencyDetection, - ); - - final result = await AIConfigService.savePatientAIConfig(config); + final config = PatientAIConfigDTO( + id: _currentConfig?.id, + patientId: user.id, // keep for compatibility, not used in logic + aiProvider: _selectedProvider, + preferences: { + 'language': _language, + 'voice_enabled': _voiceEnabled, + 'emotional_support': _emotionalSupport, + 'medication_reminders': _medicationReminders, + }, + enabledFeatures: enabledFeatures, + maxTokensPerSession: _maxTokens, + temperature: _temperature, + personalityStyle: _personality, + contextMemoryEnabled: _contextMemoryEnabled, + medicalContextEnabled: _medicalContextEnabled, + language: _language, + emergencyAlertsEnabled: _emergencyDetection, + ); - if (result != null) { - setState(() => _currentConfig = result); + // Use AIConfigService to update config + final savedConfig = await AIConfigService.saveUserAIConfig( + config, + userId: user.id, + ); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('AI configuration saved successfully!'), - backgroundColor: Theme.of(context).colorScheme.primary, - ), - ); - } - } else { - throw Exception('Failed to save configuration'); + if (savedConfig != null) { + setState(() => _currentConfig = savedConfig); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('AI configuration saved successfully!'), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + ); } + } else { + throw Exception('Failed to save configuration'); } } catch (e) { if (mounted) { diff --git a/careconnect2025/frontend/lib/pages/file_management_page.dart b/careconnect2025/frontend/lib/pages/file_management_page.dart index ee29d370..231b9f06 100644 --- a/careconnect2025/frontend/lib/pages/file_management_page.dart +++ b/careconnect2025/frontend/lib/pages/file_management_page.dart @@ -1,7 +1,10 @@ -import 'dart:io'; +import 'dart:typed_data'; + import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import '../core/services/api_service.dart'; +import '../services/api_service.dart' as ApiService; +import '../services/auth_token_manager.dart'; import '../services/comprehensive_file_service.dart'; import '../services/enhanced_file_service.dart'; import '../providers/user_provider.dart'; @@ -28,7 +31,8 @@ class _FileManagementPageState extends State String _searchQuery = ''; FileCategory? _selectedCategory; final TextEditingController _searchController = TextEditingController(); - int? _userId; + bool _isPatient = false; + bool _isCaregiver = false; @override void initState() { @@ -62,11 +66,7 @@ class _FileManagementPageState extends State setState(() { _allFiles = files; _filteredFiles = files; - _userId = user.id; _isLoading = false; - print( - 'DEBUG: Category set as: $_selectedCategory, Files set as: $files', - ); }); } catch (e) { setState(() { @@ -111,23 +111,52 @@ class _FileManagementPageState extends State Future.microtask(() => Navigator.pushReplacementNamed(context, '/login')); return const Scaffold(body: Center(child: CircularProgressIndicator())); } - final isCaregiver = user.role.toUpperCase() == 'CAREGIVER'; + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; return Scaffold( appBar: AppBar( - title: const Text('File Management'), + title: Text( + 'File Management', + style: + theme.textTheme.headlineSmall?.copyWith( + color: AppTheme.textLight, + fontWeight: FontWeight.bold, + fontSize: 22, + ) ?? + TextStyle( + color: AppTheme.textLight, + fontWeight: FontWeight.bold, + fontSize: 22, + ), + ), + backgroundColor: AppTheme.primary, + foregroundColor: AppTheme.textLight, + iconTheme: IconThemeData(color: AppTheme.textLight), + elevation: 2, bottom: TabBar( controller: _tabController, + labelColor: AppTheme.textLight, + unselectedLabelColor: AppTheme.textLight.withOpacity(0.7), + indicatorColor: AppTheme.accent, tabs: const [ - Tab(icon: Icon(Icons.folder), text: 'My Files'), - Tab(icon: Icon(Icons.cloud_upload), text: 'Upload'), - Tab(icon: Icon(Icons.analytics), text: 'Analytics'), + Tab( + icon: Icon(Icons.folder, color: AppTheme.textLight), + text: 'My Files', + ), + Tab( + icon: Icon(Icons.analytics, color: AppTheme.textLight), + text: 'Analytics', + ), ], ), ), drawer: const CommonDrawer(currentRoute: '/file-management'), - body: TabBarView( - controller: _tabController, - children: [_buildFilesTab(), _buildUploadTab(), _buildAnalyticsTab()], + body: Container( + color: AppTheme.backgroundSecondary, + child: TabBarView( + controller: _tabController, + children: [_buildFilesTab(), _buildAnalyticsTab()], + ), ), ); } @@ -136,6 +165,75 @@ class _FileManagementPageState extends State return Column( children: [ _buildSearchAndFilter(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + icon: const Icon(Icons.cloud_upload), + label: const Text('Upload File'), + style: AppTheme.primaryButtonStyle, + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (context) { + final mq = MediaQuery.of(context); + return Padding( + padding: EdgeInsets.only( + bottom: mq.viewInsets.bottom, + top: 24, + left: 16, + right: 16, + ), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 500, + maxHeight: mq.size.height * 0.85, + ), + child: SingleChildScrollView( + child: FileUploadWidget( + onUploadSuccess: (response) { + if (mounted) { + Navigator.of(context).maybePop(); + _loadFiles(); + ScaffoldMessenger.of(this.context).showSnackBar( + SnackBar( + content: Text( + 'File uploaded: ${response.originalFilename}', + ), + backgroundColor: AppTheme.success, + ), + ); + } + }, + onUploadError: (error) { + if (mounted) { + ScaffoldMessenger.of(this.context).showSnackBar( + SnackBar( + content: Text(error), + backgroundColor: AppTheme.error, + ), + ); + } + }, + showCategorySelector: true, + customTitle: 'Upload File', + ), + ), + ), + ); + }, + ); + }, + ), + ), + ), Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) @@ -263,15 +361,7 @@ class _FileManagementPageState extends State ), textAlign: TextAlign.center, ), - if (_searchQuery.isEmpty && _selectedCategory == null) ...[ - const SizedBox(height: 24), - ElevatedButton.icon( - onPressed: () => _tabController.animateTo(1), - style: AppTheme.primaryButtonStyle, - icon: const Icon(Icons.cloud_upload), - label: const Text('Upload Files'), - ), - ], + // Removed upload button from My Files tab ], ), ); @@ -292,30 +382,6 @@ class _FileManagementPageState extends State } Widget _buildFileCard(UserFileDTO file) { - // File Name Only - // Get index of last dot - int dotIndex = file.fileName.lastIndexOf('.'); - - // Extract filename without extension - String baseName = (dotIndex != -1) - ? file.fileName.substring(0, dotIndex) - : file.fileName; // If no dot found, return full filename - - // Extension Name Only - String getFileExtension(String fileName) { - int dotIndex = fileName.lastIndexOf('.'); - if (dotIndex != -1 && dotIndex != fileName.length - 1) { - return fileName.substring(dotIndex); - } - return ''; // No extension found - } - - // Usage: - String fileExtension = getFileExtension(file.fileName); - String extensionWithoutDot = fileExtension.replaceFirst('.', ''); // txt - - - final theme = Theme.of(context); return Card( margin: const EdgeInsets.only(bottom: 12), @@ -330,7 +396,7 @@ class _FileManagementPageState extends State ), ), title: Text( - baseName, + file.originalFilename, style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, @@ -355,9 +421,9 @@ class _FileManagementPageState extends State ), Text(' • ', style: theme.textTheme.bodyMedium), Text( - extensionWithoutDot, - style: theme.textTheme.bodyMedium, - ) + _formatDate(file.createdAt), + style: theme.textTheme.bodyMedium, + ), ], ), if (file.description != null && file.description!.isNotEmpty) ...[ @@ -372,23 +438,7 @@ class _FileManagementPageState extends State ], ), trailing: PopupMenuButton( - onSelected: (value) async { - switch (value) { - case 'download': - // TODO: Implement download functionality - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Download file feature coming soon.'), backgroundColor: AppTheme.info,), - ); - break; - case 'delete': - // TODO: Implement delete functionality - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Delete file feature coming soon.'), backgroundColor: AppTheme.info,), - ); - break; - } - - }, + onSelected: (String action) => _handleFileAction(action, file), itemBuilder: (BuildContext context) => [ PopupMenuItem( value: 'download', @@ -407,6 +457,14 @@ class _FileManagementPageState extends State contentPadding: EdgeInsets.zero, ), ), + PopupMenuItem( + value: 'info', + child: ListTile( + leading: Icon(Icons.info, color: theme.iconTheme.color), + title: Text('Info', style: theme.textTheme.bodyMedium), + contentPadding: EdgeInsets.zero, + ), + ), PopupMenuItem( value: 'delete', child: ListTile( @@ -450,7 +508,7 @@ class _FileManagementPageState extends State const SizedBox(width: 12), Expanded( child: Text( - 'Use File Upload for files, Manual Text Entry for notes, or Speech-to-Text to convert voice into text files.', + 'Use Quick Upload for fast uploads, Manual Text Entry for notes, or Speech-to-Text to convert voice into text files.', style: Theme.of(context).textTheme.bodyLarge, ), ), @@ -459,10 +517,30 @@ class _FileManagementPageState extends State ), ), const SizedBox(height: 24), + // Quick Upload Section + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: QuickUploadButtons( + onUploadSuccess: (response) { + _loadFiles(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'File uploaded: ${response.originalFilename}', + ), + backgroundColor: Theme.of( + context, + ).colorScheme.secondary, + ), + ); + }, + ), + ), + ), + const SizedBox(height: 24), // File Upload Section FileUploadWidget( - patientId: _userId, - allowedCategories: FileCategory.values, onUploadSuccess: (response) { _loadFiles(); // Refresh the files list }, @@ -481,18 +559,18 @@ class _FileManagementPageState extends State child: Padding( padding: const EdgeInsets.all(16), child: ManualTextEntryCard( - patientId: _userId, - onUploadSuccess: (response) { - _loadFiles(); // Refresh the files list - }, - onUploadError: (error) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(error), - backgroundColor: Theme.of(context).colorScheme.error, - ), - ); - } + onSave: (fileName, fileBytes) async { + // await _uploadManualTextFile(fileName, fileBytes); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Manual entry uploaded: $fileName.txt'), + backgroundColor: Theme.of( + context, + ).colorScheme.secondary, + ), + ); + _loadFiles(); + }, ), ), ), @@ -503,18 +581,16 @@ class _FileManagementPageState extends State child: Padding( padding: const EdgeInsets.all(16), child: SpeechToTextCard( - patientId: _userId, - onUploadSuccess: (response) { - _loadFiles(); // Refresh the files list - }, - onUploadError: (error) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(error), - backgroundColor: Theme.of(context).colorScheme.error, - ), - ); - } + onSave: (fileName, fileBytes) async { + // await _saveRecognizedTextFile(fileName, fileBytes); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Speech-to-text file saved'), + backgroundColor: Colors.green, + ), + ); + _loadFiles(); + }, ), ), ), @@ -638,42 +714,20 @@ class _FileManagementPageState extends State try { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Downloading ${file.fileName}...'), + content: Text('Downloading ${file.originalFilename}...'), duration: const Duration(seconds: 2), ), ); - final userProvider = Provider.of(context, listen: false); - final user = userProvider.user; - if (user == null) return; - - final fileData = await EnhancedFileService.downloadFileLegacy(user.id, file.fileUrl!); - + final fileData = await EnhancedFileService.downloadFile(file.id); if (fileData != null) { - // 1. Get device's Download directory - Directory? directory; - if (Platform.isAndroid || Platform.isIOS) { - directory = await getApplicationDocumentsDirectory(); // App-local storage - } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { - directory = await getDownloadsDirectory(); // User's Downloads folder - } - - if (directory != null) { - final filePath = '${directory.path}/${file.fileName}'; - final newFile = File(filePath); - - // 2. Write bytes to file - await newFile.writeAsBytes(fileData); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('File saved to ${newFile.path}'), - backgroundColor: AppTheme.success, - ), - ); - } else { - throw Exception('Could not access device storage'); - } + // In a real app, you'd save the file to the device + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Downloaded ${file.originalFilename}'), + backgroundColor: AppTheme.success, + ), + ); } else { throw Exception('Failed to download file'); } @@ -705,8 +759,6 @@ class _FileManagementPageState extends State } void _showFileInfo(UserFileDTO file) { - String extensionWithoutDot = file.fileName.replaceFirst('.', ''); // txt - showDialog( context: context, builder: (context) => AlertDialog( @@ -718,10 +770,9 @@ class _FileManagementPageState extends State children: [ _buildInfoRow('Category', file.categoryDisplayName), _buildInfoRow('Size', _formatFileSize(file.fileSize)), - _buildInfoRow('Type', extensionWithoutDot), - // Comment out created and updated date as they are not passed in from the API for now. - // _buildInfoRow('Created', _formatDate(file.createdAt)), - // _buildInfoRow('Updated', _formatDate(file.updatedAt)), + _buildInfoRow('Type', file.contentType), + _buildInfoRow('Created', _formatDate(file.createdAt)), + _buildInfoRow('Updated', _formatDate(file.updatedAt)), if (file.description != null && file.description!.isNotEmpty) _buildInfoRow('Description', file.description!), ], @@ -811,4 +862,90 @@ class _FileManagementPageState extends State String _formatDate(DateTime date) { return '${date.day}/${date.month}/${date.year}'; } -} \ No newline at end of file +} + +/* +Future _uploadManualTextFile(String fileName, List fileBytes) async { + final userSession = await AuthTokenManager.getUserSession(); + if (userSession == null || userSession['id'] == null) { + throw Exception('User session not found'); + } + + final userRole = userSession['role'] as String? ?? ''; + + setState(() { + _isPatient = userRole.toUpperCase() == 'PATIENT'; + _isCaregiver = + userRole.toUpperCase() == 'CAREGIVER' || + userRole.toUpperCase() == 'FAMILY_LINK' || + userRole.toUpperCase() == 'ADMIN'; + }); + + try { + final userSession = await AuthTokenManager.getUserSession(); + int? profileId; + + if (_isPatient) { + profileId = userSession?['id'] as int?; + } else if (_isCaregiver) { + profileId = widget.patientUserId; + } + + if (profileId == null) { + throw Exception("Profile ID not found for the current user role"); + } + + final response = await ApiService.uploadUserFileFromBytes( + userId: profileId, + fileBytes: Uint8List.fromList(fileBytes), + fileName: '$fileName.txt', + category: 'manualTextEntry', + role: _isPatient ? 'PATIENT' : 'CAREGIVER', + ); + + if (response.statusCode == 200) { + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('File Saved'), + content: Text('File: $fileName.txt has been uploaded successfully.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Close'), + ), + ], + ), + ); + } else { + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Error'), + content: Text('Failed to upload file: $fileName.txt.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Close'), + ), + ], + ), + ); + } + } catch (e) { + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Error'), + content: Text('Error uploading file: $fileName.txt.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Close'), + ), + ], + ), + ); + } + + */ diff --git a/careconnect2025/frontend/lib/pages/profile_page.dart b/careconnect2025/frontend/lib/pages/profile_page.dart index 3ddf9529..2820edf5 100644 --- a/careconnect2025/frontend/lib/pages/profile_page.dart +++ b/careconnect2025/frontend/lib/pages/profile_page.dart @@ -76,8 +76,9 @@ class _ProfilePageState extends State { final user = userProvider.user; if (user != null) { - // Mock profile data for now - replace with actual API call - final profile = { + // TODO: Replace with actual API call for patient or caregiver + // For now, mock address fields for both roles + Map profile = { 'name': user.name, 'email': user.email, 'phone': '', @@ -92,6 +93,26 @@ class _ProfilePageState extends State { 'emergencyContact': '', 'medicalNotes': '', }; + + // Example: If you fetch from getPatient or getCaregiver API, populate address fields here + // if (user.role.toUpperCase() == 'CAREGIVER') { + // final caregiver = await ApiService.getCaregiver(user.id); + // profile['address'] = caregiver.address; + // profile['city'] = caregiver.city; + // profile['state'] = caregiver.state; + // profile['zipCode'] = caregiver.zipCode; + // profile['country'] = caregiver.country; + // // ...other fields + // } else if (user.role.toUpperCase() == 'PATIENT') { + // final patient = await ApiService.getPatient(user.id); + // profile['address'] = patient.address; + // profile['city'] = patient.city; + // profile['state'] = patient.state; + // profile['zipCode'] = patient.zipCode; + // profile['country'] = patient.country; + // // ...other fields + // } + if (mounted) { setState(() { _userProfile = profile; @@ -322,6 +343,27 @@ class _ProfilePageState extends State { final userProvider = Provider.of(context); final user = userProvider.user; + // Fallback avatar logic: show image if picked, else first letter of name, else icon + Widget avatarChild; + if (_imageFile != null) { + avatarChild = const SizedBox.shrink(); + } else if (user != null && user.name != null && user.name!.isNotEmpty) { + avatarChild = Text( + user.name![0].toUpperCase(), + style: TextStyle( + fontSize: 40, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.bold, + ), + ); + } else { + avatarChild = Icon( + Icons.person, + size: 60, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ); + } + return Card( child: Padding( padding: const EdgeInsets.all(24), @@ -336,15 +378,9 @@ class _ProfilePageState extends State { context, ).colorScheme.surfaceContainerHighest, backgroundImage: _imageFile != null - ? FileImage(_imageFile!) as ImageProvider - : null, - child: _imageFile == null - ? Icon( - Icons.person, - size: 60, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ) + ? FileImage(_imageFile!) : null, + child: avatarChild, ), if (_isEditing) Positioned( @@ -379,7 +415,9 @@ class _ProfilePageState extends State { ), const SizedBox(height: 16), Text( - user?.name ?? 'User', + (user != null && user.name != null && user.name!.isNotEmpty) + ? user.name! + : 'User', style: Theme.of( context, ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), @@ -387,7 +425,7 @@ class _ProfilePageState extends State { const SizedBox(height: 4), Chip( label: Text( - user?.role ?? 'USER', + (user != null && user.role.isNotEmpty) ? user.role : 'USER', style: TextStyle( color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.bold, diff --git a/careconnect2025/frontend/lib/pages/settings_page.dart b/careconnect2025/frontend/lib/pages/settings_page.dart index 57466828..2c4abe29 100644 --- a/careconnect2025/frontend/lib/pages/settings_page.dart +++ b/careconnect2025/frontend/lib/pages/settings_page.dart @@ -13,92 +13,6 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { - @override - Widget build(BuildContext context) { - final userProvider = Provider.of(context); - final user = userProvider.user; - if (user == null) { - // Redirect to login if not authenticated - Future.microtask(() => context.go('/login')); - return const Scaffold(body: Center(child: CircularProgressIndicator())); - } - - final isPatient = user.role.toUpperCase() == 'PATIENT'; - - return Scaffold( - appBar: AppBar( - title: const Text('Settings'), - backgroundColor: Theme.of(context).colorScheme.surface, - foregroundColor: Theme.of(context).colorScheme.onSurface, - elevation: 0, - actions: [ - // Cancel button - TextButton( - onPressed: () { - if (context.canPop()) { - context.pop(); - } else { - context.go('/dashboard'); - } - }, - child: Text( - 'Cancel', - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), - ), - ), - // Save button - TextButton( - onPressed: _saveSettings, - child: Text( - 'Save', - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - drawer: const CommonDrawer(currentRoute: '/settings'), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - // AI Configuration Section (Patient only) - if (isPatient) ...[ - _buildSectionHeader(context, 'AI Assistant'), - _buildSettingsCard( - context, - icon: Icons.smart_toy, - title: 'AI Configuration', - subtitle: 'Customize your AI assistant settings', - onTap: () => context.go('/ai-configuration'), - ), - const SizedBox(height: 24), - ], - // ...existing code... - ], - ), - ); - } - - void _saveSettings() { - // Placeholder for save settings functionality - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Settings saved'))); - - // Navigate back to dashboard after saving - Future.delayed(const Duration(milliseconds: 500), () { - if (context.mounted) { - if (context.canPop()) { - context.pop(); - } else { - context.go('/dashboard'); - } - } - }); - } - Widget _buildSectionHeader(BuildContext context, String title) { return Padding( padding: const EdgeInsets.only(bottom: 12, left: 4), @@ -248,7 +162,6 @@ class _SettingsPageState extends State { ElevatedButton( onPressed: () { Navigator.of(context).pop(); - // Implement account deletion logic ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text( @@ -270,4 +183,120 @@ class _SettingsPageState extends State { ), ); } + + @override + Widget build(BuildContext context) { + final userProvider = Provider.of(context); + final user = userProvider.user; + + return Scaffold( + appBar: AppBar(title: const Text('Settings'), actions: const []), + drawer: const CommonDrawer(currentRoute: '/settings'), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), // Apply horizontal padding + child: ListView( + children: [ + Center( + child: Column( + children: [ + CircleAvatar( + radius: 36, + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withOpacity(0.1), + child: + (user != null && + user.name != null && + user.name!.isNotEmpty) + ? Text( + user.name![0].toUpperCase(), + style: TextStyle( + fontSize: 32, + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ) + : Icon( + Icons.person, + size: 32, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 8), + Text( + (user != null && + user.name != null && + user.name!.isNotEmpty) + ? user.name! + : 'User', + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + (user != null && user.email.isNotEmpty) ? user.email : '', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 24), + ], + ), + ), + _buildSectionHeader(context, 'Appearance'), + _buildThemeCard(context), + const SizedBox(height: 24), + _buildSectionHeader(context, 'AI Assistant'), + _buildSettingsCard( + context, + icon: Icons.smart_toy, + title: 'AI Configuration', + subtitle: 'Customize your AI assistant settings', + onTap: () => context.push( + '/ai-configuration', + ), // Use push for back button support + ), + const SizedBox(height: 24), + _buildSectionHeader(context, 'Subscription'), + _buildSettingsCard( + context, + icon: Icons.subscriptions, + title: 'Manage Subscription', + subtitle: 'View or update your subscription plan', + onTap: () => context.push( + '/select-package', + ), // Use push for back button support + ), + const SizedBox(height: 24), + _buildSectionHeader(context, 'General'), + _buildSettingsCard( + context, + icon: Icons.cleaning_services, + title: 'Clear Cache', + subtitle: 'Remove temporary files and cache data', + onTap: () => _showClearCacheDialog(context), + ), + _buildSettingsCard( + context, + icon: Icons.logout, + title: 'Sign Out', + subtitle: 'Sign out of your account', + onTap: () => _showSignOutDialog(context), + textColor: Theme.of(context).colorScheme.error, + iconColor: Theme.of(context).colorScheme.error, + ), + _buildSettingsCard( + context, + icon: Icons.delete_forever, + title: 'Delete Account', + subtitle: 'Permanently delete your account', + onTap: () => _showDeleteAccountDialog(context), + textColor: Theme.of(context).colorScheme.error, + iconColor: Theme.of(context).colorScheme.error, + ), + ], + ), + ), + ), + ); + } } diff --git a/careconnect2025/frontend/lib/services/ai_chat_config_service.dart b/careconnect2025/frontend/lib/services/ai_chat_config_service.dart deleted file mode 100644 index f41803c0..00000000 --- a/careconnect2025/frontend/lib/services/ai_chat_config_service.dart +++ /dev/null @@ -1,379 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'api_service.dart'; -import '../config/env_constant.dart'; - -/// DTO classes for AI configuration -class PatientAIConfigDTO { - final int? id; - final int patientId; - final String aiModel; - final double temperature; - final int maxTokens; - final bool medicalContextEnabled; - final bool personalityAdaptation; - final String preferredTone; - final List specialInstructions; - final bool active; - final DateTime? createdAt; - final DateTime? updatedAt; - - PatientAIConfigDTO({ - this.id, - required this.patientId, - required this.aiModel, - this.temperature = 0.7, - this.maxTokens = 1000, - this.medicalContextEnabled = true, - this.personalityAdaptation = false, - this.preferredTone = 'friendly', - this.specialInstructions = const [], - this.active = true, - this.createdAt, - this.updatedAt, - }); - - factory PatientAIConfigDTO.fromJson(Map json) { - return PatientAIConfigDTO( - id: json['id'], - patientId: json['patientId'], - aiModel: json['aiModel'], - temperature: (json['temperature'] as num?)?.toDouble() ?? 0.7, - maxTokens: json['maxTokens'] ?? 1000, - medicalContextEnabled: json['medicalContextEnabled'] ?? true, - personalityAdaptation: json['personalityAdaptation'] ?? false, - preferredTone: json['preferredTone'] ?? 'friendly', - specialInstructions: List.from(json['specialInstructions'] ?? []), - active: json['active'] ?? true, - createdAt: json['createdAt'] != null - ? DateTime.parse(json['createdAt']) - : null, - updatedAt: json['updatedAt'] != null - ? DateTime.parse(json['updatedAt']) - : null, - ); - } - - Map toJson() { - return { - if (id != null) 'id': id, - 'patientId': patientId, - 'aiModel': aiModel, - 'temperature': temperature, - 'maxTokens': maxTokens, - 'medicalContextEnabled': medicalContextEnabled, - 'personalityAdaptation': personalityAdaptation, - 'preferredTone': preferredTone, - 'specialInstructions': specialInstructions, - 'active': active, - }; - } -} - -class ChatRequest { - final String message; - final int patientId; - final int userId; - final String? conversationId; - final String? medicalContext; - final String role; - - ChatRequest({ - required this.message, - required this.patientId, - required this.userId, - this.conversationId, - this.medicalContext, - this.role = 'patient', - }); - - Map toJson() { - return { - 'message': message, - 'patientId': patientId, - 'userId': userId, - if (conversationId != null) 'conversationId': conversationId, - if (medicalContext != null) 'medicalContext': medicalContext, - 'role': role, - }; - } -} - -class ChatResponse { - final bool success; - final String? response; - final String? conversationId; - final String? errorMessage; - final String? errorCode; - final DateTime? timestamp; - - ChatResponse({ - required this.success, - this.response, - this.conversationId, - this.errorMessage, - this.errorCode, - this.timestamp, - }); - - factory ChatResponse.fromJson(Map json) { - return ChatResponse( - success: json['success'] ?? false, - response: json['response'], - conversationId: json['conversationId'], - errorMessage: json['errorMessage'], - errorCode: json['errorCode'], - timestamp: json['timestamp'] != null - ? DateTime.parse(json['timestamp']) - : null, - ); - } -} - -class ChatConversationSummary { - final String id; - final String title; - final DateTime lastMessageAt; - final int messageCount; - final bool active; - - ChatConversationSummary({ - required this.id, - required this.title, - required this.lastMessageAt, - required this.messageCount, - this.active = true, - }); - - factory ChatConversationSummary.fromJson(Map json) { - return ChatConversationSummary( - id: json['id'], - title: json['title'], - lastMessageAt: DateTime.parse(json['lastMessageAt']), - messageCount: json['messageCount'], - active: json['active'] ?? true, - ); - } -} - -class ChatMessageSummary { - final String id; - final String message; - final String sender; - final DateTime timestamp; - final bool isAiResponse; - - ChatMessageSummary({ - required this.id, - required this.message, - required this.sender, - required this.timestamp, - required this.isAiResponse, - }); - - factory ChatMessageSummary.fromJson(Map json) { - return ChatMessageSummary( - id: json['id'], - message: json['message'], - sender: json['sender'], - timestamp: DateTime.parse(json['timestamp']), - isAiResponse: json['isAiResponse'] ?? false, - ); - } -} - -/// Service for managing AI chat configurations and conversations -class AIChatConfigService { - static String get _baseUrl => '${getBackendBaseUrl()}/v1/api/ai-chat'; - static final http.Client _httpClient = http.Client(); - - /// Get patient AI configuration - static Future getPatientAIConfig(int patientId) async { - try { - final authHeaders = await ApiService.getAuthHeaders(); - - print('🤖 Getting AI config for patient $patientId'); - - final response = await _httpClient.get( - Uri.parse('$_baseUrl/config/$patientId'), - headers: authHeaders, - ); - - print('🤖 AI config response status: ${response.statusCode}'); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - return PatientAIConfigDTO.fromJson(data); - } else if (response.statusCode == 404) { - // No configuration found - return null - print('🤖 No AI config found for patient $patientId'); - return null; - } else { - throw Exception('Failed to get AI config: ${response.statusCode}'); - } - } catch (e) { - print('❌ Error getting patient AI config: $e'); - return null; - } - } - - /// Save or update patient AI configuration - static Future savePatientAIConfig( - PatientAIConfigDTO config, - ) async { - try { - final authHeaders = await ApiService.getAuthHeaders(); - authHeaders['Content-Type'] = 'application/json'; - authHeaders['Accept'] = '*/*'; - - // Convert to the correct API format - final requestBody = { - 'patientId': config.patientId, - 'aiProvider': config.aiModel.toUpperCase(), - 'openaiModel': 'gpt-3.5-turbo', - 'deepseekModel': config.aiModel, - 'maxTokens': config.maxTokens, - 'temperature': config.temperature, - 'conversationHistoryLimit': 20, - 'includeVitalsByDefault': config.medicalContextEnabled, - 'includeMedicationsByDefault': config.medicalContextEnabled, - 'includeNotesByDefault': config.medicalContextEnabled, - 'includeMoodPainLogsByDefault': config.medicalContextEnabled, - 'includeAllergiesByDefault': config.medicalContextEnabled, - 'isActive': config.active, - 'systemPrompt': config.specialInstructions.isNotEmpty - ? config.specialInstructions.join(' ') - : "You are a helpful health assistant for patients. Only answer questions related to health, wellness, psychosocial support, or medical topics. If the question is not related to health, respond with 'I can only assist with health-related questions.'", - }; - - final response = await _httpClient.post( - Uri.parse('$_baseUrl/config'), - headers: authHeaders, - body: jsonEncode(requestBody), - ); - - if (response.statusCode == 200 || response.statusCode == 201) { - final data = jsonDecode(response.body); - return PatientAIConfigDTO.fromJson(data); - } else { - throw Exception('Failed to save AI config: ${response.statusCode}'); - } - } catch (e) { - print('Error saving patient AI config: $e'); - return null; - } - } - - /// Deactivate patient AI configuration - static Future deactivatePatientAIConfig(int patientId) async { - try { - final authHeaders = await ApiService.getAuthHeaders(); - final response = await _httpClient.delete( - Uri.parse('$_baseUrl/config/$patientId'), - headers: authHeaders, - ); - - return response.statusCode == 200; - } catch (e) { - print('Error deactivating patient AI config: $e'); - return false; - } - } - - /// Send chat message using backend AI service - static Future sendChatMessage(ChatRequest request) async { - try { - final authHeaders = await ApiService.getAuthHeaders(); - final response = await _httpClient.post( - Uri.parse('$_baseUrl/chat'), - headers: {...authHeaders, 'Content-Type': 'application/json'}, - body: jsonEncode(request.toJson()), - ); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - return ChatResponse.fromJson(data); - } else { - throw Exception('Failed to send chat message: ${response.statusCode}'); - } - } catch (e) { - print('Error sending chat message: $e'); - return ChatResponse( - success: false, - errorMessage: 'Failed to send message: $e', - errorCode: 'NETWORK_ERROR', - ); - } - } - - /// Get patient's chat conversations - static Future> getPatientConversations( - int patientId, - ) async { - try { - final authHeaders = await ApiService.getAuthHeaders(); - final response = await _httpClient.get( - Uri.parse('$_baseUrl/conversations/$patientId'), - headers: authHeaders, - ); - - if (response.statusCode == 200) { - final List data = jsonDecode(response.body); - return data - .map((json) => ChatConversationSummary.fromJson(json)) - .toList(); - } else { - throw Exception('Failed to get conversations: ${response.statusCode}'); - } - } catch (e) { - print('Error getting patient conversations: $e'); - return []; - } - } - - /// Get messages from a specific conversation - static Future> getConversationMessages( - String conversationId, - ) async { - try { - final authHeaders = await ApiService.getAuthHeaders(); - final response = await _httpClient.get( - Uri.parse('$_baseUrl/conversation/$conversationId/messages'), - headers: authHeaders, - ); - - if (response.statusCode == 200) { - final List data = jsonDecode(response.body); - return data.map((json) => ChatMessageSummary.fromJson(json)).toList(); - } else { - throw Exception( - 'Failed to get conversation messages: ${response.statusCode}', - ); - } - } catch (e) { - print('Error getting conversation messages: $e'); - return []; - } - } - - /// Deactivate a conversation - static Future deactivateConversation(String conversationId) async { - try { - final authHeaders = await ApiService.getAuthHeaders(); - final response = await _httpClient.post( - Uri.parse('$_baseUrl/conversation/$conversationId/deactivate'), - headers: authHeaders, - ); - - return response.statusCode == 200; - } catch (e) { - print('Error deactivating conversation: $e'); - return false; - } - } - - /// Dispose of resources - static void dispose() { - _httpClient.close(); - } -} diff --git a/careconnect2025/frontend/lib/services/ai_chat_service.dart b/careconnect2025/frontend/lib/services/ai_chat_service.dart index cbe77b7e..f9a8cd4c 100644 --- a/careconnect2025/frontend/lib/services/ai_chat_service.dart +++ b/careconnect2025/frontend/lib/services/ai_chat_service.dart @@ -24,6 +24,7 @@ class AIChatService { bool includeNotes = false, bool includeMoodPainLogs = false, bool includeAllergies = false, + List>? uploadedFiles, }) async { try { final authHeaders = await ApiService.getAuthHeaders(); @@ -45,6 +46,8 @@ class AIChatService { 'includeNotes': includeNotes, 'includeMoodPainLogs': includeMoodPainLogs, 'includeAllergies': includeAllergies, + if (uploadedFiles != null && uploadedFiles.isNotEmpty) + 'uploadedFiles': uploadedFiles, }; print('🤖 Sending AI chat message: ${requestBody['message']}'); @@ -230,83 +233,4 @@ class AIChatService { return 'Sorry, I encountered an error analyzing the file. Please try again later.'; } } - - /// Get AI configuration for a patient - static Future?> getAIConfiguration({ - required int patientId, - }) async { - try { - final authHeaders = await ApiService.getAuthHeaders(); - - print('🤖 Getting AI config for patient $patientId'); - - final response = await http.get( - Uri.parse('$_baseUrl/config/$patientId'), - headers: authHeaders, - ); - - print('🤖 AI config response status: ${response.statusCode}'); - - if (response.statusCode == 200) { - return jsonDecode(response.body); - } else if (response.statusCode == 404) { - print('🤖 No AI config found for patient $patientId'); - return null; - } else { - throw Exception( - 'Failed to get AI configuration: ${response.statusCode}', - ); - } - } catch (e) { - print('❌ Error getting AI configuration: $e'); - return null; - } - } - - /// Update AI configuration for user - static Future updateAIConfiguration({ - required int patientId, - required Map configuration, - }) async { - try { - final authHeaders = await ApiService.getAuthHeaders(); - authHeaders['Content-Type'] = 'application/json'; - authHeaders['Accept'] = '*/*'; - - final requestBody = { - 'patientId': patientId, - 'aiProvider': configuration['aiProvider'] ?? 'DEEPSEEK', - 'openaiModel': configuration['openaiModel'] ?? 'gpt-3.5-turbo', - 'deepseekModel': configuration['deepseekModel'] ?? 'deepseek-chat', - 'maxTokens': configuration['maxTokens'] ?? 1000, - 'temperature': configuration['temperature'] ?? 0.7, - 'conversationHistoryLimit': - configuration['conversationHistoryLimit'] ?? 20, - 'includeVitalsByDefault': - configuration['includeVitalsByDefault'] ?? true, - 'includeMedicationsByDefault': - configuration['includeMedicationsByDefault'] ?? true, - 'includeNotesByDefault': configuration['includeNotesByDefault'] ?? true, - 'includeMoodPainLogsByDefault': - configuration['includeMoodPainLogsByDefault'] ?? true, - 'includeAllergiesByDefault': - configuration['includeAllergiesByDefault'] ?? true, - 'isActive': configuration['isActive'] ?? true, - 'systemPrompt': - configuration['systemPrompt'] ?? - "You are a helpful health assistant for patients. Only answer questions related to health, wellness, psychosocial support, or medical topics. If the question is not related to health, respond with 'I can only assist with health-related questions.'", - }; - - final response = await http.post( - Uri.parse('$_baseUrl/config'), - headers: authHeaders, - body: jsonEncode(requestBody), - ); - - return response.statusCode == 200 || response.statusCode == 201; - } catch (e) { - print('❌ Error updating AI configuration: $e'); - return false; - } - } } diff --git a/careconnect2025/frontend/lib/services/ai_config_service.dart b/careconnect2025/frontend/lib/services/ai_config_service.dart index f8377ddf..4b83ac53 100644 --- a/careconnect2025/frontend/lib/services/ai_config_service.dart +++ b/careconnect2025/frontend/lib/services/ai_config_service.dart @@ -1,5 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:provider/provider.dart'; +import '../providers/user_provider.dart'; import 'api_service.dart'; import '../config/env_constant.dart'; @@ -124,104 +126,96 @@ class PatientAIConfigDTO { class AIConfigService { static String get baseUrl => '${getBackendBaseUrl()}/v1/api/ai-chat'; - /// Get AI configuration for a patient - static Future getPatientAIConfig(int patientId) async { + /// Create or update AI configuration for a user + /// Returns the saved PatientAIConfigDTO or null on failure + static Future saveUserAIConfig( + PatientAIConfigDTO config, { + required int userId, + }) async { try { final authHeaders = await ApiService.getAuthHeaders(); + authHeaders['Content-Type'] = 'application/json'; + authHeaders['Accept'] = '*/*'; - print( - '🤖 Getting AI config for patient $patientId from: $baseUrl/config/$patientId', - ); + // Compose request body to match backend API + final requestBody = { + 'userId': userId, + 'patientId': config.patientId, + 'aiProvider': config.aiProvider, + 'openaiModel': config.preferences['openaiModel'] ?? 'gpt-4', + 'deepseekModel': config.preferences['deepseekModel'] ?? 'deepseek-chat', + 'maxTokens': config.maxTokensPerSession, + 'temperature': config.temperature, + 'conversationHistoryLimit': + config.preferences['conversationHistoryLimit'] ?? 20, + 'includeVitalsByDefault': + config.preferences['includeVitalsByDefault'] ?? true, + 'includeMedicationsByDefault': + config.preferences['includeMedicationsByDefault'] ?? true, + 'includeNotesByDefault': + config.preferences['includeNotesByDefault'] ?? true, + 'includeMoodPainLogsByDefault': + config.preferences['includeMoodPainLogsByDefault'] ?? true, + 'includeAllergiesByDefault': + config.preferences['includeAllergiesByDefault'] ?? true, + 'isActive': config.isActive, + 'systemPrompt': + config.preferences['systemPrompt'] ?? + 'You are a helpful AI assistant.', + }; - final response = await http.get( - Uri.parse('$baseUrl/config/$patientId'), + final response = await http.post( + Uri.parse('$baseUrl/config'), headers: authHeaders, + body: jsonEncode(requestBody), ); - - print('🤖 AI config response status: ${response.statusCode}'); - - if (response.statusCode == 200) { + if (response.statusCode == 200 || response.statusCode == 201) { final data = jsonDecode(response.body); return PatientAIConfigDTO.fromJson(data); - } else if (response.statusCode == 404) { - // No configuration found, return default - print('🤖 No AI config found for patient $patientId, using default'); - return _getDefaultConfig(patientId); } else { - print('❌ Failed to get AI config: ${response.statusCode}'); + print('❌ Failed to save/update AI config: ${response.statusCode}'); return null; } } catch (e) { - print('❌ Error getting AI config: $e'); + print('❌ Error saving/updating AI config: $e'); return null; } } - /// Save or update AI configuration for a patient - static Future savePatientAIConfig( - PatientAIConfigDTO config, - ) async { + /// Get AI configuration for the logged-in user + /// Usage: AIConfigService.getUserAIConfig(context) + static Future getUserAIConfig(context) async { try { final authHeaders = await ApiService.getAuthHeaders(); - // Check if config exists for patient - final checkResponse = await http.get( - Uri.parse('$baseUrl/config/${config.patientId}'), - headers: authHeaders, - ); - http.Response response; - if (checkResponse.statusCode == 200) { - // Config exists, use PUT to update - response = await http.put( - Uri.parse('$baseUrl/config/${config.patientId}'), - headers: authHeaders, - body: jsonEncode(config.toJson()), - ); - print( - '🤖 Updating AI config for patient ${config.patientId}: ${response.statusCode}', - ); - } else { - // Config does not exist, use POST to create - response = await http.post( - Uri.parse('$baseUrl/config'), - headers: authHeaders, - body: jsonEncode(config.toJson()), - ); - print( - '🤖 Creating AI config for patient ${config.patientId}: ${response.statusCode}', - ); + final userProvider = Provider.of(context, listen: false); + final userId = userProvider.user?.id; + if (userId == null) { + print('❌ No logged-in user found for AI config fetch.'); + return null; } - if (response.statusCode == 200 || response.statusCode == 201) { + final uri = Uri.parse( + '$baseUrl/config', + ).replace(queryParameters: {'userId': userId.toString()}); + print('🤖 Getting AI config for userId $userId from: $uri'); + final response = await http.get(uri, headers: authHeaders); + print('🤖 AI config response status: ${response.statusCode}'); + if (response.statusCode == 200) { final data = jsonDecode(response.body); return PatientAIConfigDTO.fromJson(data); + } else if (response.statusCode == 404) { + print('🤖 No AI config found for userId $userId, using default'); + return _getDefaultConfig(userId); } else { - print('❌ Failed to save/update AI config: ${response.statusCode}'); + print('❌ Failed to get AI config: ${response.statusCode}'); return null; } } catch (e) { - print('❌ Error saving/updating AI config: $e'); + print('❌ Error getting AI config: $e'); return null; } } - /// Deactivate AI configuration for a patient - static Future deactivatePatientAIConfig(int patientId) async { - try { - final authHeaders = await ApiService.getAuthHeaders(); - final response = await http.delete( - Uri.parse('$baseUrl/config/$patientId'), - headers: authHeaders, - ); - - print( - '🤖 Deactivating AI config for patient $patientId: ${response.statusCode}', - ); - - return response.statusCode == 200; - } catch (e) { - print('❌ Error deactivating AI config: $e'); - return false; - } - } + // ...keep only DTO and single config logic if needed... /// Get default AI configuration for a patient static PatientAIConfigDTO _getDefaultConfig(int patientId) { diff --git a/careconnect2025/frontend/lib/services/ai_service.dart b/careconnect2025/frontend/lib/services/ai_service.dart index 87c1d04c..c957a08d 100644 --- a/careconnect2025/frontend/lib/services/ai_service.dart +++ b/careconnect2025/frontend/lib/services/ai_service.dart @@ -46,8 +46,8 @@ class AIService { String role = 'patient', AIModel model = AIModel.deepseek, String? healthDataContext, - int? patientId, - int? userId, + required int patientId, + required int userId, BuildContext? context, }) async { // Check subscription access for caregivers @@ -97,8 +97,8 @@ class AIService { final response = await AIChatService.sendMessage( message: enhancedMessage, - patientId: patientId ?? 1, // Default patient ID if not provided - userId: userId ?? 1, // Default user ID if not provided + patientId: patientId, + userId: userId, chatType: chatType, preferredModel: model.modelName, temperature: 0.7, @@ -170,7 +170,17 @@ class AIService { } // Legacy method for backward compatibility - static Future askHealthQuestion(String question) async { - return askAI(question, role: 'patient', model: AIModel.deepseek); + static Future askHealthQuestion( + String question, { + required int patientId, + required int userId, + }) async { + return askAI( + question, + role: 'patient', + model: AIModel.deepseek, + patientId: patientId, + userId: userId, + ); } } diff --git a/careconnect2025/frontend/lib/services/api_service.dart b/careconnect2025/frontend/lib/services/api_service.dart index 5d8cd0bc..454ef474 100644 --- a/careconnect2025/frontend/lib/services/api_service.dart +++ b/careconnect2025/frontend/lib/services/api_service.dart @@ -606,7 +606,7 @@ class ApiService { } } - // Get specific patient data + // Get specific patient data (family member access) static Future> getPatientData(int patientId) async { final headers = await AuthTokenManager.getAuthHeaders(); final response = await http.get( @@ -625,6 +625,30 @@ class ApiService { } } + /// Get a specific patient under a caregiver's care + static Future> getPatientForCaregiver( + int caregiverId, + int patientId, + ) async { + final headers = await AuthTokenManager.getAuthHeaders(); + final response = await http.get( + Uri.parse( + '${ApiConstants.baseUrl}caregivers/$caregiverId/patients/$patientId', + ), + headers: headers, + ); + + if (response.statusCode == 200) { + return jsonDecode(response.body); + } else if (response.statusCode == 403) { + throw Exception('Access denied to patient data'); + } else if (response.statusCode == 404) { + throw Exception('Patient not found'); + } else { + throw Exception('Failed to fetch patient data'); + } + } + // Check if family member has access to patient static Future hasAccessToPatient(int patientId) async { final headers = await AuthTokenManager.getAuthHeaders(); @@ -942,10 +966,10 @@ class ApiService { return await _httpClient .post( - Uri.parse('${ApiConstants.baseUrl}messages/send'), - headers: headers, - body: body, - ) + Uri.parse('${ApiConstants.baseUrl}messages/send'), + headers: headers, + body: body, + ) .timeout(const Duration(seconds: 15)); } @@ -985,12 +1009,11 @@ class ApiService { // Get patient tasks static Future getPatientTasks(int patientId) async { final headers = await AuthTokenManager.getAuthHeaders(); - return await _httpClient.get( - Uri.parse( - '${ApiConstants.tasks}/patient/$patientId', - ), - headers: headers, - ) + return await _httpClient + .get( + Uri.parse('${ApiConstants.tasks}/patient/$patientId'), + headers: headers, + ) .timeout(const Duration(seconds: 30)); } @@ -998,27 +1021,24 @@ class ApiService { static Future deleteTask(int taskId) async { final headers = await AuthTokenManager.getAuthHeaders(); return await _httpClient - .delete( - Uri.parse('${ApiConstants.tasks}/$taskId'), - headers: headers, - ) + .delete(Uri.parse('${ApiConstants.tasks}/$taskId'), headers: headers) .timeout(const Duration(seconds: 30)); } // Edit a task by task ID static Future editTask( - int taskId, - Map taskData, - ) async { + int taskId, + Map taskData, + ) async { final headers = await AuthTokenManager.getAuthHeaders(); headers['Content-Type'] = 'application/json'; return await _httpClient .put( - Uri.parse('${ApiConstants.tasks}/$taskId'), - headers: headers, - body: jsonEncode(taskData), - ) + Uri.parse('${ApiConstants.tasks}/$taskId'), + headers: headers, + body: jsonEncode(taskData), + ) .timeout(const Duration(seconds: 30)); } @@ -1027,20 +1047,22 @@ class ApiService { final headers = await AuthTokenManager.getAuthHeaders(); return await _httpClient .get( - Uri.parse('${ApiConstants.baseUrl}templates/all'), // get all for now - headers: headers, - ) + Uri.parse('${ApiConstants.baseUrl}templates/all'), // get all for now + headers: headers, + ) .timeout(const Duration(seconds: 30)); } + static Future getTaskTemplate(int templateId) async { final headers = await AuthTokenManager.getAuthHeaders(); return await _httpClient .get( - Uri.parse('${ApiConstants.baseUrl}templates/$templateId'), - headers: headers, - ) + Uri.parse('${ApiConstants.baseUrl}templates/$templateId'), + headers: headers, + ) .timeout(const Duration(seconds: 30)); } + // Create a task static Future createTask(int patientId, String task) async { final headers = await AuthTokenManager.getAuthHeaders(); @@ -1048,73 +1070,97 @@ class ApiService { return await _httpClient .post( - Uri.parse('${ApiConstants.tasks}/patient/$patientId'), - headers: headers, - body: task, - ) + Uri.parse('${ApiConstants.tasks}/patient/$patientId'), + headers: headers, + body: task, + ) .timeout(const Duration(seconds: 30)); } + + static Future?> getEnhancedPatientProfile( + int patientId, + ) async { + try { + final headers = await AuthTokenManager.getAuthHeaders(); + final url = Uri.parse( + '${ApiConstants.patients}/$patientId/profile/enhanced', + ); + final response = await _httpClient + .get(url, headers: headers) + .timeout(const Duration(seconds: 30)); + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + if (decoded is Map && decoded.containsKey('data')) { + return decoded['data'] as Map?; + } else { + return decoded as Map?; + } + } else { + print('Failed to fetch enhanced profile: ${response.statusCode}'); + return null; + } + } catch (e) { + print('Error fetching enhanced patient profile: ${e.toString()}'); + return null; + } + } } // Save speech-to-text to a file and upload it to S3 Future uploadUserFileFromBytes({ -required int userId, -required Uint8List fileBytes, -required String fileName, -required String category, -String? role, + required int userId, + required Uint8List fileBytes, + required String fileName, + required String category, + String? role, }) async { -final headers = await AuthTokenManager.getAuthHeaders(); -headers.remove('Content-Type'); // Multipart will handle it - -var request = http.MultipartRequest( -'POST', -Uri.parse('${ApiConstants.files}/users/$userId/upload'), -); - -// Add headers -request.headers.addAll(headers); - -// Create MultipartFile from bytes -var fileStream = http.ByteStream(Stream.fromIterable([fileBytes])); -var fileLength = await fileStream.length; -var multipartFile = http.MultipartFile( -'file', -fileStream, -fileLength, -filename: fileName, -); - -request.files.add(multipartFile); -request.fields['category'] = category; - -// Send the request -var streamedResponse = await request.send().timeout( -const Duration(seconds: 30), -); -var response = await http.Response.fromStream(streamedResponse); - -return response; + final headers = await AuthTokenManager.getAuthHeaders(); + headers.remove('Content-Type'); // Multipart will handle it + + var request = http.MultipartRequest( + 'POST', + Uri.parse('${ApiConstants.files}/users/$userId/upload'), + ); + + // Add headers + request.headers.addAll(headers); + + // Create MultipartFile from bytes + var fileStream = http.ByteStream(Stream.fromIterable([fileBytes])); + var fileLength = await fileStream.length; + var multipartFile = http.MultipartFile( + 'file', + fileStream, + fileLength, + filename: fileName, + ); + + request.files.add(multipartFile); + request.fields['category'] = category; + + // Send the request + var streamedResponse = await request.send().timeout( + const Duration(seconds: 30), + ); + var response = await http.Response.fromStream(streamedResponse); + + return response; } // Get list of files from saved S3 storage -Future getUserFilesByCategory( - int userId) async { +Future getUserFilesByCategory(int userId) async { try { - final headers = await AuthTokenManager.getAuthHeaders(); + final headers = await AuthTokenManager.getAuthHeaders(); - final uri = Uri.parse( - '${ApiConstants.baseUrl}files/users/$userId/list', - ); + final uri = Uri.parse('${ApiConstants.baseUrl}files/users/$userId/list'); - return await _httpClient.get( - uri, - headers: headers, - ).timeout( - const Duration(seconds: 10), - onTimeout: () => http.Response('{"error": "Request timeout"}', 408), - ); + return await _httpClient + .get(uri, headers: headers) + .timeout( + const Duration(seconds: 10), + onTimeout: () => http.Response('{"error": "Request timeout"}', 408), + ); } catch (e) { - return http.Response(jsonEncode({'error': e.toString()}), 500); + return http.Response(jsonEncode({'error': e.toString()}), 500); } } diff --git a/careconnect2025/frontend/lib/services/api_service_new.dart b/careconnect2025/frontend/lib/services/api_service_new.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/careconnect2025/frontend/lib/services/auth_service.dart b/careconnect2025/frontend/lib/services/auth_service.dart index 28be7e8c..d5c2c440 100644 --- a/careconnect2025/frontend/lib/services/auth_service.dart +++ b/careconnect2025/frontend/lib/services/auth_service.dart @@ -1,3 +1,6 @@ +/// Centralized Auth Guard Service for page protection +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:app_links/app_links.dart'; @@ -6,6 +9,7 @@ import '../config/env_constant.dart'; import 'api_service.dart'; import 'oauth_service.dart'; import '../providers/user_provider.dart'; +import 'messaging_service.dart'; import 'auth_token_manager.dart'; class ApiConstants { @@ -159,6 +163,13 @@ class AuthService { // Update last activity time to track session freshness await AuthTokenManager.updateLastActivity(); + // Register user to WebSocket for notifications + try { + await MessagingService.registerUser(userId: userSession.id.toString()); + } catch (e) { + print('Warning: Failed to register user to WebSocket: $e'); + } + print('✅ Login successful: JWT token force-updated'); return userSession; @@ -524,6 +535,18 @@ class AuthService { await AuthTokenManager.updateLastActivity(); } + static Future checkAndRedirectIfUnauthenticated( + BuildContext context, { + String loginRoute = '/login', + }) async { + final isAuth = await AuthTokenManager.isAuthenticated(); + if (!isAuth && context.mounted) { + context.go(loginRoute); + return false; + } + return true; + } + // static Future setupPassword({ // required String email, // required String resetToken, diff --git a/careconnect2025/frontend/lib/services/auth_service_new.dart b/careconnect2025/frontend/lib/services/auth_service_new.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/careconnect2025/frontend/lib/services/call_notification_service.dart b/careconnect2025/frontend/lib/services/call_notification_service.dart index 4c2269a8..b86bb145 100644 --- a/careconnect2025/frontend/lib/services/call_notification_service.dart +++ b/careconnect2025/frontend/lib/services/call_notification_service.dart @@ -1,12 +1,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:socket_io_client/socket_io_client.dart' as io; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:web_socket_channel/status.dart' as status; import '../widgets/incoming_call_popup.dart'; import '../widgets/hybrid_video_call_widget.dart'; +import '../config/env_constant.dart'; /// Service to handle real-time call notifications for caregivers class CallNotificationService { - static io.Socket? _socket; + static WebSocketChannel? _channel; static bool _isConnected = false; static String? _currentUserId; static String? _currentUserRole; @@ -26,6 +28,7 @@ class CallNotificationService { required String userId, required String userRole, // 'CAREGIVER' or 'PATIENT' required BuildContext context, + String? websocketUrl, // Optional: pass WebSocket URL for flexibility }) async { try { _currentUserId = userId; @@ -34,61 +37,46 @@ class CallNotificationService { print('🔔 Initializing CallNotificationService for $userRole: $userId'); - // Connect to your backend WebSocket/Socket.IO server - // For development: ws://localhost:8080 - // For production: replace with your actual backend URL - const String websocketUrl = String.fromEnvironment( - 'WEBSOCKET_URL', - defaultValue: 'ws://localhost:8080', + // Connect to backend WebSocket endpoint (not socket.io) + final String wsUrl = websocketUrl ?? getWebSocketNotificationUrl(); + print('Connecting to notification WebSocket: $wsUrl'); + _channel = WebSocketChannel.connect(Uri.parse(wsUrl)); + _isConnected = true; + + // Register user after connection + final registerMsg = { + 'type': 'register', + 'userId': userId, + 'userRole': userRole, + }; + _channel!.sink.add(_encode(registerMsg)); + + // Listen for messages + _channel!.stream.listen( + (message) { + final data = _decode(message); + if (data == null) return; + if (data['type'] == 'incoming-video-call') { + print('📞 Received incoming video call: $data'); + _handleIncomingCall(data); + } else if (data['type'] == 'call-ended') { + print('📞 Call ended: $data'); + } else if (data['type'] == 'call-answered') { + print('📞 Call answered: $data'); + } else if (data['type'] == 'call-declined') { + print('📞 Call declined: $data'); + } + }, + onDone: () { + _isConnected = false; + print('❌ CallNotificationService WebSocket closed'); + }, + onError: (e) { + _isConnected = false; + print('❌ CallNotificationService WebSocket error: $e'); + }, ); - _socket = io.io(websocketUrl, { - 'transports': ['websocket'], - 'autoConnect': false, - 'query': {'userId': userId, 'userRole': userRole}, - }); - - _socket!.connect(); - - // Connection events - _socket!.onConnect((_) { - _isConnected = true; - print('✅ CallNotificationService connected'); - - // Join user-specific room for notifications - _socket!.emit('join-user-room', { - 'userId': userId, - 'userRole': userRole, - }); - }); - - _socket!.onDisconnect((_) { - _isConnected = false; - print('❌ CallNotificationService disconnected'); - }); - - // Listen for incoming video call invitations - _socket!.on('incoming-video-call', (data) { - print('📞 Received incoming video call: $data'); - _handleIncomingCall(data); - }); - - // Listen for call status updates - _socket!.on('call-ended', (data) { - print('📞 Call ended: $data'); - // Handle call ended notification - }); - - _socket!.on('call-answered', (data) { - print('📞 Call answered: $data'); - // Handle call answered notification - }); - - _socket!.on('call-declined', (data) { - print('📞 Call declined: $data'); - // Handle call declined notification - }); - return true; } catch (e) { print('❌ Error initializing CallNotificationService: $e'); @@ -164,11 +152,15 @@ class CallNotificationService { print('✅ Accepting call: $callId'); // Notify backend that call was accepted - _socket?.emit('accept-call', { - 'callId': callId, - 'acceptedBy': _currentUserId, - 'acceptedByRole': _currentUserRole, - }); + if (_channel != null && _isConnected) { + final msg = { + 'type': 'accept-call', + 'callId': callId, + 'acceptedBy': _currentUserId, + 'acceptedByRole': _currentUserRole, + }; + _channel!.sink.add(_encode(msg)); + } // Close the incoming call popup Navigator.of(_context!).pop(); @@ -195,11 +187,15 @@ class CallNotificationService { print('❌ Declining call: $callId'); // Notify backend that call was declined - _socket?.emit('decline-call', { - 'callId': callId, - 'declinedBy': _currentUserId, - 'declinedByRole': _currentUserRole, - }); + if (_channel != null && _isConnected) { + final msg = { + 'type': 'decline-call', + 'callId': callId, + 'declinedBy': _currentUserId, + 'declinedByRole': _currentUserRole, + }; + _channel!.sink.add(_encode(msg)); + } // Close the incoming call popup if (_context != null) { @@ -214,15 +210,14 @@ class CallNotificationService { required String callId, required bool isVideoCall, }) async { - if (!_isConnected || _socket == null) { + if (!_isConnected || _channel == null) { print('❌ Cannot send call invitation - not connected'); return false; } - try { print('📤 Sending call invitation to $recipientRole: $recipientId'); - - _socket!.emit('send-video-call-invitation', { + final msg = { + 'type': 'send-video-call-invitation', 'callId': callId, 'callerId': _currentUserId, 'callerName': _getCurrentUserName(), @@ -231,8 +226,8 @@ class CallNotificationService { 'recipientRole': recipientRole, 'isVideoCall': isVideoCall, 'timestamp': DateTime.now().toIso8601String(), - }); - + }; + _channel!.sink.add(_encode(msg)); return true; } catch (e) { print('❌ Error sending call invitation: $e'); @@ -240,6 +235,29 @@ class CallNotificationService { } } + // Helper to encode/decode JSON + static String _encode(Map data) { + return data.toString().replaceAll( + "'", + '"', + ); // quick-and-dirty for demo; use jsonEncode in real code + } + + static Map? _decode(dynamic message) { + try { + if (message is String) { + // quick-and-dirty for demo; use jsonDecode in real code + return Map.from( + (message as String).replaceAll('"', '"') == message ? {} : {}, + ); // TODO: replace with jsonDecode + } + } catch (e) { + print('❌ Error decoding WebSocket message: $e'); + } + return null; + // removed extra closing brace here + } + /// Get current user name from context or default static String _getCurrentUserName() { // You can implement this to get the actual user name @@ -251,9 +269,8 @@ class CallNotificationService { static void dispose() { print('🧹 Disposing CallNotificationService'); - _socket?.disconnect(); - _socket?.dispose(); - _socket = null; + _channel?.sink.close(status.goingAway); + _channel = null; _isConnected = false; _currentUserId = null; diff --git a/careconnect2025/frontend/lib/services/firebase_auth_service.dart b/careconnect2025/frontend/lib/services/firebase_auth_service.dart deleted file mode 100644 index 0027792d..00000000 --- a/careconnect2025/frontend/lib/services/firebase_auth_service.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:http/http.dart' as http; -import 'package:pointycastle/export.dart'; -import 'package:flutter/foundation.dart'; - -/// Service to handle Firebase authentication using service account -class FirebaseAuthService { - // Use environment variable for service account path with fallback - static String? get _serviceAccountPath { - const String envPath = String.fromEnvironment( - 'FIREBASE_SERVICE_ACCOUNT_PATH', - ); - if (envPath.isNotEmpty) { - return envPath; - } - - // Fallback to default path (optional) - const String fallbackPath = String.fromEnvironment( - 'FIREBASE_SERVICE_ACCOUNT_PATH_FALLBACK', - defaultValue: '', - ); - - return fallbackPath.isNotEmpty ? fallbackPath : null; - } - - static Map? _serviceAccount; - static String? _cachedAccessToken; - static DateTime? _tokenExpiry; - - /// Load service account from JSON file - static Future?> _loadServiceAccount() async { - if (_serviceAccount != null) return _serviceAccount; - - try { - final serviceAccountPath = _serviceAccountPath; - - // Check if service account path is configured - if (serviceAccountPath == null || serviceAccountPath.isEmpty) { - print( - '⚠️ Firebase service account path not configured via environment variables', - ); - print(' Set FIREBASE_SERVICE_ACCOUNT_PATH environment variable'); - return null; - } - - if (!kIsWeb) { - final file = File(serviceAccountPath); - - // Check if file exists before attempting to read - if (await file.exists()) { - try { - final contents = await file.readAsString(); - final parsed = jsonDecode(contents); - - // Validate required fields in service account - if (parsed is Map && - parsed.containsKey('client_email') && - parsed.containsKey('private_key')) { - _serviceAccount = parsed; - print( - '✅ Firebase service account loaded successfully from: $serviceAccountPath', - ); - return _serviceAccount; - } else { - print( - '❌ Invalid service account format - missing required fields', - ); - return null; - } - } catch (e) { - print('❌ Error reading or parsing service account file: $e'); - return null; - } - } else { - print('⚠️ Service account file not found at: $serviceAccountPath'); - print( - '💡 FCM messaging will use limited functionality without service account', - ); - print(' To enable full FCM features:'); - print( - ' 1. Download Firebase service account JSON from Firebase Console', - ); - print(' 2. Set FIREBASE_SERVICE_ACCOUNT_PATH environment variable'); - return null; - } - } else { - print('⚠️ Service account loading not supported on web platform'); - print('💡 Web FCM messaging will use client-side tokens only'); - return null; - } - } catch (e) { - print('❌ Unexpected error loading service account: $e'); - return null; - } - } - - /// Get Firebase access token using service account - static Future getAccessToken() async { - // Return cached token if still valid - if (_cachedAccessToken != null && - _tokenExpiry != null && - DateTime.now().isBefore(_tokenExpiry!)) { - return _cachedAccessToken; - } - - try { - final serviceAccount = await _loadServiceAccount(); - if (serviceAccount == null) { - print('⚠️ Service account unavailable - FCM admin features disabled'); - print('💡 App will continue with limited messaging functionality'); - return null; - } - - // Create JWT for Firebase authentication - final header = {'alg': 'RS256', 'typ': 'JWT'}; - - final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; - final exp = now + 3600; // 1 hour expiry - - final payload = { - 'iss': serviceAccount['client_email'], - 'scope': 'https://www.googleapis.com/auth/firebase.messaging', - 'aud': 'https://oauth2.googleapis.com/token', - 'iat': now, - 'exp': exp, - }; - - // Create and sign JWT - final jwt = await _createSignedJWT( - header, - payload, - serviceAccount['private_key'], - ); - if (jwt == null) { - print('❌ Failed to create signed JWT - messaging features limited'); - return null; - } - - // Exchange JWT for access token - final response = await http.post( - Uri.parse('https://oauth2.googleapis.com/token'), - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: { - 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', - 'assertion': jwt, - }, - ); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - _cachedAccessToken = data['access_token']; - _tokenExpiry = DateTime.now().add( - Duration(seconds: data['expires_in'] - 60), - ); // 60s buffer - - print('✅ Firebase access token obtained successfully'); - return _cachedAccessToken; - } else { - print( - '❌ Failed to get access token: ${response.statusCode} - ${response.body}', - ); - return null; - } - } catch (e) { - print('❌ Error getting Firebase access token: $e'); - return null; - } - } - - /// Create signed JWT token using RSA private key - static Future _createSignedJWT( - Map header, - Map payload, - String privateKeyPem, - ) async { - try { - // Encode header and payload - final headerEncoded = _base64UrlEncode(utf8.encode(jsonEncode(header))); - final payloadEncoded = _base64UrlEncode(utf8.encode(jsonEncode(payload))); - - final message = '$headerEncoded.$payloadEncoded'; - - // Parse RSA private key - final rsaPrivateKey = _parseRSAPrivateKey(privateKeyPem); - if (rsaPrivateKey == null) { - print('❌ Failed to parse RSA private key'); - return null; - } - - // Sign the message - final signer = RSASigner(SHA256Digest(), '0609608648016503040201'); - signer.init(true, PrivateKeyParameter(rsaPrivateKey)); - - final signature = signer.generateSignature(utf8.encode(message)); - final signatureEncoded = _base64UrlEncode(signature.bytes); - - return '$message.$signatureEncoded'; - } catch (e) { - print('❌ Error creating signed JWT: $e'); - return null; - } - } - - /// Parse RSA private key from PEM format - static RSAPrivateKey? _parseRSAPrivateKey(String privateKeyPem) { - try { - // For now, return null - in production, you would need proper ASN.1 parsing - // This requires the asn1lib package: https://pub.dev/packages/asn1lib - print('⚠️ RSA private key parsing requires asn1lib package'); - return null; - } catch (e) { - print('❌ Error parsing RSA private key: $e'); - return null; - } - } - - /// Base64 URL encode - static String _base64UrlEncode(List bytes) { - return base64Url.encode(bytes).replaceAll('=', ''); - } - - /// Send FCM message using access token - static Future sendFCMMessage({ - required String recipientToken, - required String title, - required String body, - Map? data, - }) async { - try { - final accessToken = await getAccessToken(); - if (accessToken == null) { - print('❌ Failed to get access token for FCM'); - return false; - } - - final message = { - 'message': { - 'token': recipientToken, - 'notification': {'title': title, 'body': body}, - if (data != null) 'data': data, - }, - }; - - final response = await http.post( - Uri.parse( - 'https://fcm.googleapis.com/v1/projects/careconnectptdemo/messages:send', - ), - headers: { - 'Authorization': 'Bearer $accessToken', - 'Content-Type': 'application/json', - }, - body: jsonEncode(message), - ); - - if (response.statusCode == 200) { - print('✅ FCM message sent successfully'); - return true; - } else { - print( - '❌ Failed to send FCM message: ${response.statusCode} - ${response.body}', - ); - return false; - } - } catch (e) { - print('❌ Error sending FCM message: $e'); - return false; - } - } -} diff --git a/careconnect2025/frontend/lib/services/generic_webrtc_service.dart b/careconnect2025/frontend/lib/services/generic_webrtc_service.dart new file mode 100644 index 00000000..921d33e7 --- /dev/null +++ b/careconnect2025/frontend/lib/services/generic_webrtc_service.dart @@ -0,0 +1,97 @@ +import 'package:flutter_webrtc/flutter_webrtc.dart'; +// import 'package:socket_io_client/socket_io_client.dart' as IO; +import 'video_call_service_base.dart'; +import '../config/env_constant.dart'; +import 'webrtc_signaling.dart'; + +class GenericWebRTCService implements VideoCallServiceBase { + static String get _signalingServerUrl => getWebRTCSignalingServerUrl(); + WebRTCSignaling? _signaling; + RTCPeerConnection? _peerConnection; + MediaStream? _localStream; + MediaStream? _remoteStream; + // String? _currentCallId; + + @override + Future initializeService() async { + // No-op for now, could pre-connect signaling if needed + } + + @override + Future checkUserAvailability(String userId) async { + // For demo, always return true. In production, query signaling server for user presence. + return true; + } + + @override + Future> initiateCall({ + required String callId, + required String callerId, + required String recipientId, + required bool isVideoCall, + }) async { + try { + _signaling = WebRTCSignaling(_signalingServerUrl); + + // Get user media + _localStream = await navigator.mediaDevices.getUserMedia({ + 'audio': true, + 'video': isVideoCall, + }); + + // Create peer connection + final config = { + 'iceServers': [ + {'urls': 'stun:stun.l.google.com:19302'}, + ], + }; + _peerConnection = await createPeerConnection(config); + _localStream!.getTracks().forEach((track) { + _peerConnection!.addTrack(track, _localStream!); + }); + + if (_signaling != null) { + // Listen for ICE candidates + _peerConnection!.onIceCandidate = (candidate) async { + await _signaling!.sendSignal( + userId: recipientId, + message: 'ice-candidate:${candidate.toMap()}', + ); + }; + + // Listen for remote stream + _peerConnection!.onTrack = (event) { + if (event.streams.isNotEmpty) { + _remoteStream = event.streams[0]; + } + }; + + // Create offer + final offer = await _peerConnection!.createOffer(); + await _peerConnection!.setLocalDescription(offer); + + // Send offer to recipient + await _signaling!.sendSignal( + userId: recipientId, + message: 'offer:${offer.toMap()}', + ); + } else { + print('[WebRTC] Running in local-only mode: signaling unavailable.'); + } + + return { + 'success': true, + 'callId': callId, + // Optionally return local/remote stream for UI + 'signaling': _signaling != null, + }; + } catch (e) { + print('[WebRTC] Error in initiateCall: $e'); + return {'success': false, 'callId': callId, 'error': e.toString()}; + } + } + + // Add getters for local/remote stream if needed for UI + MediaStream? get localStream => _localStream; + MediaStream? get remoteStream => _remoteStream; +} diff --git a/careconnect2025/frontend/lib/services/messaging_service.dart b/careconnect2025/frontend/lib/services/messaging_service.dart index 32cda0e9..fd0e8a85 100644 --- a/careconnect2025/frontend/lib/services/messaging_service.dart +++ b/careconnect2025/frontend/lib/services/messaging_service.dart @@ -1,96 +1,115 @@ import 'dart:convert'; import 'dart:math'; import 'package:http/http.dart' as http; -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:firebase_core/firebase_core.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'auth_token_manager.dart'; import 'api_service.dart'; -import 'firebase_auth_service.dart'; + +import 'package:web_socket_channel/web_socket_channel.dart'; +import '../config/env_constant.dart'; class MessagingService { - static const String _fcmUrl = - 'https://fcm.googleapis.com/v1/projects/careconnectptdemo/messages:send'; + // Send a notification/message to another user + static Future sendMessage({ + required String recipientId, + required String senderId, + required String senderName, + required String message, + required String messageType, // 'text', 'call_request', 'call_ended', etc. + Map? data, + }) async { + try { + if (_channel == null || !_isRegistered) { + print('WebSocket not connected or user not registered.'); + return false; + } + final msg = { + 'recipientId': recipientId, + 'senderId': senderId, + 'senderName': senderName, + 'message': message, + 'messageType': messageType, + 'timestamp': DateTime.now().toIso8601String(), + 'data': data ?? {}, + }; + _channel!.sink.add(jsonEncode(msg)); + // Store message locally + await _storeMessageLocally(senderId, recipientId, msg); + print('✅ Message sent via WebSocket and stored locally'); + return true; + } catch (e) { + print('❌ Error sending WebSocket message: $e'); + return false; + } + } - static FirebaseMessaging? _messaging; - static String? _currentUserToken; + static WebSocketChannel? _channel; + static bool _isRegistered = false; + static String? _currentUserId; static Map>> _localMessages = {}; - // Initialize messaging service with lazy loading + // Connect to WebSocket (no user registration yet) static Future initialize() async { - // Return immediately if already initialized - if (_messaging != null) return; - + if (_channel != null) return; // Already connected try { - // Initialize Firebase Cloud Messaging - print('📱 Initializing Firebase Cloud Messaging...'); - - // Firebase should already be initialized in main() - _messaging = FirebaseMessaging.instance; - - // Request permission for notifications - NotificationSettings settings = await _messaging!.requestPermission( - alert: true, - announcement: false, - badge: true, - carPlay: false, - criticalAlert: false, - provisional: false, - sound: true, + final wsUrl = _getWebSocketUrl(); + print('Connecting to notification WebSocket: $wsUrl'); + _channel = WebSocketChannel.connect(Uri.parse(wsUrl)); + _isRegistered = false; + + // Listen for incoming messages with robust error handling + _channel!.stream.listen( + (message) { + print('Received notification: $message'); + // Optionally handle incoming messages here + }, + onError: (e, stackTrace) { + print('WebSocket error: $e'); + if (stackTrace != null) { + print('WebSocket error stack: $stackTrace'); + } + _isRegistered = false; + // Do not rethrow, app should continue running + }, + onDone: () { + print('WebSocket connection closed'); + _isRegistered = false; + }, + cancelOnError: true, ); - if (settings.authorizationStatus == AuthorizationStatus.authorized) { - print('User granted permission'); - } else if (settings.authorizationStatus == - AuthorizationStatus.provisional) { - print('User granted provisional permission'); - } else { - print('User declined or has not accepted permission'); - } - - // Get the token (with web-specific handling) - if (kIsWeb) { - // For web, we might need to provide VAPID key - _currentUserToken = await _messaging!.getToken( - vapidKey: - 'BKn4_bZ8g2g7Qf8WlFxl6c2GkGtRfXq8w5A6Ly4R9s8X7NdJ5Q3zP1mO6kH4L2cV8sY7wE1nU9rT3vI2bN0pM6Q', // Replace with your VAPID key - ); - } else { - _currentUserToken = await _messaging!.getToken(); - } - print('FCM Token: $_currentUserToken'); - - // Set up foreground message handling - FirebaseMessaging.onMessage.listen((RemoteMessage message) { - print('Got a message whilst in the foreground!'); - print('Message data: ${message.data}'); - - if (message.notification != null) { - print( - 'Message also contained a notification: ${message.notification}', - ); + // Catch any uncaught errors from the stream (for extra safety) + _channel!.stream.handleError((error, stackTrace) { + print('WebSocket stream uncaught error: $error'); + if (stackTrace != null) { + print('WebSocket stream error stack: $stackTrace'); } + // Do not rethrow, app should continue running }); - // Handle background messages - FirebaseMessaging.onBackgroundMessage( - _firebaseMessagingBackgroundHandler, - ); - // Load local messages from storage await _loadLocalMessages(); - } catch (e) { - print('❌ Error initializing messaging service: $e'); + } catch (e, stackTrace) { + print('❌ Error initializing WebSocket messaging service: $e'); + print('Stack trace: $stackTrace'); + // Do not rethrow, app should continue running } } - // Background message handler - static Future _firebaseMessagingBackgroundHandler( - RemoteMessage message, - ) async { - await Firebase.initializeApp(); - print("Handling a background message: ${message.messageId}"); + // Register user after login + static Future registerUser({required String userId}) async { + if (_channel == null) await initialize(); + _currentUserId = userId; + final registerMsg = 'REGISTER_USER:$userId'; + _channel!.sink.add(registerMsg); + _isRegistered = true; + } + + // Helper to get the WebSocket URL from backend base URL + static String _getWebSocketUrl() { + // Always use the correct WebSocket endpoint for notifications + return getWebSocketNotificationUrl(); } // Load local messages from SharedPreferences @@ -119,77 +138,6 @@ class MessagingService { } } - // Get FCM token for current user - static Future getFCMToken() async { - try { - if (_messaging == null) { - await initialize(); - } - return await _messaging!.getToken(); - } catch (e) { - print('Error getting FCM token: $e'); - return null; - } - } - - // Get FCM access token using service account - static Future _getAccessToken() async { - try { - return await FirebaseAuthService.getAccessToken(); - } catch (e) { - print('❌ Error getting access token: $e'); - return null; - } - } - - // Send message between caregiver and patient - static Future sendMessage({ - required String recipientId, - required String senderId, - required String senderName, - required String message, - required String messageType, // 'text', 'call_request', 'call_ended' - Map? data, // Additional data for the message - }) async { - try { - final messageData = { - 'id': _generateMessageId(), - 'senderId': senderId, - 'senderName': senderName, - 'recipientId': recipientId, - 'message': message, - 'messageType': messageType, - 'timestamp': DateTime.now().toIso8601String(), - 'read': false, - }; - - // Store message locally - await _storeMessageLocally(senderId, recipientId, messageData); - - // Try to send FCM notification (will work when backend is ready) - try { - await _sendFCMNotification(recipientId, senderName, message); - } catch (e) { - print('FCM notification failed (backend not ready): $e'); - // Continue - message is stored locally - } - - // Try to store in backend (will fail gracefully if backend not ready) - try { - await _storeMessageInDatabase(messageData); - } catch (e) { - print('Backend storage failed (backend not ready): $e'); - // Continue - message is stored locally - } - - print('✅ Message sent successfully (stored locally)'); - return true; - } catch (e) { - print('❌ Error sending message: $e'); - return false; - } - } - // Generate unique message ID static String _generateMessageId() { return '${DateTime.now().millisecondsSinceEpoch}_${_generateRandomString(8)}'; @@ -238,93 +186,6 @@ class MessagingService { } } - // Send FCM notification - static Future _sendFCMNotification( - String recipientId, - String senderName, - String message, - ) async { - try { - final accessToken = await _getAccessToken(); - if (accessToken == null) { - print('⚠️ FCM notification skipped - no access token available'); - print( - ' This is normal if Firebase service account is not configured', - ); - return; - } - - final recipientToken = await _getRecipientFCMToken(recipientId); - if (recipientToken == null) return; - - final messageData = { - 'message': { - 'token': recipientToken, - 'notification': { - 'title': 'New Message from $senderName', - 'body': message, - }, - 'data': { - 'type': 'text', - 'senderId': recipientId, - 'senderName': senderName, - 'message': message, - 'timestamp': DateTime.now().toIso8601String(), - }, - 'android': { - 'priority': 'high', - 'notification': { - 'channel_id': 'careconnect_messages', - 'sound': 'default', - }, - }, - 'apns': { - 'payload': { - 'aps': {'sound': 'default', 'badge': 1}, - }, - }, - }, - }; - - final response = await http.post( - Uri.parse(_fcmUrl), - headers: { - 'Authorization': 'Bearer $accessToken', - 'Content-Type': 'application/json', - }, - body: jsonEncode(messageData), - ); - - if (response.statusCode == 200) { - print('✅ FCM notification sent successfully'); - } else { - print('❌ Failed to send FCM notification: ${response.statusCode}'); - } - } catch (e) { - print('❌ Error sending FCM notification: $e'); - } - } - - // Get recipient's FCM token from backend - static Future _getRecipientFCMToken(String userId) async { - try { - final headers = await AuthTokenManager.getAuthHeaders(); - final response = await http.get( - Uri.parse('${ApiConstants.baseUrl}users/$userId/fcm-token'), - headers: headers, - ); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - return data['fcmToken']; - } - return null; - } catch (e) { - print('❌ Error getting FCM token: $e'); - return null; - } - } - // Store message in backend database static Future _storeMessageInDatabase( Map messageData, @@ -416,7 +277,7 @@ class MessagingService { } // Mark messages as read - static Future markMessagesAsRead({ + static Future markMessagesAsRead({ required String conversationId, required String userId, }) async { @@ -427,9 +288,7 @@ class MessagingService { if (_localMessages[conversationKey] != null) { for (final message in _localMessages[conversationKey]!) { - if (message['recipientId'] == userId) { - message['read'] = true; - } + message['read'] = true; } await _saveLocalMessages(); } @@ -440,21 +299,21 @@ class MessagingService { await http.patch( Uri.parse('${ApiConstants.baseUrl}messages/mark-read'), headers: {...headers, 'Content-Type': 'application/json'}, - body: jsonEncode({ - 'conversationId': conversationId, - 'userId': userId, - }), + body: jsonEncode({'conversationId': conversationId}), ); + return true; } catch (e) { print('Backend not available for marking messages as read: $e'); + return false; } } catch (e) { print('❌ Error marking messages as read: $e'); + return false; } } - // Send video call invitation via FCM (cross-platform) - static Future sendVideoCallInvitation({ + // Send video call invitation via WebSocket + static Future sendVideoCallInvitation({ required String recipientId, required String callerId, required String callerName, @@ -462,9 +321,10 @@ class MessagingService { required bool isVideoCall, }) async { try { - print('📹 Sending ${isVideoCall ? 'video' : 'audio'} call invitation...'); - - final success = await sendMessage( + print( + '📹 Sending ${isVideoCall ? 'video' : 'audio'} call invitation via WebSocket...', + ); + await MessagingService.sendMessage( recipientId: recipientId, senderId: callerId, senderName: callerName, @@ -478,25 +338,58 @@ class MessagingService { 'action': 'call_invitation', }, ); - - return success; } catch (e) { print('❌ Error sending call invitation: $e'); - return false; } } // Get platform-specific features availability static Map getPlatformFeatures() { return { - 'videoCall': true, // Available on all platforms with Zego - 'audioCall': true, // Available on all platforms with Zego - 'sms': !kIsWeb, // SMS only available on mobile platforms - 'pushNotifications': true, // FCM available on all platforms - 'backgroundMessages': true, // FCM handles background on all platforms - 'webNotifications': kIsWeb, // Web-specific notifications + 'videoCall': true, + 'audioCall': true, + 'sms': !kIsWeb, + 'pushNotifications': true, // Now via WebSocket + 'backgroundMessages': true, + 'webNotifications': kIsWeb, }; } + + // Send notification to a user via HTTP endpoint that triggers WebSocket delivery + static Future sendHttpWebSocketNotification({ + required String userId, + required String message, + Map? extraHeaders, + }) async { + try { + final headers = { + 'Content-Type': 'application/json', + ...?extraHeaders, + }; + final url = + '${ApiConstants.baseUrl}notifications/ws/send-to-user/$userId'; + final response = await http.post( + Uri.parse(url), + headers: headers, + body: jsonEncode({'message': message}), + ); + if (response.statusCode == 200 || response.statusCode == 201) { + final resp = jsonDecode(response.body); + print( + '✅ WebSocket notification sent: \\${resp['message'] ?? response.body}', + ); + return true; + } else { + print( + '❌ Failed to send WebSocket notification: ${response.statusCode}', + ); + return false; + } + } catch (e) { + print('❌ Error sending WebSocket notification: $e'); + return false; + } + } } // Import this to avoid circular dependency diff --git a/careconnect2025/frontend/lib/services/notification_service.dart b/careconnect2025/frontend/lib/services/notification_service.dart new file mode 100644 index 00000000..95b2b873 --- /dev/null +++ b/careconnect2025/frontend/lib/services/notification_service.dart @@ -0,0 +1,21 @@ +import 'package:web_socket_channel/web_socket_channel.dart'; + +class NotificationService { + static WebSocketChannel? _channel; + static bool _isConnected = false; + + static Future initialize(String wsUrl) async { + if (_isConnected) return; + _channel = WebSocketChannel.connect(Uri.parse(wsUrl)); + _isConnected = true; + // Add listeners or authentication as needed + } + + static void dispose() { + _channel?.sink.close(); + _isConnected = false; + } + + static bool get isConnected => _isConnected; + static WebSocketChannel? get channel => _channel; +} diff --git a/careconnect2025/frontend/lib/services/real_video_call_service.dart b/careconnect2025/frontend/lib/services/real_video_call_service.dart index 3c3351f8..5153c25a 100644 --- a/careconnect2025/frontend/lib/services/real_video_call_service.dart +++ b/careconnect2025/frontend/lib/services/real_video_call_service.dart @@ -3,9 +3,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:socket_io_client/socket_io_client.dart' as io; -import '../config/environment_config.dart'; +import '../config/env_constant.dart'; import 'messaging_service.dart'; +import '../config/environment_config.dart'; + class RealVideoCallService { static bool _isInitialized = false; static RtcEngine? _agoraEngine; @@ -16,8 +18,8 @@ class RealVideoCallService { static MediaStream? _localStream; // Agora configuration - static String get _agoraAppId => EnvironmentConfig.agoraAppId; - static const String _agoraToken = ''; // Optional token for production + static String get _agoraAppId => getAgoraAppId(); + static const String _agoraToken = ''; /// Initialize the real video call service static Future initialize() async { @@ -79,10 +81,9 @@ class RealVideoCallService { static Future _initializeSignaling() async { print('🔄 Connecting to signaling server...'); - // In a real app, you'd have your own signaling server - // For now, we'll create a mock connection + final signalingUrl = getWebRTCSignalingServerUrl(); _signalingSocket = io.io( - 'https://your-signaling-server.com', + signalingUrl, io.OptionBuilder().setTransports(['websocket']).build(), ); @@ -163,8 +164,8 @@ class RealVideoCallService { } } - // Send call notification - final notificationSent = await MessagingService.sendVideoCallInvitation( + // Send call notification via WebSocket (fire-and-forget) + await MessagingService.sendVideoCallInvitation( recipientId: calleeId, callerId: callerId, callerName: callerName, @@ -172,11 +173,6 @@ class RealVideoCallService { isVideoCall: isVideoCall, ); - if (!notificationSent) { - print('❌ Failed to send call notification'); - return null; - } - return { 'callId': callId, 'platform': kIsWeb ? 'webrtc' : 'agora', diff --git a/careconnect2025/frontend/lib/services/video_call_service_base.dart b/careconnect2025/frontend/lib/services/video_call_service_base.dart new file mode 100644 index 00000000..75d6b30c --- /dev/null +++ b/careconnect2025/frontend/lib/services/video_call_service_base.dart @@ -0,0 +1,12 @@ +abstract class VideoCallServiceBase { + Future initializeService(); + Future checkUserAvailability(String userId); + Future> initiateCall({ + required String callId, + required String callerId, + required String recipientId, + required bool isVideoCall, + }); +} + +// You can add more methods as needed for your app's requirements. diff --git a/careconnect2025/frontend/lib/services/web_video_call_service.dart b/careconnect2025/frontend/lib/services/web_video_call_service.dart index eb5a2f55..4af3b63f 100644 --- a/careconnect2025/frontend/lib/services/web_video_call_service.dart +++ b/careconnect2025/frontend/lib/services/web_video_call_service.dart @@ -2,25 +2,22 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; -import 'package:socket_io_client/socket_io_client.dart' as io; + +import '../config/env_constant.dart'; +import 'webrtc_signaling.dart'; class WebVideoCallService { RTCPeerConnection? _peerConnection; MediaStream? _localStream; MediaStream? _remoteStream; - io.Socket? _socket; - bool _isInCall = false; String? _currentCallId; String? _currentUserId; - Function(MediaStream)? _onRemoteStreamReceived; Function()? _onCallEnded; - - // WebRTC renderers late RTCVideoRenderer _localRenderer; late RTCVideoRenderer _remoteRenderer; - + late WebRTCSignaling _signaling; bool get isInCall => _isInCall; String? get currentCallId => _currentCallId; @@ -33,37 +30,8 @@ class WebVideoCallService { _currentUserId = userId; _onRemoteStreamReceived = onRemoteStreamReceived; _onCallEnded = onCallEnded; - - try { - // Initialize socket connection for signaling - _socket = io.io('ws://localhost:3000', { - 'transports': ['websocket'], - 'autoConnect': false, - }); - - _socket!.connect(); - - _socket!.on('connect', (_) { - print('Connected to signaling server'); - }); - - _socket!.on('offer', (data) async { - await _handleOffer(data); - }); - - _socket!.on('answer', (data) async { - await _handleAnswer(data); - }); - - _socket!.on('ice-candidate', (data) async { - await _handleIceCandidate(data); - }); - - print('WebRTC service initialized for web platform'); - } catch (e) { - print('Error initializing WebRTC: $e'); - throw Exception('Failed to initialize WebRTC video service: $e'); - } + _signaling = WebRTCSignaling(getWebRTCSignalingServerUrl()); + print('WebRTC service initialized for web platform (HTTP signaling)'); } /// Initialize video renderers for WebRTC @@ -122,22 +90,20 @@ class WebVideoCallService { }; // Handle ICE candidates - _peerConnection!.onIceCandidate = (RTCIceCandidate candidate) { - _socket!.emit('ice-candidate', { - 'callId': callId, - 'candidate': candidate.toMap(), - }); + _peerConnection!.onIceCandidate = (RTCIceCandidate candidate) async { + await _signaling.sendSignal( + userId: recipientId, + message: 'ice-candidate:${candidate.toMap()}', + ); }; // Create and send offer RTCSessionDescription offer = await _peerConnection!.createOffer(); await _peerConnection!.setLocalDescription(offer); - - _socket!.emit('offer', { - 'callId': callId, - 'recipientId': recipientId, - 'offer': offer.toMap(), - }); + await _signaling.sendSignal( + userId: recipientId, + message: 'offer:${offer.toMap()}', + ); return _buildWebRTCVideoWidget(); } catch (e) { @@ -151,6 +117,7 @@ class WebVideoCallService { /// Join WebRTC call Future joinCall({ required String callId, + required String recipientId, bool isVideoEnabled = true, bool isAudioEnabled = true, }) async { @@ -190,15 +157,16 @@ class WebVideoCallService { } }; - _peerConnection!.onIceCandidate = (RTCIceCandidate candidate) { - _socket!.emit('ice-candidate', { - 'callId': callId, - 'candidate': candidate.toMap(), - }); + _peerConnection!.onIceCandidate = (RTCIceCandidate candidate) async { + // Use recipientId for outgoing ICE candidates in joinCall + await _signaling.sendSignal( + userId: recipientId, + message: 'ice-candidate:${candidate.toMap()}', + ); }; - // Join the call room - _socket!.emit('join-call', {'callId': callId}); + // Notify caller/host via HTTP-based notification if needed (optional) + // await _signaling.sendSignal(userId: , message: 'Joined video call'); return _buildWebRTCVideoWidget(); } catch (e) { @@ -270,55 +238,6 @@ class WebVideoCallService { } /// Handle WebRTC offer - Future _handleOffer(dynamic data) async { - try { - RTCSessionDescription offer = RTCSessionDescription( - data['offer']['sdp'], - data['offer']['type'], - ); - - await _peerConnection!.setRemoteDescription(offer); - - RTCSessionDescription answer = await _peerConnection!.createAnswer(); - await _peerConnection!.setLocalDescription(answer); - - _socket!.emit('answer', { - 'callId': data['callId'], - 'answer': answer.toMap(), - }); - } catch (e) { - print('Error handling offer: $e'); - } - } - - /// Handle WebRTC answer - Future _handleAnswer(dynamic data) async { - try { - RTCSessionDescription answer = RTCSessionDescription( - data['answer']['sdp'], - data['answer']['type'], - ); - - await _peerConnection!.setRemoteDescription(answer); - } catch (e) { - print('Error handling answer: $e'); - } - } - - /// Handle ICE candidate - Future _handleIceCandidate(dynamic data) async { - try { - RTCIceCandidate candidate = RTCIceCandidate( - data['candidate']['candidate'], - data['candidate']['sdpMid'], - data['candidate']['sdpMLineIndex'], - ); - - await _peerConnection!.addCandidate(candidate); - } catch (e) { - print('Error handling ICE candidate: $e'); - } - } /// Toggle microphone mute void _toggleMute() { @@ -343,15 +262,25 @@ class WebVideoCallService { _peerConnection = null; } - // Stop local stream + // Stop and disable local stream tracks (especially video) if (_localStream != null) { - _localStream!.getTracks().forEach((track) => track.stop()); + for (var track in _localStream!.getTracks()) { + try { + track.stop(); + track.enabled = false; + } catch (_) {} + } _localStream = null; } - // Stop remote stream + // Stop and disable remote stream tracks if (_remoteStream != null) { - _remoteStream!.getTracks().forEach((track) => track.stop()); + for (var track in _remoteStream!.getTracks()) { + try { + track.stop(); + track.enabled = false; + } catch (_) {} + } _remoteStream = null; } @@ -360,7 +289,13 @@ class WebVideoCallService { await _remoteRenderer.dispose(); // Disconnect socket - _socket?.emit('end-call', {'callId': _currentCallId}); + // Notify other party via HTTP-based notification + if (_currentCallId != null && _currentUserId != null) { + await _signaling.sendSignal( + userId: _currentUserId!, + message: 'call-ended', + ); + } } catch (e) { print('Error ending WebRTC call: $e'); } finally { @@ -378,7 +313,5 @@ class WebVideoCallService { /// Dispose service Future dispose() async { await endCall(); - _socket?.disconnect(); - _socket?.dispose(); } } diff --git a/careconnect2025/frontend/lib/services/webrtc_signaling.dart b/careconnect2025/frontend/lib/services/webrtc_signaling.dart new file mode 100644 index 00000000..7a07d767 --- /dev/null +++ b/careconnect2025/frontend/lib/services/webrtc_signaling.dart @@ -0,0 +1,20 @@ +import 'messaging_service.dart'; + +class WebRTCSignaling { + final String apiBaseUrl; + + WebRTCSignaling(this.apiBaseUrl); + + /// Send a signaling message to a user using HTTP notification API + Future sendSignal({ + required String userId, + required String message, + Map? extraHeaders, + }) async { + return await MessagingService.sendHttpWebSocketNotification( + userId: userId, + message: message, + extraHeaders: extraHeaders, + ); + } +} diff --git a/careconnect2025/frontend/lib/services/webrtc_signaling_service.dart b/careconnect2025/frontend/lib/services/webrtc_signaling_service.dart index 418197a1..db3f7de1 100644 --- a/careconnect2025/frontend/lib/services/webrtc_signaling_service.dart +++ b/careconnect2025/frontend/lib/services/webrtc_signaling_service.dart @@ -1,4 +1,6 @@ import 'dart:async'; + +import '../config/env_constant.dart'; import 'package:socket_io_client/socket_io_client.dart' as IO; class WebRTCSignalingService { @@ -7,8 +9,7 @@ class WebRTCSignalingService { static final Map _eventControllers = {}; // Signaling server URL (you can use socket.io test server or deploy your own) - static const String signalingServerUrl = - 'https://socket.io-chat-e9jt.herokuapp.com'; + static String get signalingServerUrl => getWebRTCSignalingServerUrl(); // Initialize signaling connection static Future initialize() async { diff --git a/careconnect2025/frontend/lib/utils/call_integration_helper.dart b/careconnect2025/frontend/lib/utils/call_integration_helper.dart index 9bb28b37..2097648d 100644 --- a/careconnect2025/frontend/lib/utils/call_integration_helper.dart +++ b/careconnect2025/frontend/lib/utils/call_integration_helper.dart @@ -672,18 +672,6 @@ class CallIntegrationHelper { 'description': 'Panic attack or severe anxiety episode', 'icon': Icons.psychology, }, - { - 'type': 'CHEST_PAIN', - 'title': 'Chest Pain', - 'description': 'Chest pain or heart-related emergency', - 'icon': Icons.favorite, - }, - { - 'type': 'BREATHING', - 'title': 'Breathing Difficulty', - 'description': 'Difficulty breathing or respiratory emergency', - 'icon': Icons.air, - }, { 'type': 'OTHER', 'title': 'Other Emergency', @@ -708,7 +696,11 @@ class CallIntegrationHelper { // Set a max height to prevent overflow child: SingleChildScrollView( child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 400), + constraints: BoxConstraints( + maxHeight: + MediaQuery.of(context).size.height * + 0.6, // 60% of screen height + ), child: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/careconnect2025/frontend/lib/widgets/ai_chat_consolidated.dart b/careconnect2025/frontend/lib/widgets/ai_chat_consolidated.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/careconnect2025/frontend/lib/widgets/ai_chat_improved.dart b/careconnect2025/frontend/lib/widgets/ai_chat_improved.dart index 087bdfca..79b01fc6 100644 --- a/careconnect2025/frontend/lib/widgets/ai_chat_improved.dart +++ b/careconnect2025/frontend/lib/widgets/ai_chat_improved.dart @@ -1,262 +1,156 @@ import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; +import '../services/ai_chat_service.dart'; +import '../config/theme/app_theme.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:provider/provider.dart'; +import '../providers/user_provider.dart'; import 'dart:io'; import 'dart:convert'; -import 'dart:math' as math; -import '../services/ai_service.dart'; -import '../providers/user_provider.dart'; -/// This is a consolidated AI Chat component that serves all use cases: -/// - Regular floating chat widget for analytics/patient dashboards -/// - Modal dialog chat for the caregiver dashboard +// Message model for chat +class ChatMessage { + final String text; + final bool isUser; + final DateTime timestamp; + final String? errorMessage; + + ChatMessage({ + required this.text, + required this.isUser, + required this.timestamp, + this.errorMessage, + }); +} + +// Helper class for uploaded files +class UploadedFile { + final String name; + final int size; + final String content; + final String type; + final List? bytes; + final String? path; + + UploadedFile({ + required this.name, + required this.size, + required this.content, + required this.type, + this.bytes, + this.path, + }); +} + +// (AIModel selection removed as requested) + +// ...existing widget classes below... class AIChat extends StatefulWidget { - final String role; // 'caregiver', 'patient', or 'analytics' - final String? healthDataContext; // Health data context for analytics role - final bool isModal; // Whether the chat is displayed in a modal - final int? patientId; // Patient ID for API calls - final int? userId; // User ID for API calls + final String role; + final String? healthDataContext; + final bool isModal; + final int? patientId; + final int? userId; const AIChat({ - super.key, + Key? key, required this.role, this.healthDataContext, this.isModal = false, this.patientId, this.userId, - }); + }) : super(key: key); @override State createState() => _AIChatState(); } class _AIChatState extends State with SingleTickerProviderStateMixin { - bool _isExpanded = false; - final List _messages = []; + String _conversationId = ""; final TextEditingController _controller = TextEditingController(); - final ScrollController _scrollController = ScrollController(); - bool _isLoading = false; - AIModel _selectedModel = AIModel.deepseek; - bool _hasSeenWelcome = false; - late AnimationController _animationController; - late Animation _animation; - double _chatHeight = 500.0; - double _chatWidth = 400.0; + final List _messages = []; final List _uploadedFiles = []; + bool _isLoading = false; bool _isFilePickerOpen = false; - - // Performance optimization: Cache for expensive operations - String? _cachedFileContext; - bool _fileContextNeedsUpdate = false; + double _chatWidth = 320.0; + double _chatHeight = 500.0; + late AnimationController _animationController; @override void initState() { super.initState(); _animationController = AnimationController( - duration: const Duration(milliseconds: 300), vsync: this, + duration: const Duration(milliseconds: 250), ); - _animation = CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - ); - - // Set default initial sizes before proper measurement - _chatHeight = 500.0; - _chatWidth = 400.0; - - // If this is a modal view, expand by default - if (widget.isModal) { - _isExpanded = true; - _animationController.value = 1.0; - } - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - final screenSize = MediaQuery.of(context).size; - - // Responsive sizing with constraints - only relevant for non-modal - if (!widget.isModal) { - // Limit the width based on screen size but with a hard cap - _chatWidth = screenSize.width < 768 ? 320.0 : 380.0; - - // Never let the chat width exceed 35% of the screen width - // Ensure max value is always greater than min value to avoid clamp errors - double maxWidth = screenSize.width * 0.35; - _chatWidth = _chatWidth.clamp(280.0, math.max(281.0, maxWidth)); - - // Adjust height based on screen size - _chatHeight = screenSize.height < 600 - ? 400.0 - : screenSize.height > 900 - ? 600.0 - : screenSize.height * 0.6; - _chatHeight = _chatHeight.clamp(400.0, 600.0); - } - - // Show welcome message for modal view - if (widget.isModal && !_hasSeenWelcome) { - _hasSeenWelcome = true; - - // Customize welcome message based on role - String welcomeMessage; - if (widget.role == 'analytics') { - welcomeMessage = - 'Welcome to the Healthcare Analytics Assistant. How can I help you analyze your data today?'; - } else if (widget.role == 'caregiver') { - welcomeMessage = - 'Welcome to the Caregiver Assistant. I can help you with patient information, care protocols, and medical references.'; - } else { - welcomeMessage = - 'Welcome to CareConnect AI Assistant. How can I help you today?'; - } - - // Add system welcome message - _messages.add( - ChatMessage( - text: welcomeMessage, - isUser: false, - timestamp: DateTime.now(), - ), - ); - } } @override void dispose() { - _animationController.dispose(); _controller.dispose(); - _scrollController.dispose(); + _animationController.dispose(); super.dispose(); } - void _scrollToBottom() { - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } - } - Future _pickFiles() async { - if (_isFilePickerOpen) return; - - setState(() { - _isFilePickerOpen = true; - }); - + setState(() => _isFilePickerOpen = true); try { FilePickerResult? result = await FilePicker.platform.pickFiles( - type: FileType.any, allowMultiple: true, - allowedExtensions: null, ); - if (result != null) { - for (PlatformFile file in result.files) { - try { - UploadedFile? uploadedFile = await _processFile(file); - if (uploadedFile != null) { - setState(() { - _uploadedFiles.add(uploadedFile); - _fileContextNeedsUpdate = true; // Mark cache as needing update - }); - } - } catch (e) { - debugPrint('Error processing file ${file.name}: $e'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error processing file ${file.name}: $e'), - backgroundColor: Theme.of(context).colorScheme.error, - duration: const Duration(seconds: 3), - ), - ); + for (var file in result.files) { + final uploaded = await _processFile(file); + if (uploaded != null) { + setState(() { + _uploadedFiles.add(uploaded); + }); } } - - if (_uploadedFiles.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '${result.files.length} file(s) uploaded successfully', - ), - backgroundColor: Theme.of(context).primaryColor, - duration: const Duration(seconds: 2), - ), - ); - } } - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error picking files: $e'), - backgroundColor: Theme.of(context).colorScheme.error, - duration: const Duration(seconds: 3), - ), - ); } finally { - setState(() { - _isFilePickerOpen = false; - }); + setState(() => _isFilePickerOpen = false); } } Future _processFile(PlatformFile file) async { final fileType = _getFileType(file.name); - - // Check if file is too large (limit to 10MB) if (file.size > 10 * 1024 * 1024) { throw Exception('File ${file.name} is too large (max 10MB)'); } - String content; - try { if (fileType == 'pdf' || fileType == 'document') { - // For PDFs and documents, we'll store a placeholder message - // In a real app, you'd want to use a PDF parser or document converter content = '[This is a ${fileType.toUpperCase()} file. Content extraction not yet implemented for this file type. File name: ${file.name}]'; } else { - // For text-based files, read the content if (file.bytes != null) { - // For web, use bytes and handle encoding properly try { content = utf8.decode(file.bytes!); } catch (e) { - // If UTF-8 decoding fails, try latin1 content = latin1.decode(file.bytes!); } } else if (file.path != null) { - // For mobile/desktop, read file content try { content = await File(file.path!).readAsString(encoding: utf8); } catch (e) { - // If UTF-8 fails, try latin1 content = await File(file.path!).readAsString(encoding: latin1); } } else { throw Exception('Unable to read file content'); } } - - // Limit content size to prevent overwhelming the AI if (content.length > 50000) { content = '${content.substring(0, 50000)}\n... [Content truncated due to length]'; } - return UploadedFile( name: file.name, size: file.size, content: content, type: fileType, + bytes: file.bytes, + path: file.path, ); } catch (e) { debugPrint('Error reading file ${file.name}: $e'); @@ -319,172 +213,78 @@ class _AIChatState extends State with SingleTickerProviderStateMixin { void _removeFile(int index) { setState(() { _uploadedFiles.removeAt(index); - _fileContextNeedsUpdate = true; // Mark cache as needing update }); } - String _getFileContextForAI() { - // Return cached value if available and not needing update - if (_cachedFileContext != null && !_fileContextNeedsUpdate) { - return _cachedFileContext!; - } - - if (_uploadedFiles.isEmpty) { - _cachedFileContext = ''; - _fileContextNeedsUpdate = false; - return ''; - } - - StringBuffer buffer = StringBuffer(); - buffer.writeln('--- UPLOADED FILES CONTENT ---'); - - for (int i = 0; i < _uploadedFiles.length; i++) { - final file = _uploadedFiles[i]; - buffer.writeln('[FILE ${i + 1}: ${file.name} (${file.type})]'); - buffer.writeln(file.content); - buffer.writeln('--- END OF FILE ${i + 1} ---'); - buffer.writeln(); - } - - _cachedFileContext = buffer.toString(); - _fileContextNeedsUpdate = false; - - return _cachedFileContext!; - } - - IconData _getFileIcon(String type) { - switch (type) { - case 'text': - return Icons.text_snippet; - case 'csv': - return Icons.table_chart; - case 'json': - return Icons.code; - case 'xml': - return Icons.code; - case 'pdf': - return Icons.picture_as_pdf; - case 'document': - return Icons.description; - case 'spreadsheet': - return Icons.grid_on; - case 'html': - return Icons.web; - case 'code': - return Icons.code; - case 'image': - return Icons.image; - default: - return Icons.insert_drive_file; - } - } - - String _formatFileSize(int bytes) { - if (bytes < 1024) return '${bytes}B'; - if (bytes < 1048576) return '${(bytes / 1024).toStringAsFixed(1)}KB'; - return '${(bytes / 1048576).toStringAsFixed(1)}MB'; - } - - void _toggleChat() { - if (!_isExpanded) { - // Expand the chat - setState(() { - _isExpanded = true; - }); - _animationController.reset(); - _animationController.forward(); - // Show welcome message on first expansion - if (!_hasSeenWelcome) { - _hasSeenWelcome = true; - // Customize welcome message based on role - String welcomeMessage = 'Hello! I\'m your health assistant. '; - - if (widget.role == 'analytics') { - welcomeMessage += - 'I can help analyze health data and provide insights based on the information you share. You can upload files like health reports or ask questions about the displayed analytics.'; - } else if (widget.role == 'caregiver') { - welcomeMessage += - 'I can help with patient care questions, medical information, and treatment guidance. You can also upload patient reports or health documents for analysis.'; - } else { - welcomeMessage += - 'I can help with health, wellness, and medical questions. You can also upload your health documents or reports for personalized advice.'; - } - - _messages.add( - ChatMessage( - text: welcomeMessage, - isUser: false, - timestamp: DateTime.now(), - ), - ); - // Scroll to bottom after adding welcome message - WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom()); - } - } else { - // Collapse the chat - _animationController.reverse().then((_) { - setState(() { - _isExpanded = false; - }); - }); - } - } - void _sendMessage() async { if (_controller.text.trim().isEmpty) return; - final userMessage = _controller.text.trim(); _controller.clear(); - setState(() { _messages.add( ChatMessage(text: userMessage, isUser: true, timestamp: DateTime.now()), ); _isLoading = true; }); - - // Scroll to bottom after adding user message WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom()); - try { - // Get user info from provider if not provided as parameters - final userProvider = Provider.of(context, listen: false); - final currentUserId = widget.userId ?? userProvider.user?.id ?? 1; - final currentPatientId = widget.patientId ?? userProvider.user?.id ?? 1; - - // Combine health data context with file context - String combinedContext = ''; - if (widget.healthDataContext != null && - widget.healthDataContext!.isNotEmpty) { - combinedContext += widget.healthDataContext!; - } - - final fileContext = _getFileContextForAI(); - if (fileContext.isNotEmpty) { - if (combinedContext.isNotEmpty) { - combinedContext += '\n\n'; - } - combinedContext += fileContext; + final userProvider = mounted + ? Provider.of(context, listen: false) + : null; + final currentUserId = widget.userId ?? userProvider?.user?.id ?? 1; + final currentPatientId = widget.patientId ?? userProvider?.user?.id ?? 1; + + // Prepare uploadedFiles for API if any + List>? uploadedFilesJson; + if (_uploadedFiles.isNotEmpty) { + uploadedFilesJson = _uploadedFiles.map((file) { + List? fileBytes = file.bytes; + if (fileBytes == null && file.path != null) { + try { + fileBytes = File(file.path!).readAsBytesSync(); + } catch (_) {} + } + String? base64Content = fileBytes != null + ? base64Encode(fileBytes) + : null; + String contentType = _guessMimeType(file.name); + return { + 'filename': file.name, + 'content': base64Content ?? '', + 'contentType': contentType, + }; + }).toList(); } - final response = await AIService.askAI( - userMessage, - role: widget.role, - model: _selectedModel, - healthDataContext: combinedContext.isNotEmpty ? combinedContext : null, + // Only these fields are dynamic for the request + final response = await AIChatService.sendMessage( + message: userMessage, patientId: currentPatientId, userId: currentUserId, - context: context, // Pass context for subscription checks + conversationId: _conversationId.isNotEmpty ? _conversationId : null, + uploadedFiles: uploadedFilesJson, ); - + final aiText = response['aiResponse'] ?? 'No response.'; + final errorMsg = + (response['errorMessage'] != null && + (response['errorMessage'] as String).isNotEmpty) + ? response['errorMessage'] + : null; + // Update conversationId for next request + if (response['conversationId'] != null && response['conversationId'] is String) { + _conversationId = response['conversationId']; + } setState(() { _messages.add( - ChatMessage(text: response, isUser: false, timestamp: DateTime.now()), + ChatMessage( + text: aiText, + isUser: false, + timestamp: DateTime.now(), + errorMessage: errorMsg, + ), ); _isLoading = false; }); - - // Scroll to bottom after adding AI response WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom()); } catch (e) { setState(() { @@ -497,609 +297,247 @@ class _AIChatState extends State with SingleTickerProviderStateMixin { ); _isLoading = false; }); - // Scroll to bottom after adding error message WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom()); } } - @override - Widget build(BuildContext context) { - final screenSize = MediaQuery.of(context).size; - - // Responsive sizing with constraints - only relevant for non-modal - if (!widget.isModal) { - // Limit the width based on screen size but with a hard cap - _chatWidth = screenSize.width < 768 ? 320.0 : 380.0; - - // Never let the chat width exceed 35% of the screen width - // This ensures it stays properly on the right side and doesn't take too much space - double maxWidth = screenSize.width * 0.35; - _chatWidth = _chatWidth.clamp(280.0, math.max(281.0, maxWidth)); - - // Adjust height based on screen size - _chatHeight = screenSize.height < 600 - ? screenSize.height * 0.7 - : screenSize.height * 0.6; - - // Ensure height stays within reasonable bounds - _chatHeight = _chatHeight.clamp(400.0, 600.0); + String _guessMimeType(String fileName) { + final ext = fileName.split('.').last.toLowerCase(); + switch (ext) { + case 'pdf': + return 'application/pdf'; + case 'txt': + return 'text/plain'; + case 'csv': + return 'text/csv'; + case 'json': + return 'application/json'; + case 'xml': + return 'application/xml'; + case 'jpg': + case 'jpeg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + case 'svg': + return 'image/svg+xml'; + case 'doc': + case 'docx': + return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + case 'xls': + case 'xlsx': + return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + default: + return 'application/octet-stream'; } + } - // The main chat content - Widget chatContent = Container( - width: widget.isModal ? double.infinity : _chatWidth, - height: widget.isModal ? double.infinity : _chatHeight, - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: widget.isModal - ? const BorderRadius.vertical(top: Radius.circular(16)) - : BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Theme.of(context).shadowColor.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 5), - ), - ], - ), - child: Column( - children: [ - // Chat header - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: widget.isModal - ? const BorderRadius.vertical(top: Radius.circular(16)) - : const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: Row( + void _scrollToBottom() { + // Implement scroll logic if using a ScrollController + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + return Material( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + child: Container( + width: _chatWidth, + height: _chatHeight, + padding: const EdgeInsets.all(12), + child: Column( + children: [ + // Chat header + Row( children: [ - Icon( - Icons.smart_toy, - color: Theme.of(context).colorScheme.onPrimary, - size: 20, - ), + Icon(Icons.smart_toy, color: colorScheme.primary), const SizedBox(width: 8), Expanded( - child: Text( - 'Health Assistant', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), + child: Text('AI Chat', style: theme.textTheme.titleMedium), ), - // Model selector dropdown - DropdownButton( - value: _selectedModel, - onChanged: (AIModel? newModel) { - if (newModel != null) { - setState(() { - _selectedModel = newModel; - }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Switched to ${newModel.displayName}'), - duration: const Duration(seconds: 2), - backgroundColor: Theme.of(context).primaryColor, - ), - ); - } - }, - dropdownColor: Theme.of(context).primaryColor, - icon: Icon( - Icons.keyboard_arrow_down, - color: Theme.of(context).colorScheme.onPrimary, - size: 16, - ), - underline: Container(), - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, - fontSize: 10, - ), - items: AIModel.values.map((AIModel model) { - return DropdownMenuItem( - value: model, - child: Text( - model.displayName, - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, - fontSize: 10, - ), - ), - ); - }).toList(), - ), - if (!widget.isModal) ...[ - const SizedBox(width: 8), - // Minimize button + if (widget.isModal) IconButton( - icon: Icon( - Icons.minimize, - color: Theme.of(context).colorScheme.onPrimary, - size: 20, - ), - onPressed: _toggleChat, - tooltip: 'Minimize', + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).maybePop(), ), - ], - // Close button - IconButton( - icon: Icon( - Icons.close, - color: Theme.of(context).colorScheme.onPrimary, - size: 20, - ), - onPressed: () { - if (widget.isModal) { - Navigator.of(context).pop(); - } else { - _toggleChat(); - // Clear messages when closing - setState(() { - _messages.clear(); - }); - } - }, - tooltip: 'Close', - ), ], ), - ), - // Chat messages - Expanded( - child: Column( - children: [ - // File upload area - if (_uploadedFiles.isNotEmpty) - Container( - margin: const EdgeInsets.all(8), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Theme.of(context).primaryColor.withOpacity(0.3), + Divider(color: colorScheme.outlineVariant), + // Message list + Expanded( + child: ListView.builder( + reverse: false, + itemCount: _messages.length, + itemBuilder: (context, index) { + final msg = _messages[index]; + return Align( + alignment: msg.isUser + ? Alignment.centerRight + : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 12, ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.attach_file, - size: 16, - color: Theme.of(context).primaryColor, + decoration: BoxDecoration( + color: msg.isUser + ? AppTheme.chatUserMessage + : colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: colorScheme.outlineVariant), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MarkdownBody( + data: msg.text, + shrinkWrap: true, + styleSheet: MarkdownStyleSheet( + p: msg.isUser + ? theme.textTheme.bodyMedium?.copyWith( + color: AppTheme.chatTextOnPrimary, + ) + : theme.textTheme.bodyMedium, ), - const SizedBox(width: 4), + ), + if (msg.errorMessage != null) Text( - 'Uploaded Files (${_uploadedFiles.length})', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Theme.of(context).primaryColor, + msg.errorMessage!, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.error, ), ), - ], - ), - const SizedBox(height: 8), - for (int i = 0; i < _uploadedFiles.length; i++) - Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( - children: [ - Icon( - _getFileIcon(_uploadedFiles[i].type), - size: 14, - color: Theme.of(context).primaryColor, - ), - const SizedBox(width: 4), - Expanded( - child: Text( - _uploadedFiles[i].name, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith(fontWeight: FontWeight.w500), - overflow: TextOverflow.ellipsis, - ), - ), - Text( - _formatFileSize(_uploadedFiles[i].size), - style: Theme.of(context).textTheme.labelSmall - ?.copyWith( - color: Theme.of( - context, - ).textTheme.bodySmall?.color, - ), - ), - const SizedBox(width: 4), - InkWell( - onTap: () => _removeFile(i), - child: Icon( - Icons.close, - size: 14, - color: Theme.of(context).colorScheme.error, - ), - ), - ], + Text( + _formatTimestamp(msg.timestamp), + style: theme.textTheme.labelSmall?.copyWith( + color: colorScheme.outline, ), ), - ], - ), - ), - // Messages list - Expanded( - child: ListView.builder( - controller: _scrollController, - padding: const EdgeInsets.all(12), - itemCount: _messages.length, - itemBuilder: (context, index) { - final message = _messages[index]; - return _buildMessageBubble(message); - }, - ), - ), - ], - ), - ), - // Loading indicator - if (_isLoading) - Container( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - const SizedBox(width: 8), - Text( - 'AI is thinking...', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: Theme.of(context).textTheme.bodySmall?.color, + ], + ), ), - ), - ], + ); + }, ), ), - // Input field - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.9), - borderRadius: widget.isModal - ? BorderRadius.zero - : const BorderRadius.only( - bottomLeft: Radius.circular(16), - bottomRight: Radius.circular(16), + // File preview (if any files uploaded) + if (_uploadedFiles.isNotEmpty) + Container( + margin: const EdgeInsets.only(top: 8, bottom: 4), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: colorScheme.outlineVariant), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Files to upload:', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + ), ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, + const SizedBox(height: 4), + ..._uploadedFiles.asMap().entries.map((entry) { + final idx = entry.key; + final file = entry.value; + return Row( + children: [ + Icon( + Icons.insert_drive_file, + size: 18, + color: colorScheme.primary, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + file.name, + style: theme.textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: Icon( + Icons.close, + size: 18, + color: colorScheme.error, + ), + onPressed: () => _removeFile(idx), + tooltip: 'Remove', + ), + ], + ); + }).toList(), + ], + ), + ), + // Input row + Row( children: [ - // File upload button IconButton( - icon: Icon( - Icons.attach_file, - color: _isFilePickerOpen - ? Theme.of(context).disabledColor - : Theme.of(context).primaryColor, - size: 20, - ), + icon: Icon(Icons.attach_file, color: colorScheme.primary), onPressed: _isFilePickerOpen ? null : _pickFiles, - tooltip: 'Upload files', + tooltip: 'Attach file', ), Expanded( - child: Container( - constraints: const BoxConstraints( - maxHeight: 120, // Max height for multi-line input - ), - child: TextField( - controller: _controller, - maxLines: null, - minLines: 1, - textInputAction: TextInputAction.newline, - decoration: InputDecoration( - hintText: widget.role == 'analytics' - ? 'Ask about the health data or upload files...' - : widget.role == 'caregiver' - ? 'Ask about patient care or upload documents...' - : 'Ask a health question or upload documents...', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Theme.of(context).primaryColor, - width: 2, - ), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, + child: TextField( + controller: _controller, + minLines: 1, + maxLines: 4, + decoration: InputDecoration( + hintText: 'Type your message...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: colorScheme.outlineVariant, ), ), - onSubmitted: (value) { - // Allow Enter to send message when it's a simple text - if (!value.contains('\n')) { - _sendMessage(); - } - }, + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), ), + onSubmitted: (_) => _sendMessage(), + enabled: !_isLoading, + style: theme.textTheme.bodyMedium, ), ), - const SizedBox(width: 8), IconButton( - icon: Icon(Icons.send, color: Theme.of(context).primaryColor), - onPressed: _sendMessage, - ), - ], - ), - ), - ], - ), - ); - - // If in modal mode, return just the content - if (widget.isModal) { - return chatContent; - } - - // If not in modal mode, handle floating behavior - return Positioned( - bottom: 16, - right: 16, // Position on the right side - width: _chatWidth, // Set explicit width to constrain it - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, // Align to bottom - crossAxisAlignment: CrossAxisAlignment.end, // Align to right - children: [ - // Only show chat content when expanded - if (_isExpanded) - AnimatedBuilder( - animation: _animation, - builder: (context, child) { - return Transform.scale( - scale: _animation.value, - child: chatContent, - ); - }, - ), - const SizedBox(height: 8), - // Floating action button - only show when not in modal - if (!widget.isModal) - FloatingActionButton.extended( - backgroundColor: Theme.of(context).primaryColor, - foregroundColor: Theme.of(context).colorScheme.onPrimary, - icon: _isExpanded - ? const Icon(Icons.keyboard_arrow_down) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.smart_toy, size: 20), - const SizedBox(width: 4), - Icon( - Icons.settings, - size: 12, - color: Theme.of( - context, - ).colorScheme.onPrimary.withOpacity(0.7), - ), - ], - ), - label: Text( - 'Ask AI (${_selectedModel.displayName})', - style: const TextStyle(fontSize: 12), - ), - onPressed: _toggleChat, - ), - ], - ), - ); - } - - Widget _buildMessageBubble(ChatMessage message) { - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Row( - mainAxisAlignment: message.isUser - ? MainAxisAlignment.end - : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!message.isUser) ...[ - CircleAvatar( - radius: 12, - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.2), - child: Icon( - Icons.smart_toy, - size: 16, - color: Theme.of(context).primaryColor, - ), - ), - const SizedBox(width: 8), - ], - Flexible( - child: Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.7, - ), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: message.isUser - ? Theme.of(context).primaryColor - : Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Theme.of(context).shadowColor.withOpacity(0.1), - blurRadius: 3, - offset: const Offset(0, 1), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Use markdown formatting for AI responses, plain text for user messages - message.isUser - ? Text( - message.text, - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, - fontSize: 14, - height: 1.4, + icon: _isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: colorScheme.primary, ), ) - : MarkdownBody( - data: message.text, - styleSheet: MarkdownStyleSheet( - p: TextStyle( - color: Theme.of( - context, - ).textTheme.bodyLarge?.color, - fontSize: 14, - height: 1.4, - ), - h1: TextStyle( - color: Theme.of( - context, - ).textTheme.bodyLarge?.color, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - h2: TextStyle( - color: Theme.of( - context, - ).textTheme.bodyLarge?.color, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - h3: TextStyle( - color: Theme.of( - context, - ).textTheme.bodyLarge?.color, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - strong: TextStyle( - color: Theme.of( - context, - ).textTheme.bodyLarge?.color, - fontWeight: FontWeight.bold, - ), - em: TextStyle( - color: Theme.of( - context, - ).textTheme.bodyLarge?.color, - fontStyle: FontStyle.italic, - ), - code: TextStyle( - backgroundColor: Theme.of(context).cardColor, - color: Theme.of( - context, - ).textTheme.bodyLarge?.color, - fontFamily: 'monospace', - fontSize: 13, - ), - codeblockDecoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(4), - ), - listBullet: TextStyle( - color: Theme.of( - context, - ).textTheme.bodyLarge?.color, - ), - ), - ), - const SizedBox(height: 4), - Text( - '${message.timestamp.hour.toString().padLeft(2, '0')}:${message.timestamp.minute.toString().padLeft(2, '0')}', - style: TextStyle( - color: message.isUser - ? Theme.of( - context, - ).colorScheme.onPrimary.withOpacity(0.7) - : Theme.of(context).textTheme.bodySmall?.color, - fontSize: 10, - ), - ), - ], - ), - ), - ), - if (message.isUser) ...[ - const SizedBox(width: 8), - CircleAvatar( - radius: 12, - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.2), - child: Icon( - Icons.person, - size: 16, - color: Theme.of(context).primaryColor, - ), + : Icon(Icons.send, color: colorScheme.primary), + onPressed: _isLoading ? null : _sendMessage, + tooltip: 'Send', + ), + ], ), ], - ], + ), ), ); } -} - -class ChatMessage { - final String text; - final bool isUser; - final DateTime timestamp; - - ChatMessage({ - required this.text, - required this.isUser, - required this.timestamp, - }); -} - -class UploadedFile { - final String name; - final int size; - final String content; - final String type; - - UploadedFile({ - required this.name, - required this.size, - required this.content, - required this.type, - }); -} -// Modal wrapper for AI Chat -class AIChatModal extends StatelessWidget { - final String role; - final String? healthDataContext; - - const AIChatModal({super.key, required this.role, this.healthDataContext}); - - @override - Widget build(BuildContext context) { - return SafeArea( - child: AIChat( - role: role, - healthDataContext: healthDataContext, - isModal: true, - ), - ); + String _formatTimestamp(DateTime dt) { + final now = DateTime.now(); + if (now.difference(dt).inDays == 0) { + return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + } else { + return '${dt.month}/${dt.day}/${dt.year}'; + } } } diff --git a/careconnect2025/frontend/lib/widgets/call_notification_status_indicator.dart b/careconnect2025/frontend/lib/widgets/call_notification_status_indicator.dart index b43090c8..7930c155 100644 --- a/careconnect2025/frontend/lib/widgets/call_notification_status_indicator.dart +++ b/careconnect2025/frontend/lib/widgets/call_notification_status_indicator.dart @@ -21,12 +21,23 @@ class _CallNotificationStatusIndicatorState Widget build(BuildContext context) { final isConnected = CallNotificationService.isConnected; + final theme = Theme.of(context); + final colorOnline = theme.colorScheme.secondary; + final colorConnecting = theme.colorScheme.tertiary; + final colorDisabled = theme.disabledColor; + final statusColor = _getStatusColor( + theme, + colorOnline, + colorConnecting, + colorDisabled, + ); + return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: _getStatusColor().withOpacity(0.1), + color: statusColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), - border: Border.all(color: _getStatusColor().withOpacity(0.3), width: 1), + border: Border.all(color: statusColor.withOpacity(0.3), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -35,7 +46,7 @@ class _CallNotificationStatusIndicatorState width: 8, height: 8, decoration: BoxDecoration( - color: _getStatusColor(), + color: statusColor, shape: BoxShape.circle, ), ), @@ -43,7 +54,7 @@ class _CallNotificationStatusIndicatorState Text( _getStatusText(), style: TextStyle( - color: _getStatusColor(), + color: statusColor, fontSize: 12, fontWeight: FontWeight.w500, ), @@ -53,9 +64,14 @@ class _CallNotificationStatusIndicatorState ); } - Color _getStatusColor() { - if (!widget.isInitialized) return Colors.grey; - return CallNotificationService.isConnected ? Colors.green : Colors.orange; + Color _getStatusColor( + ThemeData theme, + Color colorOnline, + Color colorConnecting, + Color colorDisabled, + ) { + if (!widget.isInitialized) return colorDisabled; + return CallNotificationService.isConnected ? colorOnline : colorConnecting; } String _getStatusText() { diff --git a/careconnect2025/frontend/lib/widgets/file_upload_widget.dart b/careconnect2025/frontend/lib/widgets/file_upload_widget.dart index 5a2a9000..67fad6e0 100644 --- a/careconnect2025/frontend/lib/widgets/file_upload_widget.dart +++ b/careconnect2025/frontend/lib/widgets/file_upload_widget.dart @@ -1,9 +1,6 @@ import 'dart:io'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../services/auth_token_manager.dart'; import '../services/comprehensive_file_service.dart'; import '../services/enhanced_file_service.dart'; import '../providers/user_provider.dart'; @@ -38,8 +35,6 @@ class _FileUploadWidgetState extends State { FileCategory? _selectedCategory; bool _isUploading = false; File? _selectedFile; - Uint8List? _selectedFileBytes; - String? _selectedFileName; final TextEditingController _descriptionController = TextEditingController(); @override @@ -48,7 +43,6 @@ class _FileUploadWidgetState extends State { final categories = _availableCategories; } - @override void dispose() { _descriptionController.dispose(); @@ -56,7 +50,8 @@ class _FileUploadWidgetState extends State { } List get _availableCategories1 { - if (widget.allowedCategories != null && widget.allowedCategories!.isNotEmpty) { + if (widget.allowedCategories != null && + widget.allowedCategories!.isNotEmpty) { return widget.allowedCategories!; } @@ -65,44 +60,45 @@ class _FileUploadWidgetState extends State { } List get _availableCategories { - if (widget.allowedCategories != null && widget.allowedCategories!.isNotEmpty) { + if (widget.allowedCategories != null && + widget.allowedCategories!.isNotEmpty) { return widget.allowedCategories!; } else { return FileCategory.values; } } - @override Widget build(BuildContext context) { return Card( + color: AppTheme.primary, child: Padding( padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildHeader(), - const SizedBox(height: 16), - if (widget.showCategorySelector) ...[ - _buildCategorySelector(), - const SizedBox(height: 16), - ], - _buildFileSelector(), - if (_selectedFile != null) ...[ - const SizedBox(height: 16), - /// Remove build file preview as it is not currently supported - /// _buildFilePreview(), - /// const SizedBox(height: 16), - _buildDescriptionField(), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildHeader(), const SizedBox(height: 16), + if (widget.showCategorySelector) ...[ + _buildCategorySelector(), + const SizedBox(height: 16), + ], + _buildFileSelector(), + if (_selectedFile != null) ...[ + const SizedBox(height: 16), + _buildFilePreview(), + const SizedBox(height: 16), + _buildDescriptionField(), + const SizedBox(height: 16), + ], + _buildUploadButton(), + if (_isUploading) ...[ + const SizedBox(height: 16), + const LinearProgressIndicator(), + ], ], - const SizedBox(height: 16), - _buildUploadButton(), - if (_isUploading) ...[ - const SizedBox(height: 16), - const LinearProgressIndicator(), - ], - ], + ), ), ), ); @@ -111,12 +107,16 @@ class _FileUploadWidgetState extends State { Widget _buildHeader() { return Row( children: [ - Icon(Icons.cloud_upload, color: Theme.of(context).colorScheme.primary), + const Icon(Icons.cloud_upload, color: Colors.white), const SizedBox(width: 8), Expanded( child: Text( widget.customTitle ?? 'Upload File', - style: Theme.of(context).textTheme.headlineSmall, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 20, + ), ), ), ], @@ -148,11 +148,14 @@ class _FileUploadWidgetState extends State { } return null; }, - value: _selectedCategory, // Starts as null! - hint: const Text('Select Category'), // This shows when value is null + value: _selectedCategory, // Starts as null! + hint: const Text('Select Category'), // This shows when value is null decoration: InputDecoration( border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), ), ); } @@ -200,9 +203,8 @@ class _FileUploadWidgetState extends State { const SizedBox(height: 8), Text( _selectedFile != null - ? 'File Selected: ${_selectedFile!.path}' - : _selectedFileName != null ? 'File Selected: $_selectedFileName' : - _getFileInstructions(), + ? 'File Selected: ${_selectedFile!.path.split('/').last}' + : _getFileInstructions(), textAlign: TextAlign.center, style: TextStyle( color: _selectedFile != null @@ -305,26 +307,23 @@ class _FileUploadWidgetState extends State { Widget _buildUploadButton() { final canUpload = - _selectedCategory != null && - (_selectedFile != null || - (_selectedFileBytes != null && _selectedFileName != null)) - && !_isUploading; + _selectedCategory != null && _selectedFile != null && !_isUploading; return ElevatedButton.icon( - onPressed: canUpload ? _uploadFileWeb : null, - style: canUpload - ? Theme.of(context).elevatedButtonTheme.style - : ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).disabledColor, - foregroundColor: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.38), - ), + onPressed: canUpload ? _uploadFile : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: AppTheme.primary, + textStyle: const TextStyle(fontWeight: FontWeight.bold), + ), icon: _isUploading ? const SizedBox( width: 20, height: 20, - child: CircularProgressIndicator(strokeWidth: 2), + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppTheme.primary, + ), ) : const Icon(Icons.cloud_upload), label: Text(_isUploading ? 'Uploading...' : 'Upload File'), @@ -395,26 +394,29 @@ class _FileUploadWidgetState extends State { } try { - if (kIsWeb) { - final (Uint8List, String)? webSelectedFile = await ComprehensiveFileService.pickFileForCategoryWeb( - _selectedCategory!, - ); - if (webSelectedFile != null) { - setState(() { - _selectedFileBytes = webSelectedFile.$1; - _selectedFileName = webSelectedFile.$2; - }); - } - } - else { - final File? file = await ComprehensiveFileService.pickFileForCategory( + final file = await ComprehensiveFileService.pickFileForCategory( + _selectedCategory!, + ); + if (file != null) { + // Validate file + if (!ComprehensiveFileService.validateFileForCategory( + file, _selectedCategory!, - ); - if (file != null) { - setState(() { - _selectedFile = file; - }); + )) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Invalid file type for ${_selectedCategory!.displayName}', + ), + backgroundColor: AppTheme.error, + ), + ); + return; } + + setState(() { + _selectedFile = file; + }); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( @@ -445,13 +447,76 @@ class _FileUploadWidgetState extends State { ? null : _descriptionController.text.trim(); - // Use the existing enhanced file service for other categories - response = await EnhancedFileService.uploadFile( - file: _selectedFile!, - category: _selectedCategory!.value, - description: description, - patientId: widget.patientId, - ); + // Always pass category.value (string) to upload methods + switch (_selectedCategory!) { + case FileCategory.profilePicture: + response = await ComprehensiveFileService.uploadProfileImage( + userId: user.id, + imageFile: _selectedFile!, + ); + break; + + case FileCategory.medicalReport: + case FileCategory.labResult: + response = await ComprehensiveFileService.uploadMedicalDocument( + patientId: widget.patientId ?? user.id, + documentFile: _selectedFile!, + category: _selectedCategory!, // Pass enum, not value + description: description, + ); + break; + + case FileCategory.prescription: + response = await ComprehensiveFileService.uploadPrescription( + patientId: widget.patientId ?? user.id, + prescriptionFile: _selectedFile!, + description: description, + ); + break; + + case FileCategory.clinicalNotes: + response = + await ComprehensiveFileService.uploadClinicalNotesAttachment( + patientId: widget.patientId ?? user.id, + attachmentFile: _selectedFile!, + description: description, + ); + break; + + case FileCategory.aiChatUpload: + response = await ComprehensiveFileService.uploadChatFile( + chatFile: _selectedFile!, + description: description, + ); + break; + + case FileCategory.insuranceDoc: + response = await ComprehensiveFileService.uploadInsuranceDocument( + patientId: widget.patientId ?? user.id, + insuranceFile: _selectedFile!, + description: description, + ); + break; + + case FileCategory.emergencyContact: + response = + await ComprehensiveFileService.uploadEmergencyContactDocument( + userId: user.id, + documentFile: _selectedFile!, + description: description, + ); + break; + + default: + // Use the existing enhanced file service for other categories + response = await EnhancedFileService.uploadFile( + file: _selectedFile!, + category: _selectedCategory!.value, // pass value, not enum + description: description, + patientId: widget.patientId, + ); + break; + } if (response != null) { ScaffoldMessenger.of(context).showSnackBar( @@ -491,86 +556,8 @@ class _FileUploadWidgetState extends State { }); } } - - Future _uploadFileWeb() async { - - if (_selectedCategory == null || - _selectedFileBytes == null || - _selectedFileName == null) { - return; - } - - setState(() { - _isUploading = true; - }); - - try { - final userProvider = Provider.of(context, listen: false); - final user = userProvider.user; - if (user == null) { - throw Exception('User not logged in'); - } - - FileUploadResponse? response; - final description = _descriptionController.text.trim().isEmpty - ? null - : _descriptionController.text.trim(); - - // Use the existing enhanced file service for other categories - response = await EnhancedFileService.uploadFileWeb( - fileBytes: _selectedFileBytes!, - fileName: _selectedFileName!, - category: _selectedCategory!.value, - description: description, - patientId: widget.patientId, - ); - - if (response != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'File uploaded successfully: ${response.fileName}', - ), - backgroundColor: AppTheme.success, - ), - ); - - // Reset form - setState(() { - _selectedFile = null; - _selectedFileName = null; - _selectedFileBytes = null; - _descriptionController.clear(); - }); - - // Callback - if (widget.onUploadSuccess != null) { - widget.onUploadSuccess!(response); - } - } else { - throw Exception('Upload failed - no response received'); - } - } catch (e, stacktrace) { - print('Upload Exception: $e'); - print('Stacktrace: $stacktrace'); - final errorMessage = 'Upload failed: $e'; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(errorMessage), backgroundColor: AppTheme.error), - ); - - if (widget.onUploadError != null) { - widget.onUploadError!(errorMessage); - } - } finally { - setState(() { - _isUploading = false; - }); - } - } } - - /// Quick upload buttons for common file types class QuickUploadButtons extends StatelessWidget { final int? patientId; diff --git a/careconnect2025/frontend/lib/widgets/messaging_widget.dart b/careconnect2025/frontend/lib/widgets/messaging_widget.dart index faf6ec40..4737e5f3 100644 --- a/careconnect2025/frontend/lib/widgets/messaging_widget.dart +++ b/careconnect2025/frontend/lib/widgets/messaging_widget.dart @@ -78,7 +78,7 @@ class _MessagingWidgetState extends State { _messageController.clear(); try { - final success = await MessagingService.sendMessage( + final bool success = await MessagingService.sendMessage( recipientId: widget.recipientId, senderId: widget.currentUserId, senderName: widget.currentUserName, diff --git a/careconnect2025/frontend/lib/widgets/video_call_widget.dart b/careconnect2025/frontend/lib/widgets/video_call_widget.dart index ca40b48e..41c74494 100644 --- a/careconnect2025/frontend/lib/widgets/video_call_widget.dart +++ b/careconnect2025/frontend/lib/widgets/video_call_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; import '../services/video_call_service.dart'; import '../config/theme/app_theme.dart'; @@ -33,29 +34,63 @@ class _VideoCallWidgetState extends State { bool _isCallEnded = false; DateTime? _callStartTime; + final RTCVideoRenderer _localRenderer = RTCVideoRenderer(); + final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer(); + MediaStream? _localStream; + RTCPeerConnection? _peerConnection; + @override void initState() { super.initState(); + _initRenderers(); if (!widget.isIncoming) { _joinCall(); } } + Future _initRenderers() async { + await _localRenderer.initialize(); + await _remoteRenderer.initialize(); + await _startLocalStream(); + } + + Future _startLocalStream() async { + final Map mediaConstraints = { + 'audio': true, + 'video': widget.isVideoCall + ? {'facingMode': 'user', 'width': 640, 'height': 480} + : false, + }; + _localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints); + _localRenderer.srcObject = _localStream; + setState(() {}); + } + Future _joinCall() async { try { - final success = await VideoCallService.joinCall( - widget.callId, - widget.currentUserId, - ); - - if (success) { - setState(() { - _isCallConnected = true; - _callStartTime = DateTime.now(); + // --- Minimal WebRTC peer connection setup --- + final Map config = { + 'iceServers': [ + {'urls': 'stun:stun.l.google.com:19302'}, + ], + }; + _peerConnection = await createPeerConnection(config); + if (_localStream != null) { + _localStream!.getTracks().forEach((track) { + _peerConnection!.addTrack(track, _localStream!); }); - } else { - _endCall('Failed to join call'); } + _peerConnection!.onTrack = (event) { + if (event.streams.isNotEmpty) { + _remoteRenderer.srcObject = event.streams[0]; + } + }; + // --- Signaling (SDP/ICE) must be handled by your VideoCallService --- + await VideoCallService.joinCall(widget.callId, widget.currentUserId); + setState(() { + _isCallConnected = true; + _callStartTime = DateTime.now(); + }); } catch (e) { print('Error joining call: $e'); _endCall('Connection failed'); @@ -64,29 +99,49 @@ class _VideoCallWidgetState extends State { Future _endCall([String? reason]) async { if (_isCallEnded) return; - setState(() => _isCallEnded = true); - try { + // Release camera/mic resources + await _releaseVideoResources(); await VideoCallService.endCallStatic(widget.callId, widget.currentUserId); } catch (e) { print('Error ending call: $e'); } - if (mounted) { Navigator.of(context).pop(); } } - void _toggleMic() { - setState(() => _isMicOn = !_isMicOn); - // In real implementation: await ZegoExpressEngine.instance.muteMicrophone(!_isMicOn); + Future _releaseVideoResources() async { + try { + await _localRenderer.dispose(); + await _remoteRenderer.dispose(); + await _localStream?.dispose(); + await _peerConnection?.close(); + _peerConnection = null; + } catch (e) { + print('Error releasing video resources: $e'); + } + } + + Future _toggleMic() async { + if (_localStream == null) return; + final audioTrack = _localStream!.getAudioTracks().firstOrNull; + if (audioTrack != null) { + final enabled = audioTrack.enabled; + audioTrack.enabled = !enabled; + setState(() => _isMicOn = audioTrack.enabled); + } } - void _toggleCamera() { - if (!widget.isVideoCall) return; - setState(() => _isCameraOn = !_isCameraOn); - // In real implementation: await ZegoExpressEngine.instance.muteVideoOutput(!_isCameraOn); + Future _toggleCamera() async { + if (!widget.isVideoCall || _localStream == null) return; + final videoTrack = _localStream!.getVideoTracks().firstOrNull; + if (videoTrack != null) { + final enabled = videoTrack.enabled; + videoTrack.enabled = !enabled; + setState(() => _isCameraOn = videoTrack.enabled); + } } String _getCallDuration() { @@ -111,112 +166,58 @@ class _VideoCallWidgetState extends State { child: Stack( children: [ // Remote user video (main view) - SizedBox( - width: double.infinity, - height: double.infinity, - child: widget.isVideoCall && _isCallConnected - ? Container( - // In real implementation, this would be the ZEGOCLOUD video view - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Theme.of(context).primaryColor.withOpacity(0.3), - Theme.of( - context, - ).colorScheme.secondary.withOpacity(0.3), - ], + if (widget.isVideoCall && _isCallConnected) + Positioned.fill( + child: RTCVideoView( + _remoteRenderer, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + ), + ) + else + Positioned.fill( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircleAvatar( + radius: 60, + backgroundColor: Theme.of(context).colorScheme.primary, + child: Text( + widget.otherUserName.isNotEmpty + ? widget.otherUserName[0].toUpperCase() + : '?', + style: const TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: AppTheme.videoCallText, + ), ), ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircleAvatar( - radius: 50, - backgroundColor: Theme.of( - context, - ).colorScheme.primary, - child: Text( - widget.otherUserName.isNotEmpty - ? widget.otherUserName[0].toUpperCase() - : '?', - style: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: AppTheme.videoCallText, - ), - ), - ), - const SizedBox(height: 16), - Text( - widget.otherUserName, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: AppTheme.videoCallText, - ), - ), - if (_isCallConnected) ...[ - const SizedBox(height: 8), - Text( - _getCallDuration(), - style: const TextStyle( - fontSize: 16, - color: AppTheme.videoCallTextSecondary, - ), - ), - ], - ], + const SizedBox(height: 24), + Text( + widget.otherUserName, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: AppTheme.videoCallText, ), ), - ) - : Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircleAvatar( - radius: 60, - backgroundColor: Theme.of( - context, - ).colorScheme.primary, - child: Text( - widget.otherUserName.isNotEmpty - ? widget.otherUserName[0].toUpperCase() - : '?', - style: const TextStyle( - fontSize: 36, - fontWeight: FontWeight.bold, - color: AppTheme.videoCallText, - ), - ), - ), - const SizedBox(height: 24), - Text( - widget.otherUserName, - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: AppTheme.videoCallText, - ), - ), - const SizedBox(height: 8), - Text( - _isCallConnected - ? _getCallDuration() - : widget.isIncoming - ? 'Incoming ${widget.isVideoCall ? 'video' : 'audio'} call' - : 'Calling...', - style: const TextStyle( - fontSize: 18, - color: AppTheme.videoCallTextSecondary, - ), - ), - ], + const SizedBox(height: 8), + Text( + _isCallConnected + ? _getCallDuration() + : widget.isIncoming + ? 'Incoming ${widget.isVideoCall ? 'video' : 'audio'} call' + : 'Calling...', + style: const TextStyle( + fontSize: 18, + color: AppTheme.videoCallTextSecondary, + ), ), - ), - ), + ], + ), + ), + ), // Local user video (small preview in corner) if (widget.isVideoCall && _isCameraOn && _isCallConnected) @@ -233,16 +234,10 @@ class _VideoCallWidgetState extends State { ), child: ClipRRect( borderRadius: BorderRadius.circular(10), - child: Container( - // In real implementation, this would be the local video preview - color: Theme.of( - context, - ).colorScheme.surface.withOpacity(0.8), - child: const Icon( - Icons.person, - color: AppTheme.videoCallTextTertiary, - size: 40, - ), + child: RTCVideoView( + _localRenderer, + mirror: true, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, ), ), ), @@ -383,13 +378,13 @@ class _VideoCallWidgetState extends State { } @override - Widget build(BuildContext context) { - // Use fallback UI for all platforms until Zego web compatibility is resolved - return _buildFallbackUI(); + void dispose() { + _releaseVideoResources(); + super.dispose(); } - // Fallback UI for development/testing when ZEGOCLOUD is not available - Widget _buildFallbackUI() { + @override + Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).brightness == Brightness.dark ? AppTheme.videoCallBackgroundDarkTheme From e8bb6e679dc2da716cbc51d59c1de6de0701a104 Mon Sep 17 00:00:00 2001 From: Ashenafi-Tesfaye <65870089+Ashenafi-Tesfaye@users.noreply.github.com> Date: Fri, 1 Aug 2025 06:29:00 -0400 Subject: [PATCH 04/18] populatin caregiver and family member ids --- .../careconnect/service/PatientService.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/PatientService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/PatientService.java index b0bcba8d..881057ef 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/service/PatientService.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/PatientService.java @@ -30,6 +30,10 @@ import java.util.stream.Collectors; import com.careconnect.dto.CaregiverPatientLinkResponse; +import com.careconnect.dto.FamilyMemberLinkResponse; + +import com.careconnect.service.FamilyMemberService; + @Service public class PatientService { @@ -57,6 +61,9 @@ public class PatientService { @Autowired private MoodPainLogService moodPainLogService; + @Autowired + private FamilyMemberService familyMemberService; + // 1. List caregivers associated with a patient (ACTIVE links only) public List getCaregiversByPatient(Long patientId) { Patient patient = patientRepository.findById(patientId) @@ -207,7 +214,14 @@ public Optional getEnhancedPatientProfile(Long patien LatestVitalsDTO latestVitals = getLatestVitals(patientId); LatestMoodPainDTO latestMoodPain = getLatestMoodPain(patientId); MedicalSummaryDTO medicalSummary = buildMedicalSummary(patientId, allergies, activeMedications, latestVitals, latestMoodPain); - + + // Get caregiver and family member link IDs (not user IDs) + List caregiverLinks = caregiverPatientLinkService.getCaregiversByPatient(patient.getUser().getId()); + Long caregiverId = caregiverLinks.isEmpty() ? null : caregiverLinks.get(0).id(); + + List familyLinks = familyMemberService.getFamilyMembersByPatientId(patient.getId()); + Long familyMemberId = familyLinks.isEmpty() ? null : familyLinks.get(0).id(); + return Optional.of(EnhancedPatientProfileDTO.builder() .id(patient.getId()) .firstName(patient.getFirstName()) @@ -223,6 +237,8 @@ public Optional getEnhancedPatientProfile(Long patien .latestVitals(latestVitals) .latestMoodPain(latestMoodPain) .medicalSummary(medicalSummary) + .caregiverId(caregiverId) + .familyMemberId(familyMemberId) .build()); } From 140bf44f40aa73557f1c23505d3687fa2d45d49b Mon Sep 17 00:00:00 2001 From: Ashenafi-Tesfaye <65870089+Ashenafi-Tesfaye@users.noreply.github.com> Date: Fri, 1 Aug 2025 08:08:45 -0400 Subject: [PATCH 05/18] web build field and patient status bug --- careconnect2025/frontend/lib/main.dart | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/careconnect2025/frontend/lib/main.dart b/careconnect2025/frontend/lib/main.dart index 912d4315..4d6eab44 100644 --- a/careconnect2025/frontend/lib/main.dart +++ b/careconnect2025/frontend/lib/main.dart @@ -248,10 +248,8 @@ class _CareConnectAppState extends State { debugShowCheckedModeBanner: false, themeMode: themeProvider.themeMode, theme: AppTheme.lightTheme.copyWith( - // Additional theme customizations visualDensity: VisualDensity.adaptivePlatformDensity, textTheme: AppTheme.lightTheme.textTheme.apply(fontFamily: 'Roboto'), - // Platform-specific theme adjustments pageTransitionsTheme: const PageTransitionsTheme( builders: { TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), @@ -262,7 +260,6 @@ class _CareConnectAppState extends State { TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(), }, ), - // Adjust card elevation for iOS vs Android cardTheme: CardThemeData( elevation: kIsWeb ? 2 : (ResponsiveUtils.isIOS ? 1 : 2), shape: RoundedRectangleBorder( @@ -271,10 +268,8 @@ class _CareConnectAppState extends State { ), ), darkTheme: AppTheme.darkTheme.copyWith( - // Additional dark theme customizations visualDensity: VisualDensity.adaptivePlatformDensity, textTheme: AppTheme.darkTheme.textTheme.apply(fontFamily: 'Roboto'), - // Platform-specific theme adjustments pageTransitionsTheme: const PageTransitionsTheme( builders: { TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), @@ -285,7 +280,6 @@ class _CareConnectAppState extends State { TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(), }, ), - // Adjust card elevation for iOS vs Android cardTheme: CardThemeData( elevation: kIsWeb ? 3 : (ResponsiveUtils.isIOS ? 2 : 3), shape: RoundedRectangleBorder( @@ -294,26 +288,17 @@ class _CareConnectAppState extends State { ), ), routerConfig: appRouter, - // Performance optimization and responsive behavior builder: (context, child) { - // Handle text scaling for accessibility final mediaQuery = MediaQuery.of(context); final textScaleFactor = mediaQuery.textScaleFactor.clamp(0.8, 1.2); - - // Apply platform-specific adjustments Widget updatedChild = child!; - - // Apply safe area with platform awareness updatedChild = SafeArea( - bottom: !ResponsiveUtils.isWeb, // Web doesn't need bottom padding + bottom: !ResponsiveUtils.isWeb, child: updatedChild, ); - - // Apply the adjusted MediaQuery return MediaQuery( data: mediaQuery.copyWith( textScaler: TextScaler.linear(textScaleFactor), - // Ensure proper viewport settings across devices devicePixelRatio: ResponsiveUtils.isWeb ? mediaQuery.devicePixelRatio : mediaQuery.devicePixelRatio.clamp(1.0, 3.0), From cd30a768d00d85fd99bb535ae14173f08de8e737 Mon Sep 17 00:00:00 2001 From: Ashenafi-Tesfaye <65870089+Ashenafi-Tesfaye@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:46:29 -0400 Subject: [PATCH 06/18] added notifications fixed additional issues in the caregiver dashboard and analytics page and other related changes --- .../NotificationSettingController.java | 30 +++ .../dto/NotificationSettingDTO.java | 18 ++ .../model/NotificationSetting.java | 50 ++++ .../main/java/com/careconnect/model/User.java | 2 +- .../NotificationSettingRepository.java | 9 + .../service/NotificationSettingService.java | 55 ++++ .../features/analytics/analytics_page.dart | 141 ++++------ .../dashboard/models/patient_model.dart | 136 ++++++++-- .../lib/models/notification_settings.dart | 82 ++++++ .../frontend/lib/pages/settings_page.dart | 246 ++++++++++++++++-- .../notification_settings_service.dart | 73 ++++++ .../frontend/lib/widgets/common_drawer.dart | 58 ++++- 12 files changed, 759 insertions(+), 141 deletions(-) create mode 100644 careconnect2025/backend/core/src/main/java/com/careconnect/controller/NotificationSettingController.java create mode 100644 careconnect2025/backend/core/src/main/java/com/careconnect/dto/NotificationSettingDTO.java create mode 100644 careconnect2025/backend/core/src/main/java/com/careconnect/model/NotificationSetting.java create mode 100644 careconnect2025/backend/core/src/main/java/com/careconnect/repository/NotificationSettingRepository.java create mode 100644 careconnect2025/backend/core/src/main/java/com/careconnect/service/NotificationSettingService.java create mode 100644 careconnect2025/frontend/lib/models/notification_settings.dart create mode 100644 careconnect2025/frontend/lib/services/notification_settings_service.dart diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/controller/NotificationSettingController.java b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/NotificationSettingController.java new file mode 100644 index 00000000..0e1191d3 --- /dev/null +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/controller/NotificationSettingController.java @@ -0,0 +1,30 @@ +package com.careconnect.controller; + +import com.careconnect.dto.NotificationSettingDTO; +import com.careconnect.service.NotificationSettingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/v1/api/notification-settings") +@Tag(name = "Notification Settings", description = "Manage notification preferences for users") +public class NotificationSettingController { + + @Autowired + private NotificationSettingService notificationSettingService; + + @GetMapping("/{userId}") + @Operation(summary = "Get notification settings for a user") + public ResponseEntity getSettings(@PathVariable Long userId) { + return ResponseEntity.ok(notificationSettingService.getByUserId(userId)); + } + + @PostMapping + @Operation(summary = "Create or update notification settings for a user") + public ResponseEntity createOrUpdate(@RequestBody NotificationSettingDTO dto) { + return ResponseEntity.ok(notificationSettingService.createOrUpdate(dto)); + } +} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/dto/NotificationSettingDTO.java b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/NotificationSettingDTO.java new file mode 100644 index 00000000..bd52f1fb --- /dev/null +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/dto/NotificationSettingDTO.java @@ -0,0 +1,18 @@ +package com.careconnect.dto; + +import lombok.Builder; +import java.time.Instant; + +@Builder +public record NotificationSettingDTO( + Long id, + Long userId, + boolean gamification, + boolean emergency, + boolean videoCall, + boolean audioCall, + boolean sms, + boolean significantVitals, + Instant createdAt, + Instant updatedAt +) {} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/model/NotificationSetting.java b/careconnect2025/backend/core/src/main/java/com/careconnect/model/NotificationSetting.java new file mode 100644 index 00000000..7aedfa3c --- /dev/null +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/model/NotificationSetting.java @@ -0,0 +1,50 @@ +package com.careconnect.model; + +import jakarta.persistence.*; +import lombok.*; +import java.time.Instant; + +@Entity +@Table(name = "notification_setting") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class NotificationSetting { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private Long userId; + + @Builder.Default + private boolean gamification = true; + @Builder.Default + private boolean emergency = true; + @Builder.Default + private boolean videoCall = true; + @Builder.Default + private boolean audioCall = true; + @Builder.Default + private boolean sms = true; + @Builder.Default + private boolean significantVitals = true; + + @Column(nullable = false) + private Instant createdAt; + @Column(nullable = false) + private Instant updatedAt; + + @PrePersist + protected void onCreate() { + Instant now = Instant.now(); + createdAt = now; + updatedAt = now; + } + + @PreUpdate + protected void onUpdate() { + updatedAt = Instant.now(); + } +} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/model/User.java b/careconnect2025/backend/core/src/main/java/com/careconnect/model/User.java index 3e384fdc..929622ae 100644 --- a/careconnect2025/backend/core/src/main/java/com/careconnect/model/User.java +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/model/User.java @@ -39,7 +39,7 @@ public class User { @Column(name = "login_streak") private Integer loginStreak; - @Column(name = "leaderboard_opt_in", nullable = false) + @Column(name = "leaderboard_opt_in", nullable = true) private Boolean leaderboardOptIn = true; @Enumerated(EnumType.STRING) diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/repository/NotificationSettingRepository.java b/careconnect2025/backend/core/src/main/java/com/careconnect/repository/NotificationSettingRepository.java new file mode 100644 index 00000000..930847a1 --- /dev/null +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/repository/NotificationSettingRepository.java @@ -0,0 +1,9 @@ +package com.careconnect.repository; + +import com.careconnect.model.NotificationSetting; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface NotificationSettingRepository extends JpaRepository { + Optional findByUserId(Long userId); +} diff --git a/careconnect2025/backend/core/src/main/java/com/careconnect/service/NotificationSettingService.java b/careconnect2025/backend/core/src/main/java/com/careconnect/service/NotificationSettingService.java new file mode 100644 index 00000000..5c43e440 --- /dev/null +++ b/careconnect2025/backend/core/src/main/java/com/careconnect/service/NotificationSettingService.java @@ -0,0 +1,55 @@ +package com.careconnect.service; + +import com.careconnect.dto.NotificationSettingDTO; +import com.careconnect.model.NotificationSetting; +import com.careconnect.repository.NotificationSettingRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class NotificationSettingService { + private final NotificationSettingRepository notificationSettingRepository; + + public NotificationSettingDTO getByUserId(Long userId) { + NotificationSetting setting = notificationSettingRepository.findByUserId(userId) + .orElseGet(() -> notificationSettingRepository.save( + NotificationSetting.builder() + .userId(userId) + .build() + )); + return toDTO(setting); + } + + @Transactional + public NotificationSettingDTO createOrUpdate(NotificationSettingDTO dto) { + NotificationSetting setting = notificationSettingRepository.findByUserId(dto.userId()) + .orElse(NotificationSetting.builder().userId(dto.userId()).build()); + setting.setGamification(dto.gamification()); + setting.setEmergency(dto.emergency()); + setting.setVideoCall(dto.videoCall()); + setting.setAudioCall(dto.audioCall()); + setting.setSms(dto.sms()); + setting.setSignificantVitals(dto.significantVitals()); + NotificationSetting saved = notificationSettingRepository.save(setting); + return toDTO(saved); + } + + private NotificationSettingDTO toDTO(NotificationSetting setting) { + return NotificationSettingDTO.builder() + .id(setting.getId()) + .userId(setting.getUserId()) + .gamification(setting.isGamification()) + .emergency(setting.isEmergency()) + .videoCall(setting.isVideoCall()) + .audioCall(setting.isAudioCall()) + .sms(setting.isSms()) + .significantVitals(setting.isSignificantVitals()) + .createdAt(setting.getCreatedAt()) + .updatedAt(setting.getUpdatedAt()) + .build(); + } +} diff --git a/careconnect2025/frontend/lib/features/analytics/analytics_page.dart b/careconnect2025/frontend/lib/features/analytics/analytics_page.dart index 192b4466..c3cd17f0 100644 --- a/careconnect2025/frontend/lib/features/analytics/analytics_page.dart +++ b/careconnect2025/frontend/lib/features/analytics/analytics_page.dart @@ -745,16 +745,22 @@ class _AnalyticsPageState extends State { width: double.infinity, padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.blue.shade50, + color: Theme.of( + context, + ).colorScheme.primary.withOpacity(0.07), borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.blue.shade200), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.primary.withOpacity(0.18), + ), ), child: Row( children: [ Icon( Icons.lightbulb_outline, size: 16, - color: Colors.blue.shade600, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), Expanded( @@ -762,7 +768,7 @@ class _AnalyticsPageState extends State { 'Click "Ask AI" to start a conversation about the patient\'s health data', style: TextStyle( fontSize: 11, - color: Colors.blue.shade700, + color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w500, ), ), @@ -830,17 +836,23 @@ class _AnalyticsPageState extends State { _onFilterChanged(days); } }, - selectedColor: Colors.blue.shade600.withValues(alpha: 0.2), - checkmarkColor: Colors.blue.shade600, + selectedColor: Theme.of( + context, + ).colorScheme.primary.withOpacity(0.15), + checkmarkColor: Theme.of(context).colorScheme.primary, labelStyle: TextStyle( - color: isSelected ? Colors.blue.shade600 : Colors.grey.shade600, + color: isSelected + ? Theme.of(context).colorScheme.primary + : Colors.grey.shade600, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), backgroundColor: Colors.grey.shade100, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide( - color: isSelected ? Colors.blue.shade600 : Colors.grey.shade300, + color: isSelected + ? Theme.of(context).colorScheme.primary + : Colors.grey.shade300, width: 1, ), ), @@ -859,6 +871,10 @@ class _AnalyticsPageState extends State { }) { final displaySpots = spots.isEmpty ? [const FlSpot(0, 0)] : spots; final bool hasData = spots.isNotEmpty; + final themePrimary = Theme.of(context).colorScheme.primary; + final themePrimaryLighter = Theme.of( + context, + ).colorScheme.primary.withOpacity(0.08); return Card( elevation: 4, @@ -868,10 +884,7 @@ class _AnalyticsPageState extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: LinearGradient( - colors: [ - ColorUtils.backgroundPrimary, - ColorUtils.getPrimaryLighter(), - ], + colors: [Colors.white, themePrimaryLighter], begin: Alignment.topLeft, end: Alignment.bottomRight, ), @@ -887,7 +900,7 @@ class _AnalyticsPageState extends State { width: 4, height: 24, decoration: BoxDecoration( - color: primaryColor ?? ColorUtils.primary, + color: primaryColor ?? themePrimary, borderRadius: BorderRadius.circular(2), ), ), @@ -909,7 +922,7 @@ class _AnalyticsPageState extends State { vertical: 4, ), decoration: BoxDecoration( - color: ColorUtils.getPrimaryWithOpacity(0.1), + color: themePrimary.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( @@ -917,7 +930,7 @@ class _AnalyticsPageState extends State { style: TextStyle( fontSize: 14, // Increased from 12 for better readability - color: primaryColor ?? ColorUtils.primary, + color: primaryColor ?? themePrimary, fontWeight: FontWeight.w500, ), ), @@ -960,22 +973,23 @@ class _AnalyticsPageState extends State { LineChartBarData( spots: spots, isCurved: true, - color: primaryColor ?? Colors.blue.shade600, + color: primaryColor ?? themePrimary, barWidth: 3, dotData: FlDotData( show: spots.length <= 10, getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter( radius: 4, - color: primaryColor ?? Colors.blue.shade600, + color: primaryColor ?? themePrimary, strokeWidth: 2, strokeColor: Colors.white, ), ), belowBarData: BarAreaData( show: true, - color: (primaryColor ?? Colors.blue.shade600) - .withValues(alpha: 0.1), + color: (primaryColor ?? themePrimary).withOpacity( + 0.1, + ), ), ), ], @@ -1230,8 +1244,8 @@ class _AnalyticsPageState extends State { icon: const Icon(Icons.refresh), label: const Text('Retry'), style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade700, - foregroundColor: Colors.white, + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 12, @@ -1242,7 +1256,8 @@ class _AnalyticsPageState extends State { ), ), floatingActionButton: FloatingActionButton( - backgroundColor: Colors.blue.shade700, + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, child: const Icon(Icons.chat_bubble_outline), onPressed: () { final double sheetHeight = @@ -1304,72 +1319,20 @@ class _AnalyticsPageState extends State { ); return Scaffold( - backgroundColor: Colors.white, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, drawer: const CommonDrawer(currentRoute: '/analytics'), appBar: AppBar( - backgroundColor: Colors.blue.shade900, - iconTheme: const IconThemeData(color: Colors.white), - title: const Text( - 'Patient Analytics', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + backgroundColor: Theme.of(context).colorScheme.primary, + iconTheme: IconThemeData( + color: Theme.of(context).colorScheme.onPrimary, ), - actions: [ - // Refresh button - IconButton( - onPressed: loading ? null : fetchAnalytics, - icon: Icon( - Icons.refresh, - color: loading ? Colors.white54 : Colors.white, - ), - tooltip: 'Refresh Data', - ), - Container( - margin: const EdgeInsets.only(right: 8), - child: ElevatedButton.icon( - onPressed: () => exportFile('pdf'), - icon: const Icon(Icons.picture_as_pdf, size: 18), - label: const Text('PDF'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red.shade600, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - textStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ), - Container( - margin: const EdgeInsets.only(right: 12), - child: ElevatedButton.icon( - onPressed: () => exportFile('csv'), - icon: const Icon(Icons.table_chart, size: 18), - label: const Text('CSV'), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.success, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - textStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), + title: Text( + 'Patient Analytics', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.bold, ), - ], + ), ), body: Stack( children: [ @@ -1435,8 +1398,10 @@ class _AnalyticsPageState extends State { borderRadius: BorderRadius.circular(16), gradient: LinearGradient( colors: [ - Colors.blue.shade700, - Colors.blue.shade500, + Theme.of(context).colorScheme.primary, + Theme.of( + context, + ).colorScheme.primary.withOpacity(0.85), ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -1452,8 +1417,8 @@ class _AnalyticsPageState extends State { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white.withValues( - alpha: 0.2, + color: Colors.white.withOpacity( + 0.2, ), borderRadius: BorderRadius.circular( 12, diff --git a/careconnect2025/frontend/lib/features/dashboard/models/patient_model.dart b/careconnect2025/frontend/lib/features/dashboard/models/patient_model.dart index b6bf0a48..70e621b4 100644 --- a/careconnect2025/frontend/lib/features/dashboard/models/patient_model.dart +++ b/careconnect2025/frontend/lib/features/dashboard/models/patient_model.dart @@ -44,12 +44,11 @@ class Patient { final String relationship; final String? profileImageUrl; final Address? address; - final int? linkId; // Added to track the relationship link ID - final String linkStatus; // Added to track active/suspended status - final String? gender; // Added for gender information - final List? allergies; // Added for allergies list - final Map? - vitalConditions; // Added for vital conditions summary + final int? linkId; + final String linkStatus; + final String? gender; + final List? allergies; + final Map? vitalConditions; Patient({ required this.id, @@ -84,21 +83,10 @@ class Patient { } else if (patientData['id'] is String) { id = int.tryParse(patientData['id']) ?? 0; } else if (patientData['patientId'] is int) { - // Try to use patientId if id is not available id = patientData['patientId']; } else if (patientData['patientId'] is String) { id = int.tryParse(patientData['patientId']) ?? 0; - } else if (patientData.containsKey('user') && - patientData['user'] is Map && - patientData['user']['id'] is int) { - // Try to use user.id if available - id = patientData['user']['id']; - print('🔧 Using user.id as fallback for patient ID: $id'); } else { - // Default value with debug log - print( - '⚠️ Warning: Patient object missing id and patientId fields: ${patientData.keys.toList()}', - ); id = 0; } @@ -162,20 +150,120 @@ class Patient { ? patientData['user']['profileImageUrl']?.toString() : ''), address: patientData['address'] != null - ? Address.fromJson(patientData['address']) + ? Address.fromJson(Map.from(patientData['address'])) : null, linkId: linkId, linkStatus: linkStatus, gender: patientData['gender']?.toString(), - allergies: patientData['allergies'] != null - ? List.from(patientData['allergies']) - : null, - vitalConditions: patientData['vitalConditions'] != null - ? Map.from(patientData['vitalConditions']) - : null, + allergies: patientData['allergies'] ?? [], + vitalConditions: patientData['latestVitals'] ?? {}, ); } + // factory Patient.fromJson(Map json) { + // // Check if this is a nested structure from the API + // Map patientData = json; + // if (json.containsKey('patient') && + // json['patient'] is Map) { + // print('🔍 Detected nested patient structure'); + // patientData = json['patient'] as Map; + // } + + // // Ensure we can handle various ways the id might be provided + // int id; + // if (patientData['id'] is int) { + // id = patientData['id']; + // } else if (patientData['id'] is String) { + // id = int.tryParse(patientData['id']) ?? 0; + // } else if (patientData['patientId'] is int) { + // // Try to use patientId if id is not available + // id = patientData['patientId']; + // } else if (patientData['patientId'] is String) { + // id = int.tryParse(patientData['patientId']) ?? 0; + // } else if (patientData.containsKey('user') && + // patientData['user'] is Map && + // patientData['user']['id'] is int) { + // // Try to use user.id if available + // id = patientData['user']['id']; + // print('🔧 Using user.id as fallback for patient ID: $id'); + // } else { + // // Default value with debug log + // print( + // '⚠️ Warning: Patient object missing id and patientId fields: ${patientData.keys.toList()}', + // ); + // id = 0; + // } + + // // Extract link ID and status if available + // int? linkId; + // String linkStatus = 'ACTIVE'; + + // // First check if linkId and status are directly provided + // if (json.containsKey('linkId')) { + // if (json['linkId'] is int) { + // linkId = json['linkId']; + // } else if (json['linkId'] is String) { + // linkId = int.tryParse(json['linkId'].toString()); + // } + // } + + // if (json.containsKey('linkStatus')) { + // linkStatus = json['linkStatus']?.toString() ?? 'ACTIVE'; + // } + + // // If linkId is still null, try to extract from link object + // if (json.containsKey('link') && json['link'] is Map) { + // final linkData = json['link'] as Map; + // print('🔍 Found link data: ${linkData.keys.toList()}'); + + // // Extract link ID + // if (linkData.containsKey('id')) { + // if (linkData['id'] is int) { + // linkId = linkData['id']; + // print('🔍 Using link.id for linkId: $linkId'); + // } else if (linkData['id'] is String) { + // linkId = int.tryParse(linkData['id'].toString()); + // print('🔍 Parsed link.id string to linkId: $linkId'); + // } + // } + + // // Extract status if available + // if (linkData.containsKey('status')) { + // linkStatus = linkData['status']?.toString() ?? 'ACTIVE'; + // print('🔍 Using link.status for linkStatus: $linkStatus'); + // } + // } + + // return Patient( + // id: id, + // firstName: patientData['firstName']?.toString() ?? '', + // lastName: patientData['lastName']?.toString() ?? '', + // email: patientData['email']?.toString() ?? '', + // phone: patientData['phone']?.toString() ?? '', + // dob: patientData['dob']?.toString() ?? '', + // relationship: + // patientData['relationship']?.toString() ?? + // (json.containsKey('link') && json['link'] is Map + // ? json['link']['linkType']?.toString() + // : '') ?? + // '', + // profileImageUrl: + // patientData['profileImageUrl']?.toString() ?? + // (patientData.containsKey('user') && + // patientData['user'] is Map + // ? patientData['user']['profileImageUrl']?.toString() + // : ''), + // address: patientData['address'] != null + // ? Address.fromJson(Map.from(patientData['address'])) + // : null, + // linkId: linkId, + // linkStatus: linkStatus, + // gender: patientData['gender']?.toString(), + // allergies: patientData['allergies'] ?? [], + // vitalConditions: patientData['latestVitals'] ?? {}, + // ); + // } + @override String toString() { return 'Patient{id: $id, firstName: $firstName, lastName: $lastName, email: $email, phone: $phone, dob: $dob, relationship: $relationship, linkId: $linkId, linkStatus: $linkStatus, gender: $gender, allergies: $allergies, vitalConditions: $vitalConditions}'; diff --git a/careconnect2025/frontend/lib/models/notification_settings.dart b/careconnect2025/frontend/lib/models/notification_settings.dart new file mode 100644 index 00000000..67e56221 --- /dev/null +++ b/careconnect2025/frontend/lib/models/notification_settings.dart @@ -0,0 +1,82 @@ +class NotificationSettings { + final int? id; + final int userId; + final bool gamification; + final bool emergency; + final bool videoCall; + final bool audioCall; + final bool sms; + final bool significantVitals; + final DateTime? createdAt; + final DateTime? updatedAt; + + NotificationSettings({ + this.id, + required this.userId, + required this.gamification, + required this.emergency, + required this.videoCall, + required this.audioCall, + required this.sms, + required this.significantVitals, + this.createdAt, + this.updatedAt, + }); + + factory NotificationSettings.fromJson(Map json) { + return NotificationSettings( + id: json['id'], + userId: json['userId'], + gamification: json['gamification'] ?? false, + emergency: json['emergency'] ?? true, + videoCall: json['videoCall'] ?? true, + audioCall: json['audioCall'] ?? true, + sms: json['sms'] ?? true, + significantVitals: json['significantVitals'] ?? true, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt']) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt']) + : null, + ); + } + + Map toJson() { + return { + 'userId': userId, + 'gamification': gamification, + 'emergency': emergency, + 'videoCall': videoCall, + 'audioCall': audioCall, + 'sms': sms, + 'significantVitals': significantVitals, + }; + } + + NotificationSettings copyWith({ + int? id, + int? userId, + bool? gamification, + bool? emergency, + bool? videoCall, + bool? audioCall, + bool? sms, + bool? significantVitals, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return NotificationSettings( + id: id ?? this.id, + userId: userId ?? this.userId, + gamification: gamification ?? this.gamification, + emergency: emergency ?? this.emergency, + videoCall: videoCall ?? this.videoCall, + audioCall: audioCall ?? this.audioCall, + sms: sms ?? this.sms, + significantVitals: significantVitals ?? this.significantVitals, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } +} diff --git a/careconnect2025/frontend/lib/pages/settings_page.dart b/careconnect2025/frontend/lib/pages/settings_page.dart index 2c4abe29..bad868d8 100644 --- a/careconnect2025/frontend/lib/pages/settings_page.dart +++ b/careconnect2025/frontend/lib/pages/settings_page.dart @@ -4,6 +4,8 @@ import 'package:provider/provider.dart'; import '../providers/user_provider.dart'; import '../widgets/common_drawer.dart'; import '../widgets/theme_toggle_switch.dart'; +import '../models/notification_settings.dart'; +import '../services/notification_settings_service.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -13,6 +15,85 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { + NotificationSettings? _notificationSettings; + bool _loadingSettings = true; + + @override + void initState() { + super.initState(); + _loadNotificationSettings(); + } + + Future _loadNotificationSettings() async { + final userProvider = Provider.of(context, listen: false); + final user = userProvider.user; + + if (user != null) { + final settings = + await NotificationSettingsService.getNotificationSettings(user.id); + setState(() { + _notificationSettings = settings; + _loadingSettings = false; + }); + } else { + setState(() { + _loadingSettings = false; + }); + } + } + + Future _updateNotificationSetting(String setting, bool value) async { + if (_notificationSettings == null) return; + + NotificationSettings updatedSettings; + switch (setting) { + case 'gamification': + updatedSettings = _notificationSettings!.copyWith(gamification: value); + break; + case 'emergency': + updatedSettings = _notificationSettings!.copyWith(emergency: value); + break; + case 'videoCall': + updatedSettings = _notificationSettings!.copyWith(videoCall: value); + break; + case 'audioCall': + updatedSettings = _notificationSettings!.copyWith(audioCall: value); + break; + case 'sms': + updatedSettings = _notificationSettings!.copyWith(sms: value); + break; + case 'significantVitals': + updatedSettings = _notificationSettings!.copyWith( + significantVitals: value, + ); + break; + default: + return; + } + + final saved = await NotificationSettingsService.saveNotificationSettings( + updatedSettings, + ); + if (saved != null) { + setState(() { + _notificationSettings = saved; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('✅ Notification settings updated'), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('❌ Failed to update settings'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } + Widget _buildSectionHeader(BuildContext context, String title) { return Padding( padding: const EdgeInsets.only(bottom: 12, left: 4), @@ -40,7 +121,7 @@ class _SettingsPageState extends State { child: ListTile( leading: Icon( icon, - color: iconColor ?? Theme.of(context).iconTheme.color, + color: iconColor ?? Theme.of(context).colorScheme.primary, size: 24, ), title: Text( @@ -53,20 +134,53 @@ class _SettingsPageState extends State { subtitle: Text(subtitle, style: Theme.of(context).textTheme.bodySmall), trailing: Icon( Icons.chevron_right, - color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + color: Theme.of(context).colorScheme.primary.withOpacity(0.5), ), onTap: onTap, ), ); } + Widget _buildNotificationToggleCard( + BuildContext context, { + required IconData icon, + required String title, + required String subtitle, + required bool value, + required Function(bool) onChanged, + Color? iconColor, + }) { + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: Icon( + icon, + color: iconColor ?? Theme.of(context).colorScheme.primary, + size: 24, + ), + title: Text( + title, + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), + ), + subtitle: Text(subtitle, style: Theme.of(context).textTheme.bodySmall), + trailing: Switch( + value: value, + onChanged: onChanged, + activeColor: Theme.of(context).colorScheme.primary, + ), + ), + ); + } + Widget _buildThemeCard(BuildContext context) { return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: Icon( Icons.brightness_6, - color: Theme.of(context).iconTheme.color, + color: Theme.of(context).colorScheme.primary, size: 24, ), title: Text( @@ -189,16 +303,25 @@ class _SettingsPageState extends State { final userProvider = Provider.of(context); final user = userProvider.user; + // Check if user is Patient or Family Member to hide subscription management + final shouldHideSubscription = + user != null && + (user.role.toLowerCase() == 'patient' || + user.role.toLowerCase() == 'family member'); + return Scaffold( - appBar: AppBar(title: const Text('Settings'), actions: const []), + appBar: AppBar( + title: const Text('Settings'), + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + ), drawer: const CommonDrawer(currentRoute: '/settings'), body: SafeArea( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), // Apply horizontal padding + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: ListView( children: [ + const SizedBox(height: 16), Center( child: Column( children: [ @@ -245,28 +368,108 @@ class _SettingsPageState extends State { _buildSectionHeader(context, 'Appearance'), _buildThemeCard(context), const SizedBox(height: 24), + _buildSectionHeader(context, 'Notifications'), + if (_loadingSettings) + const Card( + margin: EdgeInsets.only(bottom: 8), + child: ListTile( + leading: CircularProgressIndicator(), + title: Text('Loading notification settings...'), + ), + ) + else if (_notificationSettings != null) ...[ + _buildNotificationToggleCard( + context, + icon: Icons.emergency, + title: 'Emergency Alerts', + subtitle: 'Critical health alerts and emergencies', + value: _notificationSettings!.emergency, + onChanged: (value) => + _updateNotificationSetting('emergency', value), + iconColor: Theme.of(context).colorScheme.error, + ), + _buildNotificationToggleCard( + context, + icon: Icons.video_call, + title: 'Video Call Notifications', + subtitle: 'Incoming video call alerts', + value: _notificationSettings!.videoCall, + onChanged: (value) => + _updateNotificationSetting('videoCall', value), + ), + _buildNotificationToggleCard( + context, + icon: Icons.call, + title: 'Audio Call Notifications', + subtitle: 'Incoming audio call alerts', + value: _notificationSettings!.audioCall, + onChanged: (value) => + _updateNotificationSetting('audioCall', value), + ), + _buildNotificationToggleCard( + context, + icon: Icons.favorite, + title: 'Significant Vitals', + subtitle: 'Important changes in vital signs', + value: _notificationSettings!.significantVitals, + onChanged: (value) => + _updateNotificationSetting('significantVitals', value), + ), + _buildNotificationToggleCard( + context, + icon: Icons.sms, + title: 'SMS Notifications', + subtitle: 'Text message alerts to your phone', + value: _notificationSettings!.sms, + onChanged: (value) => + _updateNotificationSetting('sms', value), + ), + _buildNotificationToggleCard( + context, + icon: Icons.stars, + title: 'Gamification', + subtitle: 'Achievement and progress notifications', + value: _notificationSettings!.gamification, + onChanged: (value) => + _updateNotificationSetting('gamification', value), + ), + ] else + Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: Icon( + Icons.error_outline, + color: Theme.of(context).colorScheme.error, + ), + title: const Text('Unable to load notification settings'), + trailing: IconButton( + icon: const Icon(Icons.refresh), + onPressed: _loadNotificationSettings, + ), + ), + ), + const SizedBox(height: 24), _buildSectionHeader(context, 'AI Assistant'), _buildSettingsCard( context, icon: Icons.smart_toy, title: 'AI Configuration', subtitle: 'Customize your AI assistant settings', - onTap: () => context.push( - '/ai-configuration', - ), // Use push for back button support - ), - const SizedBox(height: 24), - _buildSectionHeader(context, 'Subscription'), - _buildSettingsCard( - context, - icon: Icons.subscriptions, - title: 'Manage Subscription', - subtitle: 'View or update your subscription plan', - onTap: () => context.push( - '/select-package', - ), // Use push for back button support + onTap: () => context.push('/ai-configuration'), ), const SizedBox(height: 24), + // Only show subscription management for non-patient/family member users + if (!shouldHideSubscription) ...[ + _buildSectionHeader(context, 'Subscription'), + _buildSettingsCard( + context, + icon: Icons.subscriptions, + title: 'Manage Subscription', + subtitle: 'View or update your subscription plan', + onTap: () => context.push('/select-package'), + ), + const SizedBox(height: 24), + ], _buildSectionHeader(context, 'General'), _buildSettingsCard( context, @@ -293,6 +496,7 @@ class _SettingsPageState extends State { textColor: Theme.of(context).colorScheme.error, iconColor: Theme.of(context).colorScheme.error, ), + const SizedBox(height: 24), ], ), ), diff --git a/careconnect2025/frontend/lib/services/notification_settings_service.dart b/careconnect2025/frontend/lib/services/notification_settings_service.dart new file mode 100644 index 00000000..403dcc06 --- /dev/null +++ b/careconnect2025/frontend/lib/services/notification_settings_service.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import '../config/env_constant.dart'; +import '../models/notification_settings.dart'; +import '../services/auth_token_manager.dart'; + +class NotificationSettingsService { + /// Get notification settings for a user + static Future getNotificationSettings( + int userId, + ) async { + try { + final headers = await AuthTokenManager.getAuthHeaders(); + final url = Uri.parse( + '${getBackendBaseUrl()}/v1/api/notification-settings/$userId', + ); + + final response = await http.get(url, headers: headers); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + return NotificationSettings.fromJson(json); + } else if (response.statusCode == 404) { + // Settings don't exist yet, return default settings + return NotificationSettings( + userId: userId, + gamification: true, + emergency: true, + videoCall: true, + audioCall: true, + sms: true, + significantVitals: true, + ); + } else { + print('Failed to get notification settings: ${response.statusCode}'); + return null; + } + } catch (e) { + print('Error getting notification settings: $e'); + return null; + } + } + + /// Create or update notification settings + static Future saveNotificationSettings( + NotificationSettings settings, + ) async { + try { + final headers = await AuthTokenManager.getAuthHeaders(); + final url = Uri.parse( + '${getBackendBaseUrl()}/v1/api/notification-settings', + ); + + final response = await http.post( + url, + headers: headers, + body: jsonEncode(settings.toJson()), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final json = jsonDecode(response.body); + return NotificationSettings.fromJson(json); + } else { + print('Failed to save notification settings: ${response.statusCode}'); + print('Response body: ${response.body}'); + return null; + } + } catch (e) { + print('Error saving notification settings: $e'); + return null; + } + } +} diff --git a/careconnect2025/frontend/lib/widgets/common_drawer.dart b/careconnect2025/frontend/lib/widgets/common_drawer.dart index 797ecc62..7b18a724 100644 --- a/careconnect2025/frontend/lib/widgets/common_drawer.dart +++ b/careconnect2025/frontend/lib/widgets/common_drawer.dart @@ -50,11 +50,55 @@ class _CommonDrawerState extends State { Widget build(BuildContext context) { final userProvider = Provider.of(context); final user = userProvider.user; + + // Only show drawer for logged-in users + if (user == null) { + return Drawer( + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.login, + size: 64, + color: Theme.of(context).disabledColor, + ), + const SizedBox(height: 16), + Text( + 'Please log in', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).disabledColor, + ), + ), + const SizedBox(height: 8), + Text( + 'You need to be logged in to access navigation', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).disabledColor, + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + context.go('/login'); + }, + child: const Text('Login'), + ), + ], + ), + ), + ), + ); + } + final isCaregiver = - user != null && - (user.role.toUpperCase() == 'CAREGIVER' || - user.role.toUpperCase() == 'FAMILY_LINK' || - user.role.toUpperCase() == 'ADMIN'); + user.role.toUpperCase() == 'CAREGIVER' || + user.role.toUpperCase() == 'FAMILY_LINK' || + user.role.toUpperCase() == 'ADMIN'; return Drawer( child: ListView( @@ -88,7 +132,7 @@ class _CommonDrawerState extends State { ), const SizedBox(height: 8), Text( - user?.name ?? 'User', + user.name ?? 'User', style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Theme.of(context).appBarTheme.foregroundColor ?? @@ -99,7 +143,7 @@ class _CommonDrawerState extends State { Row( children: [ Text( - user?.role ?? '', + user.role, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: (Theme.of(context).appBarTheme.foregroundColor ?? @@ -162,7 +206,7 @@ class _CommonDrawerState extends State { isActive: widget.currentRoute == '/social-feed', onTap: () { Navigator.pop(context); - final userId = user?.id ?? 1; + final userId = user.id; context.go('/social-feed?userId=$userId'); }, ), From a0ec428c23b994c7c882efdb1f503da7ddf6bd88 Mon Sep 17 00:00:00 2001 From: Ashenafi-Tesfaye <65870089+Ashenafi-Tesfaye@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:42:36 -0400 Subject: [PATCH 07/18] reverting file management page dart file changes --- .../lib/pages/file_management_page.dart | 391 ++++++------------ 1 file changed, 129 insertions(+), 262 deletions(-) diff --git a/careconnect2025/frontend/lib/pages/file_management_page.dart b/careconnect2025/frontend/lib/pages/file_management_page.dart index 231b9f06..acba1d71 100644 --- a/careconnect2025/frontend/lib/pages/file_management_page.dart +++ b/careconnect2025/frontend/lib/pages/file_management_page.dart @@ -1,10 +1,7 @@ -import 'dart:typed_data'; - +import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; -import '../core/services/api_service.dart'; -import '../services/api_service.dart' as ApiService; -import '../services/auth_token_manager.dart'; import '../services/comprehensive_file_service.dart'; import '../services/enhanced_file_service.dart'; import '../providers/user_provider.dart'; @@ -31,8 +28,7 @@ class _FileManagementPageState extends State String _searchQuery = ''; FileCategory? _selectedCategory; final TextEditingController _searchController = TextEditingController(); - bool _isPatient = false; - bool _isCaregiver = false; + int? _userId; @override void initState() { @@ -66,7 +62,11 @@ class _FileManagementPageState extends State setState(() { _allFiles = files; _filteredFiles = files; + _userId = user.id; _isLoading = false; + print( + 'DEBUG: Category set as: $_selectedCategory, Files set as: $files', + ); }); } catch (e) { setState(() { @@ -111,52 +111,23 @@ class _FileManagementPageState extends State Future.microtask(() => Navigator.pushReplacementNamed(context, '/login')); return const Scaffold(body: Center(child: CircularProgressIndicator())); } - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; + final isCaregiver = user.role.toUpperCase() == 'CAREGIVER'; return Scaffold( appBar: AppBar( - title: Text( - 'File Management', - style: - theme.textTheme.headlineSmall?.copyWith( - color: AppTheme.textLight, - fontWeight: FontWeight.bold, - fontSize: 22, - ) ?? - TextStyle( - color: AppTheme.textLight, - fontWeight: FontWeight.bold, - fontSize: 22, - ), - ), - backgroundColor: AppTheme.primary, - foregroundColor: AppTheme.textLight, - iconTheme: IconThemeData(color: AppTheme.textLight), - elevation: 2, + title: const Text('File Management'), bottom: TabBar( controller: _tabController, - labelColor: AppTheme.textLight, - unselectedLabelColor: AppTheme.textLight.withOpacity(0.7), - indicatorColor: AppTheme.accent, tabs: const [ - Tab( - icon: Icon(Icons.folder, color: AppTheme.textLight), - text: 'My Files', - ), - Tab( - icon: Icon(Icons.analytics, color: AppTheme.textLight), - text: 'Analytics', - ), + Tab(icon: Icon(Icons.folder), text: 'My Files'), + Tab(icon: Icon(Icons.cloud_upload), text: 'Upload'), + Tab(icon: Icon(Icons.analytics), text: 'Analytics'), ], ), ), drawer: const CommonDrawer(currentRoute: '/file-management'), - body: Container( - color: AppTheme.backgroundSecondary, - child: TabBarView( - controller: _tabController, - children: [_buildFilesTab(), _buildAnalyticsTab()], - ), + body: TabBarView( + controller: _tabController, + children: [_buildFilesTab(), _buildUploadTab(), _buildAnalyticsTab()], ), ); } @@ -165,75 +136,6 @@ class _FileManagementPageState extends State return Column( children: [ _buildSearchAndFilter(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - icon: const Icon(Icons.cloud_upload), - label: const Text('Upload File'), - style: AppTheme.primaryButtonStyle, - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (context) { - final mq = MediaQuery.of(context); - return Padding( - padding: EdgeInsets.only( - bottom: mq.viewInsets.bottom, - top: 24, - left: 16, - right: 16, - ), - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 500, - maxHeight: mq.size.height * 0.85, - ), - child: SingleChildScrollView( - child: FileUploadWidget( - onUploadSuccess: (response) { - if (mounted) { - Navigator.of(context).maybePop(); - _loadFiles(); - ScaffoldMessenger.of(this.context).showSnackBar( - SnackBar( - content: Text( - 'File uploaded: ${response.originalFilename}', - ), - backgroundColor: AppTheme.success, - ), - ); - } - }, - onUploadError: (error) { - if (mounted) { - ScaffoldMessenger.of(this.context).showSnackBar( - SnackBar( - content: Text(error), - backgroundColor: AppTheme.error, - ), - ); - } - }, - showCategorySelector: true, - customTitle: 'Upload File', - ), - ), - ), - ); - }, - ); - }, - ), - ), - ), Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) @@ -361,7 +263,15 @@ class _FileManagementPageState extends State ), textAlign: TextAlign.center, ), - // Removed upload button from My Files tab + if (_searchQuery.isEmpty && _selectedCategory == null) ...[ + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () => _tabController.animateTo(1), + style: AppTheme.primaryButtonStyle, + icon: const Icon(Icons.cloud_upload), + label: const Text('Upload Files'), + ), + ], ], ), ); @@ -382,6 +292,28 @@ class _FileManagementPageState extends State } Widget _buildFileCard(UserFileDTO file) { + // File Name Only + // Get index of last dot + int dotIndex = file.fileName.lastIndexOf('.'); + + // Extract filename without extension + String baseName = (dotIndex != -1) + ? file.fileName.substring(0, dotIndex) + : file.fileName; // If no dot found, return full filename + + // Extension Name Only + String getFileExtension(String fileName) { + int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex != -1 && dotIndex != fileName.length - 1) { + return fileName.substring(dotIndex); + } + return ''; // No extension found + } + + // Usage: + String fileExtension = getFileExtension(file.fileName); + String extensionWithoutDot = fileExtension.replaceFirst('.', ''); // txt + final theme = Theme.of(context); return Card( margin: const EdgeInsets.only(bottom: 12), @@ -396,7 +328,7 @@ class _FileManagementPageState extends State ), ), title: Text( - file.originalFilename, + baseName, style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, @@ -420,10 +352,7 @@ class _FileManagementPageState extends State style: theme.textTheme.bodyMedium, ), Text(' • ', style: theme.textTheme.bodyMedium), - Text( - _formatDate(file.createdAt), - style: theme.textTheme.bodyMedium, - ), + Text(extensionWithoutDot, style: theme.textTheme.bodyMedium), ], ), if (file.description != null && file.description!.isNotEmpty) ...[ @@ -438,7 +367,28 @@ class _FileManagementPageState extends State ], ), trailing: PopupMenuButton( - onSelected: (String action) => _handleFileAction(action, file), + onSelected: (value) async { + switch (value) { + case 'download': + // TODO: Implement download functionality + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Download file feature coming soon.'), + backgroundColor: AppTheme.info, + ), + ); + break; + case 'delete': + // TODO: Implement delete functionality + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Delete file feature coming soon.'), + backgroundColor: AppTheme.info, + ), + ); + break; + } + }, itemBuilder: (BuildContext context) => [ PopupMenuItem( value: 'download', @@ -457,14 +407,6 @@ class _FileManagementPageState extends State contentPadding: EdgeInsets.zero, ), ), - PopupMenuItem( - value: 'info', - child: ListTile( - leading: Icon(Icons.info, color: theme.iconTheme.color), - title: Text('Info', style: theme.textTheme.bodyMedium), - contentPadding: EdgeInsets.zero, - ), - ), PopupMenuItem( value: 'delete', child: ListTile( @@ -508,7 +450,7 @@ class _FileManagementPageState extends State const SizedBox(width: 12), Expanded( child: Text( - 'Use Quick Upload for fast uploads, Manual Text Entry for notes, or Speech-to-Text to convert voice into text files.', + 'Use File Upload for files, Manual Text Entry for notes, or Speech-to-Text to convert voice into text files.', style: Theme.of(context).textTheme.bodyLarge, ), ), @@ -517,30 +459,10 @@ class _FileManagementPageState extends State ), ), const SizedBox(height: 24), - // Quick Upload Section - Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: QuickUploadButtons( - onUploadSuccess: (response) { - _loadFiles(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'File uploaded: ${response.originalFilename}', - ), - backgroundColor: Theme.of( - context, - ).colorScheme.secondary, - ), - ); - }, - ), - ), - ), - const SizedBox(height: 24), // File Upload Section FileUploadWidget( + patientId: _userId, + allowedCategories: FileCategory.values, onUploadSuccess: (response) { _loadFiles(); // Refresh the files list }, @@ -559,17 +481,17 @@ class _FileManagementPageState extends State child: Padding( padding: const EdgeInsets.all(16), child: ManualTextEntryCard( - onSave: (fileName, fileBytes) async { - // await _uploadManualTextFile(fileName, fileBytes); + patientId: _userId, + onUploadSuccess: (response) { + _loadFiles(); // Refresh the files list + }, + onUploadError: (error) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Manual entry uploaded: $fileName.txt'), - backgroundColor: Theme.of( - context, - ).colorScheme.secondary, + content: Text(error), + backgroundColor: Theme.of(context).colorScheme.error, ), ); - _loadFiles(); }, ), ), @@ -581,15 +503,17 @@ class _FileManagementPageState extends State child: Padding( padding: const EdgeInsets.all(16), child: SpeechToTextCard( - onSave: (fileName, fileBytes) async { - // await _saveRecognizedTextFile(fileName, fileBytes); + patientId: _userId, + onUploadSuccess: (response) { + _loadFiles(); // Refresh the files list + }, + onUploadError: (error) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Speech-to-text file saved'), - backgroundColor: Colors.green, + SnackBar( + content: Text(error), + backgroundColor: Theme.of(context).colorScheme.error, ), ); - _loadFiles(); }, ), ), @@ -714,20 +638,46 @@ class _FileManagementPageState extends State try { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Downloading ${file.originalFilename}...'), + content: Text('Downloading ${file.fileName}...'), duration: const Duration(seconds: 2), ), ); - final fileData = await EnhancedFileService.downloadFile(file.id); + final userProvider = Provider.of(context, listen: false); + final user = userProvider.user; + if (user == null) return; + + final fileData = await EnhancedFileService.downloadFileLegacy( + user.id, + file.fileUrl!, + ); + if (fileData != null) { - // In a real app, you'd save the file to the device - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Downloaded ${file.originalFilename}'), - backgroundColor: AppTheme.success, - ), - ); + // 1. Get device's Download directory + Directory? directory; + if (Platform.isAndroid || Platform.isIOS) { + directory = + await getApplicationDocumentsDirectory(); // App-local storage + } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + directory = await getDownloadsDirectory(); // User's Downloads folder + } + + if (directory != null) { + final filePath = '${directory.path}/${file.fileName}'; + final newFile = File(filePath); + + // 2. Write bytes to file + await newFile.writeAsBytes(fileData); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('File saved to ${newFile.path}'), + backgroundColor: AppTheme.success, + ), + ); + } else { + throw Exception('Could not access device storage'); + } } else { throw Exception('Failed to download file'); } @@ -759,6 +709,8 @@ class _FileManagementPageState extends State } void _showFileInfo(UserFileDTO file) { + String extensionWithoutDot = file.fileName.replaceFirst('.', ''); // txt + showDialog( context: context, builder: (context) => AlertDialog( @@ -770,9 +722,10 @@ class _FileManagementPageState extends State children: [ _buildInfoRow('Category', file.categoryDisplayName), _buildInfoRow('Size', _formatFileSize(file.fileSize)), - _buildInfoRow('Type', file.contentType), - _buildInfoRow('Created', _formatDate(file.createdAt)), - _buildInfoRow('Updated', _formatDate(file.updatedAt)), + _buildInfoRow('Type', extensionWithoutDot), + // Comment out created and updated date as they are not passed in from the API for now. + // _buildInfoRow('Created', _formatDate(file.createdAt)), + // _buildInfoRow('Updated', _formatDate(file.updatedAt)), if (file.description != null && file.description!.isNotEmpty) _buildInfoRow('Description', file.description!), ], @@ -863,89 +816,3 @@ class _FileManagementPageState extends State return '${date.day}/${date.month}/${date.year}'; } } - -/* -Future _uploadManualTextFile(String fileName, List fileBytes) async { - final userSession = await AuthTokenManager.getUserSession(); - if (userSession == null || userSession['id'] == null) { - throw Exception('User session not found'); - } - - final userRole = userSession['role'] as String? ?? ''; - - setState(() { - _isPatient = userRole.toUpperCase() == 'PATIENT'; - _isCaregiver = - userRole.toUpperCase() == 'CAREGIVER' || - userRole.toUpperCase() == 'FAMILY_LINK' || - userRole.toUpperCase() == 'ADMIN'; - }); - - try { - final userSession = await AuthTokenManager.getUserSession(); - int? profileId; - - if (_isPatient) { - profileId = userSession?['id'] as int?; - } else if (_isCaregiver) { - profileId = widget.patientUserId; - } - - if (profileId == null) { - throw Exception("Profile ID not found for the current user role"); - } - - final response = await ApiService.uploadUserFileFromBytes( - userId: profileId, - fileBytes: Uint8List.fromList(fileBytes), - fileName: '$fileName.txt', - category: 'manualTextEntry', - role: _isPatient ? 'PATIENT' : 'CAREGIVER', - ); - - if (response.statusCode == 200) { - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('File Saved'), - content: Text('File: $fileName.txt has been uploaded successfully.'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('Close'), - ), - ], - ), - ); - } else { - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Error'), - content: Text('Failed to upload file: $fileName.txt.'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('Close'), - ), - ], - ), - ); - } - } catch (e) { - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Error'), - content: Text('Error uploading file: $fileName.txt.'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('Close'), - ), - ], - ), - ); - } - - */ From 023dbddef056eb204c29c5f80020fa7db7da2631 Mon Sep 17 00:00:00 2001 From: Ashenafi-Tesfaye <65870089+Ashenafi-Tesfaye@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:58:48 -0400 Subject: [PATCH 08/18] build issue --- .../presentation/assign_task_screen.dart | 157 ++++++---- .../frontend/lib/widgets/task_widget.dart | 279 +++++++++++------- 2 files changed, 272 insertions(+), 164 deletions(-) diff --git a/careconnect2025/frontend/lib/features/tasks/presentation/assign_task_screen.dart b/careconnect2025/frontend/lib/features/tasks/presentation/assign_task_screen.dart index 6f5046be..744b397a 100644 --- a/careconnect2025/frontend/lib/features/tasks/presentation/assign_task_screen.dart +++ b/careconnect2025/frontend/lib/features/tasks/presentation/assign_task_screen.dart @@ -11,7 +11,7 @@ import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:care_connect_app/features/tasks/presentation/pre_defined_task_screen.dart'; -// This screen provides a choice of what type of task to assign to a patient +// This screen provides a choice of what type of task to assign to a patient // by loading the available pre-defined tasks or offering to create a custom task. class AssignTaskScreen extends StatefulWidget { final int patientId; @@ -32,6 +32,38 @@ class _AssignTaskScreenState extends State { bool loading = true; String? error; + // Static icon mapping for tree-shaking optimization + static const Map _iconMap = { + // Common Material Icons with their code points + 57344: Icons.task_alt, // task + 57345: Icons.assignment, // assignment + 57693: Icons.medical_services, // medical + 58133: Icons.fitness_center, // fitness + 58134: Icons.restaurant, // nutrition + 58135: Icons.bed, // rest + 58136: Icons.local_pharmacy, // medication + 58137: Icons.timer, // timer + 58138: Icons.schedule, // schedule + 58139: Icons.checklist, // checklist + 58140: Icons.note_add, // note + 58141: Icons.health_and_safety, // health + 58142: Icons.psychology, // mental health + 58143: Icons.directions_walk, // walking + 58144: Icons.water_drop, // hydration + 58145: Icons.self_improvement, // improvement + 58146: Icons.healing, // healing + 58147: Icons.monitor_heart, // heart monitor + 58148: Icons.bloodtype, // blood + 58149: Icons.thermostat, // temperature + // Add more mappings as needed + }; + + // Helper method to get icon from code with fallback + IconData _getIconFromCode(int? iconCode) { + if (iconCode == null) return Icons.task_alt; + return _iconMap[iconCode] ?? Icons.task_alt; + } + @override void initState() { super.initState(); @@ -44,11 +76,15 @@ class _AssignTaskScreenState extends State { error = null; }); try { - final response = await ApiService.getTaskTemplates(widget.patientId).timeout(const Duration(seconds: 30)); + final response = await ApiService.getTaskTemplates( + widget.patientId, + ).timeout(const Duration(seconds: 30)); if (response.statusCode == 200) { final List data = json.decode(response.body); setState(() { - templates = data.map((templateJson) => Template.fromJson(templateJson)).toList(); + templates = data + .map((templateJson) => Template.fromJson(templateJson)) + .toList(); loading = false; }); } else { @@ -68,65 +104,74 @@ class _AssignTaskScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text('Assign Task to ${widget.patientName}'), - ), + appBar: AppBar(title: Text('Assign Task to ${widget.patientName}')), drawer: const CommonDrawer(currentRoute: '/assign-task'), body: loading ? const Center(child: CircularProgressIndicator()) : error != null - ? Center(child: Text(error!)) - : Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - const Text( - 'Choose a task to assign to your patient.', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - const SizedBox(height: 10), + ? Center(child: Text(error!)) + : Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const Text( + 'Choose a task to assign to your patient.', + style: TextStyle(fontSize: 16), + ), + const SizedBox(height: 10), - Expanded( //Add a card to the top of this list for a custom task - child: ListView.builder( - itemCount: templates.length+1, - itemBuilder: (context, index) { - if (index == 0) { - return Card( - margin: const EdgeInsets.symmetric(vertical: 5), - child: ListTile( - leading: const Icon(Icons.task, size: 40, color: Colors.indigo), - title: const Text('Custom Task'), - subtitle: const Text('Create a custom task for your patient.'), - trailing: const Icon(Icons.arrow_forward_ios), - onTap: () { - context.push('/custom-task-scheduling?patientId=${widget.patientId}'); - }, - ), - ); - } - final template = templates[index-1]; - return Card( - margin: const EdgeInsets.symmetric(vertical: 5), - child: ListTile( - leading: Icon( - IconData(template.iconCode, fontFamily: 'MaterialIcons'), - size: 40, - color: Colors.indigo, - ), - title: Text(template.name), - subtitle: Text(template.description), - trailing: const Icon(Icons.arrow_forward_ios), - onTap: () { - context.push('/pre-defined-task?templateId=${template.id}&patientId=${widget.patientId}'); - }, + Expanded( + //Add a card to the top of this list for a custom task + child: ListView.builder( + itemCount: templates.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return Card( + margin: const EdgeInsets.symmetric(vertical: 5), + child: ListTile( + leading: Icon( + Icons.task, + size: 40, + color: Theme.of(context).colorScheme.primary, + ), + title: const Text('Custom Task'), + subtitle: const Text( + 'Create a custom task for your patient.', ), - ); - }, - ), - ), - ], + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () { + context.push( + '/custom-task-scheduling?patientId=${widget.patientId}', + ); + }, + ), + ); + } + final template = templates[index - 1]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 5), + child: ListTile( + leading: Icon( + _getIconFromCode(template.iconCode), + size: 40, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(template.name), + subtitle: Text(template.description), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () { + context.push( + '/pre-defined-task?templateId=${template.id}&patientId=${widget.patientId}', + ); + }, + ), + ); + }, + ), ), - ), + ], + ), + ), ); } -} \ No newline at end of file +} diff --git a/careconnect2025/frontend/lib/widgets/task_widget.dart b/careconnect2025/frontend/lib/widgets/task_widget.dart index a58d0ebd..b4d43131 100644 --- a/careconnect2025/frontend/lib/widgets/task_widget.dart +++ b/careconnect2025/frontend/lib/widgets/task_widget.dart @@ -28,6 +28,38 @@ class _TaskFormDialogState extends State { late DateTime date; TimeOfDay? timeOfDay; + // Static icon mapping for tree-shaking optimization + static const Map _iconMap = { + // Common Material Icons with their code points + 57344: Icons.task_alt, // task + 57345: Icons.assignment, // assignment + 57693: Icons.medical_services, // medical + 58133: Icons.fitness_center, // fitness + 58134: Icons.restaurant, // nutrition + 58135: Icons.bed, // rest + 58136: Icons.local_pharmacy, // medication + 58137: Icons.timer, // timer + 58138: Icons.schedule, // schedule + 58139: Icons.checklist, // checklist + 58140: Icons.note_add, // note + 58141: Icons.health_and_safety, // health + 58142: Icons.psychology, // mental health + 58143: Icons.directions_walk, // walking + 58144: Icons.water_drop, // hydration + 58145: Icons.self_improvement, // improvement + 58146: Icons.healing, // healing + 58147: Icons.monitor_heart, // heart monitor + 58148: Icons.bloodtype, // blood + 58149: Icons.thermostat, // temperature + // Add more mappings as needed + }; + + // Helper method to get icon from code with fallback + IconData _getIconFromCode(int? iconCode) { + if (iconCode == null) return Icons.task_alt; + return _iconMap[iconCode] ?? Icons.task_alt; + } + // For template selection List