post_enrollment_app/lib/features/chatbot/application/chatbot_controller.dart
2026-06-11 09:25:55 +05:30

460 lines
14 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:nhance_app_pwa/features/chatbot/data/chatbot_api_service.dart';
import 'package:nhance_app_pwa/features/chatbot/data/chatbot_repository.dart';
import 'package:nhance_app_pwa/features/chatbot/domain/models/chatbot_models.dart';
import 'package:nhance_app_pwa/pages/helpers/ecard_download_notification_service.dart';
import 'package:nhance_app_pwa/pages/helpers/ecard_download_service.dart';
import 'package:nhance_app_pwa/pages/service/TokenService.dart';
import 'package:url_launcher/url_launcher.dart';
final chatbotRepositoryProvider = Provider<ChatbotRepository>((ref) {
return ChatbotRepository(ChatbotApiService());
});
final chatbotControllerProvider =
StateNotifierProvider<ChatbotController, ChatbotUiState>((ref) {
return ChatbotController(ref.read(chatbotRepositoryProvider));
});
class ChatbotUiState {
final bool isOpen;
final bool isIntroAnimating;
final bool isLoading;
final String? errorMessage;
final ChatbotFlowStep step;
final List<ChatbotMessage> messages;
final List<ChatbotOption> options;
final List<ChatbotPolicy> policies;
final ChatbotPolicy? selectedPolicy;
final List<ClaimStatusItem> claimStatuses;
const ChatbotUiState({
required this.isOpen,
required this.isIntroAnimating,
required this.isLoading,
required this.errorMessage,
required this.step,
required this.messages,
required this.options,
required this.policies,
required this.selectedPolicy,
required this.claimStatuses,
});
factory ChatbotUiState.initial() {
return ChatbotUiState(
isOpen: false,
isIntroAnimating: false,
isLoading: false,
errorMessage: null,
step: ChatbotFlowStep.rootMenu,
messages: [
ChatbotMessage(
text: 'Welcome! Im here to assist you with your policy services. Please choose an option to continue.',
isBot: true,
createdAt: DateTime.now(),
),
],
options: const [
ChatbotOption(id: 'my_policies', label: 'My Policies'),
ChatbotOption(id: 'claims_procedure', label: 'Claims Procedure'),
ChatbotOption(id: 'my_claims_status', label: 'My Claims Status'),
ChatbotOption(id: 'help', label: 'Help'),
],
policies: const [],
selectedPolicy: null,
claimStatuses: const [],
);
}
ChatbotUiState copyWith({
bool? isOpen,
bool? isIntroAnimating,
bool? isLoading,
String? errorMessage,
bool clearError = false,
ChatbotFlowStep? step,
List<ChatbotMessage>? messages,
List<ChatbotOption>? options,
List<ChatbotPolicy>? policies,
ChatbotPolicy? selectedPolicy,
bool clearSelectedPolicy = false,
List<ClaimStatusItem>? claimStatuses,
}) {
return ChatbotUiState(
isOpen: isOpen ?? this.isOpen,
isIntroAnimating: isIntroAnimating ?? this.isIntroAnimating,
isLoading: isLoading ?? this.isLoading,
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
step: step ?? this.step,
messages: messages ?? this.messages,
options: options ?? this.options,
policies: policies ?? this.policies,
selectedPolicy:
clearSelectedPolicy ? null : (selectedPolicy ?? this.selectedPolicy),
claimStatuses: claimStatuses ?? this.claimStatuses,
);
}
}
class ChatbotController extends StateNotifier<ChatbotUiState> {
final ChatbotRepository _repository;
final EcardDownloadService _ecardDownloadService = const EcardDownloadService();
List<ChatbotPolicy>? _policiesCache;
DateTime? _policiesCacheAt;
static const Duration _cacheTtl = Duration(minutes: 10);
bool _didHomeIntroAnimation = false;
ChatbotController(this._repository) : super(ChatbotUiState.initial());
void toggleOpen() {
state = state.copyWith(isOpen: !state.isOpen);
}
void close() {
state = state.copyWith(isOpen: false);
}
void resetAll() {
_policiesCache = null;
_policiesCacheAt = null;
_didHomeIntroAnimation = false;
state = ChatbotUiState.initial();
}
Future<void> triggerHomeIntroAnimation() async {
if (_didHomeIntroAnimation) return;
_didHomeIntroAnimation = true;
state = state.copyWith(isIntroAnimating: true);
await Future.delayed(const Duration(milliseconds: 900));
state = state.copyWith(isIntroAnimating: false);
}
List<ClaimStatusItem> _latestThreeClaims(List<ClaimStatusItem> claims) {
if (claims.length <= 3) return List<ClaimStatusItem>.from(claims.reversed);
return claims.reversed.take(3).toList();
}
void _appendUserMessage(String text) {
final updated = List<ChatbotMessage>.from(state.messages)
..add(
ChatbotMessage(
text: text,
isBot: false,
createdAt: DateTime.now(),
),
);
state = state.copyWith(messages: updated);
}
void _appendBotMessage(String text) {
final updated = List<ChatbotMessage>.from(state.messages)
..add(
ChatbotMessage(
text: text,
isBot: true,
createdAt: DateTime.now(),
),
);
state = state.copyWith(messages: updated);
}
void _showRootMenu() {
state = state.copyWith(
step: ChatbotFlowStep.rootMenu,
options: const [
ChatbotOption(id: 'my_policies', label: 'My Policies'),
ChatbotOption(id: 'claims_procedure', label: 'Claims Procedure'),
ChatbotOption(id: 'my_claims_status', label: 'My Claims Status'),
ChatbotOption(id: 'help', label: 'Help'),
],
clearSelectedPolicy: true,
clearError: true,
);
}
Future<void> onOptionSelected(
BuildContext context,
ChatbotOption option,
) async {
_appendUserMessage(option.label);
switch (option.id) {
case 'my_policies':
await loadPolicies(context);
return;
case 'claims_procedure':
_appendBotMessage('Opening claims page.');
close();
context.go('/claimprocess', extra: 2);
return;
case 'my_claims_status':
await loadClaimsStatus(context);
return;
case 'help':
_appendBotMessage('Opening help page.');
close();
context.go('/help');
return;
case 'covered_member_list':
showCoveredMembers();
return;
case 'ecard_download':
await openEcard(context);
return;
case 'covered_hospital_list':
await openHospitalList();
return;
case 'back_to_root':
_appendBotMessage('Back to main menu.');
_showRootMenu();
return;
case 'back_to_policies':
_appendBotMessage('Choose a policy.');
state = state.copyWith(
step: ChatbotFlowStep.policyList,
options: _policyOptions(state.policies),
clearSelectedPolicy: true,
clearError: true,
);
return;
default:
if (option.id.startsWith('policy::')) {
final policyId = option.id.replaceFirst('policy::', '');
final selected = state.policies.firstWhere(
(policy) => policy.clientPolicyId == policyId,
orElse: () => state.policies.first,
);
selectPolicy(selected);
}
return;
}
}
List<ChatbotOption> _policyOptions(List<ChatbotPolicy> policies) {
final policyOptions = policies
.map((policy) => ChatbotOption(
id: 'policy::${policy.clientPolicyId}',
label: policy.policyName,
))
.toList();
return [
...policyOptions,
const ChatbotOption(id: 'back_to_root', label: 'Back'),
];
}
Future<void> loadPolicies(BuildContext context) async {
final now = DateTime.now();
final canUseCache = _policiesCache != null &&
_policiesCacheAt != null &&
now.difference(_policiesCacheAt!) < _cacheTtl;
if (canUseCache) {
final cachedPolicies = _policiesCache!;
if (cachedPolicies.isEmpty) {
_appendBotMessage('No active policies found.');
_showRootMenu();
} else {
_appendBotMessage('Choose a policy.');
state = state.copyWith(
isLoading: false,
policies: cachedPolicies,
step: ChatbotFlowStep.policyList,
options: _policyOptions(cachedPolicies),
clearError: true,
);
}
return;
}
state = state.copyWith(isLoading: true, clearError: true);
try {
final policies = await _repository.getPolicies(context);
_policiesCache = policies;
_policiesCacheAt = DateTime.now();
if (policies.isEmpty) {
_appendBotMessage('No active policies found.');
_showRootMenu();
} else {
_appendBotMessage('Choose a policy.');
state = state.copyWith(
isLoading: false,
policies: policies,
step: ChatbotFlowStep.policyList,
options: _policyOptions(policies),
);
}
} catch (_) {
state = state.copyWith(
isLoading: false,
errorMessage: 'Unable to load policies. Please try again.',
);
_appendBotMessage('Unable to load policies right now.');
_showRootMenu();
}
}
void selectPolicy(ChatbotPolicy policy) {
_appendBotMessage('Selected ${policy.policyName}.');
final isGmcPolicy = _isGmcPolicy(policy);
final actionOptions = <ChatbotOption>[
const ChatbotOption(id: 'covered_member_list', label: 'Covered Member List'),
if (isGmcPolicy)
const ChatbotOption(id: 'ecard_download', label: 'E-Card Download'),
if (isGmcPolicy)
const ChatbotOption(
id: 'covered_hospital_list',
label: 'Covered Hospital List',
),
const ChatbotOption(id: 'back_to_policies', label: 'Back to Policies'),
];
state = state.copyWith(
selectedPolicy: policy,
step: ChatbotFlowStep.policyActions,
options: actionOptions,
clearError: true,
);
}
bool _isGmcPolicy(ChatbotPolicy policy) {
final normalizedType = policy.policyType.trim().toUpperCase();
return normalizedType.startsWith('GMC');
}
void showCoveredMembers() {
final policy = state.selectedPolicy;
if (policy == null) {
_appendBotMessage('Please choose a policy first.');
return;
}
if (policy.members.isEmpty) {
_appendBotMessage('No covered members found for this policy.');
} else {
final names = policy.members
.map((member) => '${member.name} (${member.relationship})')
.join('\n');
_appendBotMessage('Covered members:\n$names');
}
}
Future<void> openEcard(BuildContext context) async {
final policy = state.selectedPolicy;
if (policy == null) {
_appendBotMessage('Please choose a policy first.');
return;
}
final url = policy.eCardDownloadUrl.trim();
if (url.isEmpty) {
_appendBotMessage('E-Card is not available for this policy.');
return;
}
final uri = Uri.tryParse(url);
if (uri == null) {
_appendBotMessage('E-Card link is invalid.');
return;
}
final fileName = _chatbotEcardFileName(policy);
final postToken = await TokenService.getPostToken();
final headers = <String, String>{
'APP-SIGNATURE': 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y',
if (postToken != null && postToken.isNotEmpty)
'Authorization': 'Bearer $postToken',
};
if (!kIsWeb) {
_appendBotMessage('Downloading eCard...');
}
final result = await _ecardDownloadService.downloadEcard(
url: uri.toString(),
fileName: fileName,
headers: headers,
);
if (!result.success) {
_appendBotMessage(result.message ?? 'Unable to download E-Card.');
return;
}
if (!kIsWeb && result.savedPath != null && result.savedPath!.isNotEmpty) {
await EcardDownloadNotificationService.showDownloadCompleted(
filePath: result.savedPath!,
fileName: fileName,
);
}
_appendBotMessage(result.message ?? 'E-Card downloaded successfully.');
}
String _chatbotEcardFileName(ChatbotPolicy policy) {
final sanitized = policy.clientPolicyId.replaceAll(RegExp(r'[^\w\-.]'), '_');
if (sanitized.isEmpty) return 'ecard.pdf';
return 'ecard_$sanitized.pdf';
}
Future<void> openHospitalList() async {
final policy = state.selectedPolicy;
if (policy == null) {
_appendBotMessage('Please choose a policy first.');
return;
}
final url = policy.networkHospitalsUrl.trim();
if (url.isEmpty) {
_appendBotMessage('Hospital list link is not available.');
return;
}
final uri = Uri.tryParse(url);
if (uri == null) {
_appendBotMessage('Hospital list link is invalid.');
return;
}
final launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!launched) {
_appendBotMessage('Unable to open hospital list link.');
return;
}
_appendBotMessage('Opened hospital list in browser.');
}
Future<void> loadClaimsStatus(BuildContext context) async {
state = state.copyWith(isLoading: true, clearError: true);
try {
final claims = await _repository.getClaimStatuses(context);
final latestClaims = _latestThreeClaims(claims);
if (claims.isEmpty) {
_appendBotMessage('No claims found.');
} else {
_appendBotMessage('Here is your latest claims status.');
}
state = state.copyWith(
isLoading: false,
step: ChatbotFlowStep.claimStatusList,
claimStatuses: latestClaims,
options: const [ChatbotOption(id: 'back_to_root', label: 'Back')],
);
} catch (_) {
state = state.copyWith(
isLoading: false,
errorMessage: 'Unable to load claims status. Please try again.',
);
_appendBotMessage('Unable to load claims status right now.');
_showRootMenu();
}
}
}