Integrated the API for Symptoms Checker
parent
e35fe9ac05
commit
2ffd4cc1ae
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,73 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
void main() async {
|
||||
final file = File('assets/json/body_symptoms_data.json');
|
||||
final content = await file.readAsString();
|
||||
|
||||
print('File size: ${content.length} characters');
|
||||
|
||||
// Split into two parts
|
||||
final parts = content.split('export const SymptomsData = [');
|
||||
if (parts.length != 2) {
|
||||
print('ERROR: Could not split file properly');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
print('Split into ${parts.length} parts');
|
||||
|
||||
// Process BodySymptomsData
|
||||
var bodyPart = parts[0]
|
||||
.replaceAll('export const BodySymptomsData = ', '')
|
||||
.trim()
|
||||
.replaceAll(RegExp(r';$'), '');
|
||||
|
||||
print('Parsing body symptoms...');
|
||||
Map<String, dynamic> bodySymptomsData;
|
||||
try {
|
||||
bodySymptomsData = json.decode(bodyPart);
|
||||
print('✅ Body symptoms parsed: ${bodySymptomsData.length} body parts');
|
||||
} catch (e) {
|
||||
print('❌ Error parsing body symptoms: $e');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Process SymptomsData
|
||||
print('Processing symptoms data...');
|
||||
var symptomsPart = parts[1]
|
||||
.trim()
|
||||
.replaceAll(RegExp(r'\];$'), '')
|
||||
.replaceAllMapped(RegExp(r'\n(\s*)id:'), (m) => '\n${m.group(1)}"id":')
|
||||
.replaceAllMapped(RegExp(r'\n(\s*)type:'), (m) => '\n${m.group(1)}"type":')
|
||||
.replaceAllMapped(RegExp(r'\n(\s*)name:'), (m) => '\n${m.group(1)}"name":')
|
||||
.replaceAllMapped(RegExp(r'\n(\s*)common_name:'), (m) => '\n${m.group(1)}"common_name":');
|
||||
|
||||
print('Parsing symptoms...');
|
||||
List<dynamic> symptomsData;
|
||||
try {
|
||||
symptomsData = json.decode('[$symptomsPart]');
|
||||
print('✅ Symptoms parsed: ${symptomsData.length} symptoms');
|
||||
} catch (e) {
|
||||
print('❌ Error parsing symptoms: $e');
|
||||
print('First 1000 chars:');
|
||||
print('[$symptomsPart]'.substring(0, 1000));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Create final JSON
|
||||
print('Creating final JSON structure...');
|
||||
final jsonStructure = {
|
||||
'bodySymptoms': bodySymptomsData,
|
||||
'symptoms': symptomsData,
|
||||
};
|
||||
|
||||
// Write to file
|
||||
print('Writing to file...');
|
||||
final encoder = JsonEncoder.withIndent(' ');
|
||||
await file.writeAsString(encoder.convert(jsonStructure));
|
||||
|
||||
print('\n✅ SUCCESS! File converted to proper JSON');
|
||||
print('✅ Body parts: ${bodySymptomsData.length}');
|
||||
print('✅ Symptoms: ${symptomsData.length}');
|
||||
}
|
||||
|
||||
@ -0,0 +1,151 @@
|
||||
class BodySymptomResponseModel {
|
||||
final DataDetails? dataDetails;
|
||||
|
||||
BodySymptomResponseModel({
|
||||
this.dataDetails,
|
||||
});
|
||||
|
||||
factory BodySymptomResponseModel.fromJson(Map<String, dynamic> json) {
|
||||
return BodySymptomResponseModel(
|
||||
dataDetails: json['dataDetails'] != null ? DataDetails.fromJson(json['dataDetails']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'dataDetails': dataDetails?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class DataDetails {
|
||||
final List<OrganSymptomResult>? result;
|
||||
final int? id;
|
||||
final dynamic exception;
|
||||
final int? status;
|
||||
final bool? isCanceled;
|
||||
final bool? isCompleted;
|
||||
final bool? isCompletedSuccessfully;
|
||||
final int? creationOptions;
|
||||
final dynamic asyncState;
|
||||
final bool? isFaulted;
|
||||
|
||||
DataDetails({
|
||||
this.result,
|
||||
this.id,
|
||||
this.exception,
|
||||
this.status,
|
||||
this.isCanceled,
|
||||
this.isCompleted,
|
||||
this.isCompletedSuccessfully,
|
||||
this.creationOptions,
|
||||
this.asyncState,
|
||||
this.isFaulted,
|
||||
});
|
||||
|
||||
factory DataDetails.fromJson(Map<String, dynamic> json) {
|
||||
return DataDetails(
|
||||
result: json['Result'] != null ? (json['Result'] as List).map((item) => OrganSymptomResult.fromJson(item)).toList() : null,
|
||||
id: json['Id'],
|
||||
exception: json['Exception'],
|
||||
status: json['Status'],
|
||||
isCanceled: json['IsCanceled'],
|
||||
isCompleted: json['IsCompleted'],
|
||||
isCompletedSuccessfully: json['IsCompletedSuccessfully'],
|
||||
creationOptions: json['CreationOptions'],
|
||||
asyncState: json['AsyncState'],
|
||||
isFaulted: json['IsFaulted'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'Result': result?.map((item) => item.toJson()).toList(),
|
||||
'Id': id,
|
||||
'Exception': exception,
|
||||
'Status': status,
|
||||
'IsCanceled': isCanceled,
|
||||
'IsCompleted': isCompleted,
|
||||
'IsCompletedSuccessfully': isCompletedSuccessfully,
|
||||
'CreationOptions': creationOptions,
|
||||
'AsyncState': asyncState,
|
||||
'IsFaulted': isFaulted,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class OrganSymptomResult {
|
||||
final String? name;
|
||||
final List<BodySymptom>? bodySymptoms;
|
||||
|
||||
OrganSymptomResult({
|
||||
this.name,
|
||||
this.bodySymptoms,
|
||||
});
|
||||
|
||||
factory OrganSymptomResult.fromJson(Map<String, dynamic> json) {
|
||||
return OrganSymptomResult(
|
||||
name: json['name'],
|
||||
bodySymptoms: json['bodySymptoms'] != null ? (json['bodySymptoms'] as List).map((item) => BodySymptom.fromJson(item)).toList() : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'bodySymptoms': bodySymptoms?.map((item) => item.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class BodySymptom {
|
||||
final String? id;
|
||||
final String? type;
|
||||
final String? symptomsName;
|
||||
final String? name;
|
||||
final String? commonName;
|
||||
final String? nameAr;
|
||||
final String? commonNameAr;
|
||||
|
||||
BodySymptom({
|
||||
this.id,
|
||||
this.type,
|
||||
this.symptomsName,
|
||||
this.name,
|
||||
this.commonName,
|
||||
this.nameAr,
|
||||
this.commonNameAr,
|
||||
});
|
||||
|
||||
factory BodySymptom.fromJson(Map<String, dynamic> json) {
|
||||
return BodySymptom(
|
||||
id: json['Id'],
|
||||
type: json['type'],
|
||||
symptomsName: json['symptoms_name'],
|
||||
name: json['name'],
|
||||
commonName: json['common_name'],
|
||||
nameAr: json['nameAr'],
|
||||
commonNameAr: json['common_nameAr'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'Id': id,
|
||||
'type': type,
|
||||
'symptoms_name': symptomsName,
|
||||
'name': name,
|
||||
'common_name': commonName,
|
||||
'nameAr': nameAr,
|
||||
'common_nameAr': commonNameAr,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper method to get display name based on locale
|
||||
String getDisplayName(bool isArabic) {
|
||||
if (isArabic) {
|
||||
return commonNameAr ?? nameAr ?? commonName ?? name ?? '';
|
||||
}
|
||||
return commonName ?? name ?? '';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:hmg_patient_app_new/core/api/api_client.dart';
|
||||
import 'package:hmg_patient_app_new/core/api_consts.dart';
|
||||
import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
|
||||
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart';
|
||||
import 'package:hmg_patient_app_new/services/logger_service.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
abstract class SymptomsCheckerRepo {
|
||||
Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>> getBodySymptomsByName({
|
||||
required List<String> organNames,
|
||||
});
|
||||
}
|
||||
|
||||
class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
|
||||
final ApiClient apiClient;
|
||||
final LoggerService loggerService;
|
||||
|
||||
SymptomsCheckerRepoImp({
|
||||
required this.apiClient,
|
||||
required this.loggerService,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, GenericApiModel<BodySymptomResponseModel>>> getBodySymptomsByName({
|
||||
required List<String> organNames,
|
||||
}) async {
|
||||
try {
|
||||
// API expects a direct JSON array: ["mid_abdomen", "chest"]
|
||||
// Not an object like: {"organNames": [...]}
|
||||
// Since ApiClient.post expects Map<String, dynamic> and encodes it as object,
|
||||
// we make direct HTTP call here to send array body
|
||||
|
||||
final String requestBody = jsonEncode(organNames);
|
||||
|
||||
loggerService.logInfo("GetBodySymptomsByName Request: $requestBody");
|
||||
log("GetBodySymptomsByName Request URL: ${ApiConsts.getBodySymptomsByName}");
|
||||
log("GetBodySymptomsByName Request Body: $requestBody");
|
||||
|
||||
// Make direct HTTP POST request with JSON array body
|
||||
final response = await http.post(
|
||||
Uri.parse(ApiConsts.getBodySymptomsByName),
|
||||
headers: {'Content-Type': 'application/json', 'Accept': 'text/plain'},
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
final int statusCode = response.statusCode;
|
||||
|
||||
log("GetBodySymptomsByName Response Status: $statusCode");
|
||||
loggerService.logInfo("GetBodySymptomsByName Response Status: $statusCode");
|
||||
|
||||
try {
|
||||
// Parse the response
|
||||
final responseBody = jsonDecode(response.body);
|
||||
|
||||
loggerService.logInfo("GetBodySymptomsByName API Success: $responseBody");
|
||||
log("GetBodySymptomsByName Response: $responseBody");
|
||||
|
||||
BodySymptomResponseModel bodySymptomResponse = BodySymptomResponseModel.fromJson(responseBody);
|
||||
|
||||
GenericApiModel<BodySymptomResponseModel> apiResponse = GenericApiModel<BodySymptomResponseModel>(
|
||||
messageStatus: 1,
|
||||
statusCode: statusCode,
|
||||
errorMessage: null,
|
||||
data: bodySymptomResponse,
|
||||
);
|
||||
|
||||
return Right(apiResponse);
|
||||
} catch (e, stackTrace) {
|
||||
loggerService.logError("Error parsing GetBodySymptomsByName response: $e");
|
||||
loggerService.logError("StackTrace: $stackTrace");
|
||||
log("Parse Error: $e");
|
||||
return Left(DataParsingFailure(e.toString()));
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
loggerService.logError("Exception in getBodySymptomsByName: $e");
|
||||
loggerService.logError("StackTrace: $stackTrace");
|
||||
log("Exception: $e");
|
||||
return Left(UnknownFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,326 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:hmg_patient_app_new/core/enums.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/data/organ_mapping_data.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_repo.dart';
|
||||
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
|
||||
|
||||
class SymptomsCheckerViewModel extends ChangeNotifier {
|
||||
final SymptomsCheckerRepo symptomsCheckerRepo;
|
||||
final ErrorHandlerService errorHandlerService;
|
||||
|
||||
SymptomsCheckerViewModel({
|
||||
required this.symptomsCheckerRepo,
|
||||
required this.errorHandlerService,
|
||||
});
|
||||
|
||||
// State variables
|
||||
bool isBodyHidden = false;
|
||||
BodyView _currentView = BodyView.front;
|
||||
final Set<String> _selectedOrganIds = {};
|
||||
bool _isBottomSheetExpanded = false;
|
||||
|
||||
// Tooltip state
|
||||
String? _tooltipOrganId;
|
||||
Timer? _tooltipTimer;
|
||||
|
||||
// API loading states
|
||||
bool isBodySymptomsLoading = false;
|
||||
|
||||
// API data storage - using API models directly
|
||||
BodySymptomResponseModel? bodySymptomResponse;
|
||||
|
||||
// Selected symptoms tracking (organId -> Set of symptom IDs)
|
||||
final Map<String, Set<String>> _selectedSymptomsByOrgan = {};
|
||||
|
||||
// Getters
|
||||
|
||||
bool isPossibleConditionsLoading = false;
|
||||
|
||||
BodyView get currentView => _currentView;
|
||||
|
||||
Set<String> get selectedOrganIds => _selectedOrganIds;
|
||||
|
||||
bool get isBottomSheetExpanded => _isBottomSheetExpanded;
|
||||
|
||||
String? get tooltipOrganId => _tooltipOrganId;
|
||||
|
||||
/// Get organs for current view
|
||||
List<OrganModel> get currentOrgans => OrganData.getOrgansForView(_currentView);
|
||||
|
||||
/// Get all selected organs from both views
|
||||
List<OrganModel> get selectedOrgans {
|
||||
final allOrgans = [
|
||||
...OrganData.frontViewOrgans,
|
||||
...OrganData.backViewOrgans,
|
||||
];
|
||||
return allOrgans.where((organ) => _selectedOrganIds.contains(organ.id)).toList();
|
||||
}
|
||||
|
||||
/// Check if any organs are selected
|
||||
bool get hasSelectedOrgans => _selectedOrganIds.isNotEmpty;
|
||||
|
||||
/// Get count of selected organs
|
||||
int get selectedOrgansCount => _selectedOrganIds.length;
|
||||
|
||||
/// Get organ symptoms from API response
|
||||
List<OrganSymptomResult> get organSymptomsResults {
|
||||
if (bodySymptomResponse?.dataDetails?.result == null) {
|
||||
return [];
|
||||
}
|
||||
return bodySymptomResponse!.dataDetails!.result ?? [];
|
||||
}
|
||||
|
||||
/// Get total selected symptoms count across all organs
|
||||
int get totalSelectedSymptomsCount {
|
||||
return _selectedSymptomsByOrgan.values.fold(0, (sum, symptomIds) => sum + symptomIds.length);
|
||||
}
|
||||
|
||||
/// Check if at least one symptom is selected
|
||||
bool get hasSelectedSymptoms {
|
||||
return _selectedSymptomsByOrgan.values.any((symptomIds) => symptomIds.isNotEmpty);
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
/// Toggle between front and back body view
|
||||
void toggleView() {
|
||||
_currentView = _currentView == BodyView.front ? BodyView.back : BodyView.front;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
toggleIsBodyHidden() {
|
||||
isBodyHidden = !isBodyHidden;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Toggle organ selection (add if not selected, remove if selected)
|
||||
void toggleOrganSelection(String organId) {
|
||||
if (_selectedOrganIds.contains(organId)) {
|
||||
_selectedOrganIds.remove(organId);
|
||||
} else {
|
||||
_selectedOrganIds.add(organId);
|
||||
}
|
||||
|
||||
// Show tooltip
|
||||
_showTooltip(organId);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Show tooltip for an organ
|
||||
void _showTooltip(String organId) {
|
||||
// Cancel any existing timer
|
||||
_tooltipTimer?.cancel();
|
||||
|
||||
// Set the tooltip organ
|
||||
_tooltipOrganId = organId;
|
||||
notifyListeners();
|
||||
|
||||
// Hide tooltip after 2 seconds
|
||||
_tooltipTimer = Timer(const Duration(seconds: 1), () {
|
||||
_tooltipOrganId = null;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
/// Hide tooltip immediately
|
||||
void hideTooltip() {
|
||||
_tooltipTimer?.cancel();
|
||||
_tooltipOrganId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Remove a specific organ from selection
|
||||
void removeOrgan(String organId) {
|
||||
_selectedOrganIds.remove(organId);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear all selected organs
|
||||
void clearAllSelections() {
|
||||
_selectedOrganIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Toggle bottom sheet expanded/collapsed state
|
||||
void toggleBottomSheet() {
|
||||
_isBottomSheetExpanded = !_isBottomSheetExpanded;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set bottom sheet expanded state
|
||||
void setBottomSheetExpanded(bool isExpanded) {
|
||||
_isBottomSheetExpanded = isExpanded;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Validate if at least one organ is selected
|
||||
bool validateSelection() {
|
||||
return _selectedOrganIds.isNotEmpty;
|
||||
}
|
||||
|
||||
/// Get selected organ IDs as a list
|
||||
List<String> getSelectedOrganIds() {
|
||||
return _selectedOrganIds.toList();
|
||||
}
|
||||
|
||||
/// Get selected organ names as a list
|
||||
List<String> getSelectedOrganNames() {
|
||||
return selectedOrgans.map((organ) => organ.description).toList();
|
||||
}
|
||||
|
||||
/// Initialize symptoms from API based on selected organs
|
||||
Future<void> initializeSymptomGroups({
|
||||
Function()? onSuccess,
|
||||
Function(String)? onError,
|
||||
}) async {
|
||||
if (_selectedOrganIds.isEmpty) {
|
||||
if (onError != null) {
|
||||
onError('No organs selected');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the 'name' field from selected organs to send to API
|
||||
List<String> organNames = selectedOrgans.map((organ) => organ.name).toList();
|
||||
|
||||
// Fetch symptoms from API
|
||||
await getBodySymptomsByName(
|
||||
organNames: organNames,
|
||||
onSuccess: (response) {
|
||||
// API response is already stored in bodySymptomResponse
|
||||
if (onSuccess != null) {
|
||||
onSuccess();
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (onError != null) {
|
||||
onError(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Toggle symptom selection for a specific organ
|
||||
void toggleSymptomSelection(String organId, String symptomId) {
|
||||
if (!_selectedSymptomsByOrgan.containsKey(organId)) {
|
||||
_selectedSymptomsByOrgan[organId] = {};
|
||||
}
|
||||
|
||||
if (_selectedSymptomsByOrgan[organId]!.contains(symptomId)) {
|
||||
_selectedSymptomsByOrgan[organId]!.remove(symptomId);
|
||||
} else {
|
||||
_selectedSymptomsByOrgan[organId]!.add(symptomId);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Check if a symptom is selected
|
||||
bool isSymptomSelected(String organId, String symptomId) {
|
||||
return _selectedSymptomsByOrgan[organId]?.contains(symptomId) ?? false;
|
||||
}
|
||||
|
||||
/// Get all selected symptoms across all organs (using API models)
|
||||
List<BodySymptom> getAllSelectedSymptoms() {
|
||||
List<BodySymptom> allSymptoms = [];
|
||||
|
||||
if (bodySymptomResponse?.dataDetails?.result == null) {
|
||||
return allSymptoms;
|
||||
}
|
||||
|
||||
for (var organResult in bodySymptomResponse!.dataDetails!.result!) {
|
||||
// Find matching organ ID
|
||||
String? matchingOrganId;
|
||||
for (var organ in selectedOrgans) {
|
||||
if (organ.name == organResult.name) {
|
||||
matchingOrganId = organ.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingOrganId != null && _selectedSymptomsByOrgan.containsKey(matchingOrganId)) {
|
||||
final selectedIds = _selectedSymptomsByOrgan[matchingOrganId]!;
|
||||
|
||||
if (organResult.bodySymptoms != null) {
|
||||
for (var symptom in organResult.bodySymptoms!) {
|
||||
if (symptom.id != null && selectedIds.contains(symptom.id)) {
|
||||
allSymptoms.add(symptom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allSymptoms;
|
||||
}
|
||||
|
||||
/// Clear all symptom selections
|
||||
void clearAllSymptomSelections() {
|
||||
_selectedSymptomsByOrgan.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Reset the view model to initial state
|
||||
void reset() {
|
||||
_currentView = BodyView.front;
|
||||
_selectedOrganIds.clear();
|
||||
_selectedSymptomsByOrgan.clear();
|
||||
bodySymptomResponse = null;
|
||||
_isBottomSheetExpanded = false;
|
||||
_tooltipTimer?.cancel();
|
||||
_tooltipOrganId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Fetch body symptoms by organ names from API
|
||||
Future<void> getBodySymptomsByName({
|
||||
required List<String> organNames,
|
||||
Function(BodySymptomResponseModel)? onSuccess,
|
||||
Function(String)? onError,
|
||||
}) async {
|
||||
isBodySymptomsLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
final result = await symptomsCheckerRepo.getBodySymptomsByName(
|
||||
organNames: organNames,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
// Handle failure
|
||||
(failure) async {
|
||||
isBodySymptomsLoading = false;
|
||||
notifyListeners();
|
||||
await errorHandlerService.handleError(failure: failure);
|
||||
if (onError != null) {
|
||||
onError(failure.toString());
|
||||
}
|
||||
},
|
||||
// Handle success
|
||||
(apiResponse) {
|
||||
isBodySymptomsLoading = false;
|
||||
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
|
||||
bodySymptomResponse = apiResponse.data;
|
||||
notifyListeners();
|
||||
if (onSuccess != null) {
|
||||
onSuccess(apiResponse.data!);
|
||||
}
|
||||
} else {
|
||||
notifyListeners();
|
||||
if (onError != null) {
|
||||
onError(apiResponse.errorMessage ?? 'Failed to fetch symptoms');
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tooltipTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,291 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RiskFactorsScreen extends StatefulWidget {
|
||||
const RiskFactorsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RiskFactorsScreen> createState() => _RiskFactorsScreenState();
|
||||
}
|
||||
|
||||
class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize symptom groups based on selected organs
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final viewModel = context.read<SymptomsCheckerViewModel>();
|
||||
viewModel.initializeSymptomGroups();
|
||||
});
|
||||
}
|
||||
|
||||
void _onOptionSelected(int optionIndex) {}
|
||||
|
||||
void _onNextPressed(SymptomsCheckerViewModel viewModel) {
|
||||
if (viewModel.hasSelectedSymptoms) {
|
||||
// Navigate to triage screen
|
||||
context.navigateWithName(AppRoutes.suggestionsScreen);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Please select at least one option before proceeding'.needTranslation),
|
||||
backgroundColor: AppColors.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPreviousPressed() {
|
||||
context.pop();
|
||||
}
|
||||
|
||||
_buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) {
|
||||
return showCommonBottomSheetWithoutHeight(
|
||||
title: LocaleKeys.notice.tr(context: context),
|
||||
context,
|
||||
child: Utils.getWarningWidget(
|
||||
loadingText: "Are you sure you want to restart the organ selection?".needTranslation,
|
||||
isShowActionButtons: true,
|
||||
onCancelTap: () => Navigator.pop(context),
|
||||
onConfirmTap: () => onConfirm(),
|
||||
),
|
||||
callBackFunc: () {},
|
||||
isFullScreen: false,
|
||||
isCloseButtonVisible: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionItem(int index, bool selected, String optionText) {
|
||||
return GestureDetector(
|
||||
onTap: () => _onOptionSelected(index),
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(bottom: 12.h),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? AppColors.primaryRedColor : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(5.r),
|
||||
border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.borderGrayColor, width: 1.w),
|
||||
),
|
||||
child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
optionText,
|
||||
style: TextStyle(fontSize: 14.f, color: AppColors.textColor, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildFactorsList() {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
final offsetAnimation = Tween<Offset>(
|
||||
begin: const Offset(1.0, 0.0),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
return SlideTransition(
|
||||
position: offsetAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
|
||||
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...List.generate(4, (index) {
|
||||
return _buildOptionItem(index, false, "currentQuestion.options[index].text");
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgScaffoldColor,
|
||||
body: Consumer<SymptomsCheckerViewModel>(
|
||||
builder: (context, viewModel, _) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CollapsingListView(
|
||||
title: "Risks".needTranslation,
|
||||
onLeadingTapped: () => _buildConfirmationBottomSheet(
|
||||
context: context,
|
||||
onConfirm: () => {
|
||||
context.pop(),
|
||||
context.pop(),
|
||||
}),
|
||||
child: _buildEmptyState(),
|
||||
// child: viewModel.organSymptomsGroups.isEmpty
|
||||
// ? _buildEmptyState()
|
||||
// : Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// SizedBox(height: 16.h),
|
||||
// ...viewModel.organSymptomsGroups.map((group) {
|
||||
// return Padding(
|
||||
// padding: EdgeInsets.only(bottom: 16.h),
|
||||
// child: Container(
|
||||
// width: double.infinity,
|
||||
// margin: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
|
||||
// padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// 'Possible symptoms related to "${group.organName}"',
|
||||
// style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// SizedBox(height: 24.h),
|
||||
// Wrap(
|
||||
// runSpacing: 12.h,
|
||||
// spacing: 8.w,
|
||||
// children: group.symptoms.map((symptom) {
|
||||
// bool isSelected = viewModel.isSymptomSelected(group.organId, symptom.id);
|
||||
// return GestureDetector(
|
||||
// onTap: () => viewModel.toggleSymptomSelection(group.organId, symptom.id),
|
||||
// child: CustomSelectableChip(
|
||||
// label: symptom.name,
|
||||
// selected: isSelected,
|
||||
// activeColor: AppColors.primaryRedBorderColor,
|
||||
// activeTextColor: AppColors.primaryRedBorderColor,
|
||||
// inactiveBorderColor: AppColors.bottomNAVBorder,
|
||||
// inactiveTextColor: AppColors.textColor,
|
||||
// ),
|
||||
// );
|
||||
// }).toList(),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// ],
|
||||
// ),
|
||||
),
|
||||
),
|
||||
_buildStickyBottomCard(context, viewModel),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.h),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 64.h, color: AppColors.greyTextColor),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
'No organs selected'.needTranslation,
|
||||
style: TextStyle(
|
||||
fontSize: 18.f,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textColor,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
'Please go back and select organs first'.needTranslation,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.f,
|
||||
color: AppColors.greyTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) {
|
||||
return Container(
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 16.h),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomButton(
|
||||
text: "Previous".needTranslation,
|
||||
onPressed: _onPreviousPressed,
|
||||
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11),
|
||||
borderColor: Colors.transparent,
|
||||
textColor: AppColors.primaryRedColor,
|
||||
fontSize: 16.f,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: CustomButton(
|
||||
text: "Next".needTranslation,
|
||||
onPressed: () => _onNextPressed(viewModel),
|
||||
backgroundColor: AppColors.primaryRedColor,
|
||||
borderColor: AppColors.primaryRedColor,
|
||||
textColor: AppColors.whiteColor,
|
||||
fontSize: 16.f,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
).paddingSymmetrical(24.w, 0),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,292 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SuggestionsScreen extends StatefulWidget {
|
||||
const SuggestionsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SuggestionsScreen> createState() => _SuggestionsScreenState();
|
||||
}
|
||||
|
||||
class _SuggestionsScreenState extends State<SuggestionsScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize symptom groups based on selected organs
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final viewModel = context.read<SymptomsCheckerViewModel>();
|
||||
viewModel.initializeSymptomGroups();
|
||||
});
|
||||
}
|
||||
|
||||
void _onOptionSelected(int optionIndex) {}
|
||||
|
||||
void _onNextPressed(SymptomsCheckerViewModel viewModel) {
|
||||
if (viewModel.hasSelectedSymptoms) {
|
||||
// Navigate to triage screen
|
||||
context.navigateWithName(AppRoutes.triageScreen);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Please select at least one option before proceeding'.needTranslation),
|
||||
backgroundColor: AppColors.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPreviousPressed() {
|
||||
context.pop();
|
||||
}
|
||||
|
||||
_buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) {
|
||||
return showCommonBottomSheetWithoutHeight(
|
||||
title: LocaleKeys.notice.tr(context: context),
|
||||
context,
|
||||
child: Utils.getWarningWidget(
|
||||
loadingText: "Are you sure you want to restart the organ selection?".needTranslation,
|
||||
isShowActionButtons: true,
|
||||
onCancelTap: () => Navigator.pop(context),
|
||||
onConfirmTap: () => onConfirm(),
|
||||
),
|
||||
callBackFunc: () {},
|
||||
isFullScreen: false,
|
||||
isCloseButtonVisible: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionItem(int index, bool selected, String optionText) {
|
||||
return GestureDetector(
|
||||
onTap: () => _onOptionSelected(index),
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(bottom: 12.h),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? AppColors.primaryRedColor : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(5.r),
|
||||
border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.borderGrayColor, width: 1.w),
|
||||
),
|
||||
child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
optionText,
|
||||
style: TextStyle(fontSize: 14.f, color: AppColors.textColor, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildFactorsList() {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
final offsetAnimation = Tween<Offset>(
|
||||
begin: const Offset(1.0, 0.0),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
return SlideTransition(
|
||||
position: offsetAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
|
||||
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...List.generate(4, (index) {
|
||||
return _buildOptionItem(index, false, "currentQuestion.options[index].text");
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgScaffoldColor,
|
||||
body: Consumer<SymptomsCheckerViewModel>(
|
||||
builder: (context, viewModel, _) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CollapsingListView(
|
||||
title: "Suggestions".needTranslation,
|
||||
onLeadingTapped: () => _buildConfirmationBottomSheet(
|
||||
context: context,
|
||||
onConfirm: () => {
|
||||
context.pop(),
|
||||
context.pop(),
|
||||
}),
|
||||
child: _buildEmptyState(),
|
||||
|
||||
// child: viewModel.organSymptomsGroups.isEmpty
|
||||
// ? _buildEmptyState()
|
||||
// : Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// SizedBox(height: 16.h),
|
||||
// ...viewModel.organSymptomsGroups.map((group) {
|
||||
// return Padding(
|
||||
// padding: EdgeInsets.only(bottom: 16.h),
|
||||
// child: Container(
|
||||
// width: double.infinity,
|
||||
// margin: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
|
||||
// padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// 'Possible symptoms related to "${group.organName}"',
|
||||
// style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// SizedBox(height: 24.h),
|
||||
// Wrap(
|
||||
// runSpacing: 12.h,
|
||||
// spacing: 8.w,
|
||||
// children: group.symptoms.map((symptom) {
|
||||
// bool isSelected = viewModel.isSymptomSelected(group.organId, symptom.id);
|
||||
// return GestureDetector(
|
||||
// onTap: () => viewModel.toggleSymptomSelection(group.organId, symptom.id),
|
||||
// child: CustomSelectableChip(
|
||||
// label: symptom.name,
|
||||
// selected: isSelected,
|
||||
// activeColor: AppColors.primaryRedBorderColor,
|
||||
// activeTextColor: AppColors.primaryRedBorderColor,
|
||||
// inactiveBorderColor: AppColors.bottomNAVBorder,
|
||||
// inactiveTextColor: AppColors.textColor,
|
||||
// ),
|
||||
// );
|
||||
// }).toList(),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// ],
|
||||
// ),
|
||||
),
|
||||
),
|
||||
_buildStickyBottomCard(context, viewModel),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.h),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 64.h, color: AppColors.greyTextColor),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
'No organs selected'.needTranslation,
|
||||
style: TextStyle(
|
||||
fontSize: 18.f,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textColor,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
'Please go back and select organs first'.needTranslation,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.f,
|
||||
color: AppColors.greyTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) {
|
||||
return Container(
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 16.h),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomButton(
|
||||
text: "Previous".needTranslation,
|
||||
onPressed: _onPreviousPressed,
|
||||
backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11),
|
||||
borderColor: Colors.transparent,
|
||||
textColor: AppColors.primaryRedColor,
|
||||
fontSize: 16.f,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: CustomButton(
|
||||
text: "Next".needTranslation,
|
||||
onPressed: () => _onNextPressed(viewModel),
|
||||
backgroundColor: AppColors.primaryRedColor,
|
||||
borderColor: AppColors.primaryRedColor,
|
||||
textColor: AppColors.whiteColor,
|
||||
fontSize: 16.f,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
).paddingSymmetrical(24.w, 0),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,240 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:hmg_patient_app_new/core/enums.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/data/symptoms_mapping_data.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/models/symptom_model.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/organ_mapping_data.dart';
|
||||
|
||||
class SymptomsCheckerViewModel extends ChangeNotifier {
|
||||
// State variables
|
||||
bool isBodyHidden = false;
|
||||
BodyView _currentView = BodyView.front;
|
||||
final Set<String> _selectedOrganIds = {};
|
||||
bool _isBottomSheetExpanded = false;
|
||||
|
||||
// Symptom selection state
|
||||
final Map<String, OrganSymptomsGroup> _organSymptomsGroups = {};
|
||||
|
||||
// Tooltip state
|
||||
String? _tooltipOrganId;
|
||||
Timer? _tooltipTimer;
|
||||
|
||||
// Getters
|
||||
|
||||
bool isPossibleConditionsLoading = false;
|
||||
|
||||
BodyView get currentView => _currentView;
|
||||
|
||||
Set<String> get selectedOrganIds => _selectedOrganIds;
|
||||
|
||||
bool get isBottomSheetExpanded => _isBottomSheetExpanded;
|
||||
|
||||
String? get tooltipOrganId => _tooltipOrganId;
|
||||
|
||||
/// Get organs for current view
|
||||
List<OrganModel> get currentOrgans => OrganData.getOrgansForView(_currentView);
|
||||
|
||||
/// Get all selected organs from both views
|
||||
List<OrganModel> get selectedOrgans {
|
||||
final allOrgans = [
|
||||
...OrganData.frontViewOrgans,
|
||||
...OrganData.backViewOrgans,
|
||||
];
|
||||
return allOrgans.where((organ) => _selectedOrganIds.contains(organ.id)).toList();
|
||||
}
|
||||
|
||||
/// Check if any organs are selected
|
||||
bool get hasSelectedOrgans => _selectedOrganIds.isNotEmpty;
|
||||
|
||||
/// Get count of selected organs
|
||||
int get selectedOrgansCount => _selectedOrganIds.length;
|
||||
|
||||
/// Get organ symptoms groups for selected organs
|
||||
List<OrganSymptomsGroup> get organSymptomsGroups {
|
||||
return _organSymptomsGroups.values.toList();
|
||||
}
|
||||
|
||||
/// Get total selected symptoms count across all organs
|
||||
int get totalSelectedSymptomsCount {
|
||||
return _organSymptomsGroups.values.fold(0, (sum, group) => sum + group.selectedCount);
|
||||
}
|
||||
|
||||
/// Check if at least one symptom is selected
|
||||
bool get hasSelectedSymptoms {
|
||||
return _organSymptomsGroups.values.any((group) => group.hasSelectedSymptoms);
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
/// Toggle between front and back body view
|
||||
void toggleView() {
|
||||
_currentView = _currentView == BodyView.front ? BodyView.back : BodyView.front;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
toggleIsBodyHidden() {
|
||||
isBodyHidden = !isBodyHidden;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Toggle organ selection (add if not selected, remove if selected)
|
||||
void toggleOrganSelection(String organId) {
|
||||
if (_selectedOrganIds.contains(organId)) {
|
||||
_selectedOrganIds.remove(organId);
|
||||
} else {
|
||||
_selectedOrganIds.add(organId);
|
||||
}
|
||||
|
||||
// Show tooltip
|
||||
_showTooltip(organId);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Show tooltip for an organ
|
||||
void _showTooltip(String organId) {
|
||||
// Cancel any existing timer
|
||||
_tooltipTimer?.cancel();
|
||||
|
||||
// Set the tooltip organ
|
||||
_tooltipOrganId = organId;
|
||||
notifyListeners();
|
||||
|
||||
// Hide tooltip after 2 seconds
|
||||
_tooltipTimer = Timer(const Duration(seconds: 1), () {
|
||||
_tooltipOrganId = null;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
/// Hide tooltip immediately
|
||||
void hideTooltip() {
|
||||
_tooltipTimer?.cancel();
|
||||
_tooltipOrganId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Remove a specific organ from selection
|
||||
void removeOrgan(String organId) {
|
||||
_selectedOrganIds.remove(organId);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear all selected organs
|
||||
void clearAllSelections() {
|
||||
_selectedOrganIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Toggle bottom sheet expanded/collapsed state
|
||||
void toggleBottomSheet() {
|
||||
_isBottomSheetExpanded = !_isBottomSheetExpanded;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set bottom sheet expanded state
|
||||
void setBottomSheetExpanded(bool isExpanded) {
|
||||
_isBottomSheetExpanded = isExpanded;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Validate if at least one organ is selected
|
||||
bool validateSelection() {
|
||||
return _selectedOrganIds.isNotEmpty;
|
||||
}
|
||||
|
||||
/// Get selected organ IDs as a list
|
||||
List<String> getSelectedOrganIds() {
|
||||
return _selectedOrganIds.toList();
|
||||
}
|
||||
|
||||
/// Get selected organ names as a list
|
||||
List<String> getSelectedOrganNames() {
|
||||
return selectedOrgans.map((organ) => organ.name).toList();
|
||||
}
|
||||
|
||||
/// Initialize symptom groups based on selected organs
|
||||
void initializeSymptomGroups() {
|
||||
_organSymptomsGroups.clear();
|
||||
|
||||
for (String organId in _selectedOrganIds) {
|
||||
List<SymptomModel> symptoms = SymptomsMappingData.getSymptomsForOrgan(organId);
|
||||
if (symptoms.isNotEmpty) {
|
||||
// Find organ name from selectedOrgans
|
||||
String organName = selectedOrgans
|
||||
.firstWhere((organ) => organ.id == organId, orElse: () => OrganModel(id: organId, name: organId, bodyView: BodyView.front, position: OrganPosition(x: 0, y: 0)))
|
||||
.name;
|
||||
|
||||
_organSymptomsGroups[organId] = OrganSymptomsGroup(
|
||||
organId: organId,
|
||||
organName: organName,
|
||||
symptoms: symptoms,
|
||||
);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Toggle symptom selection for a specific organ
|
||||
void toggleSymptomSelection(String organId, String symptomId) {
|
||||
if (_organSymptomsGroups.containsKey(organId)) {
|
||||
final group = _organSymptomsGroups[organId]!;
|
||||
final selectedIds = Set<String>.from(group.selectedSymptomIds);
|
||||
|
||||
if (selectedIds.contains(symptomId)) {
|
||||
selectedIds.remove(symptomId);
|
||||
} else {
|
||||
selectedIds.add(symptomId);
|
||||
}
|
||||
|
||||
_organSymptomsGroups[organId] = group.copyWith(selectedSymptomIds: selectedIds);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a symptom is selected
|
||||
bool isSymptomSelected(String organId, String symptomId) {
|
||||
return _organSymptomsGroups[organId]?.selectedSymptomIds.contains(symptomId) ?? false;
|
||||
}
|
||||
|
||||
/// Get all selected symptoms across all organs
|
||||
List<SymptomModel> getAllSelectedSymptoms() {
|
||||
List<SymptomModel> allSymptoms = [];
|
||||
for (var group in _organSymptomsGroups.values) {
|
||||
for (var symptom in group.symptoms) {
|
||||
if (group.selectedSymptomIds.contains(symptom.id)) {
|
||||
allSymptoms.add(symptom);
|
||||
}
|
||||
}
|
||||
}
|
||||
return allSymptoms;
|
||||
}
|
||||
|
||||
/// Clear all symptom selections
|
||||
void clearAllSymptomSelections() {
|
||||
for (var organId in _organSymptomsGroups.keys) {
|
||||
_organSymptomsGroups[organId] = _organSymptomsGroups[organId]!.copyWith(selectedSymptomIds: {});
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Reset the view model to initial state
|
||||
void reset() {
|
||||
_currentView = BodyView.front;
|
||||
_selectedOrganIds.clear();
|
||||
_organSymptomsGroups.clear();
|
||||
_isBottomSheetExpanded = false;
|
||||
_tooltipTimer?.cancel();
|
||||
_tooltipOrganId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tooltipTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue