post_enrollment_app/lib/features/chatbot/presentation/widgets/chatbot_window.dart
2026-04-08 09:11:24 +05:30

290 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:nhance_app_pwa/customAppBar/responsive.dart';
import 'package:nhance_app_pwa/features/chatbot/application/chatbot_controller.dart';
import 'package:nhance_app_pwa/features/chatbot/domain/models/chatbot_models.dart';
import 'package:nhance_app_pwa/features/chatbot/presentation/widgets/chatbot_message_card.dart';
import 'package:nhance_app_pwa/features/chatbot/presentation/widgets/chatbot_option_button.dart';
import 'package:nhance_app_pwa/features/chatbot/presentation/widgets/chatbot_typing_indicator.dart';
class ChatbotWindow extends ConsumerStatefulWidget {
const ChatbotWindow({super.key});
@override
ConsumerState<ChatbotWindow> createState() => _ChatbotWindowState();
}
class _ChatbotWindowState extends ConsumerState<ChatbotWindow> {
final ScrollController _messagesScrollController = ScrollController();
final Map<String, Color> statusColors = {
'Claim Received': Colors.blueGrey,
'Under Process': Colors.orangeAccent,
'Information Required': Colors.deepOrange,
'Approved': Colors.green,
'Settled': Colors.teal,
'Denial Review Awaited': Colors.redAccent,
'Rejected': Colors.red,
};
@override
void dispose() {
_messagesScrollController.dispose();
super.dispose();
}
void _scrollToLatestMessage() {
if (!_messagesScrollController.hasClients) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_messagesScrollController.hasClients) return;
_messagesScrollController.animateTo(
_messagesScrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 220),
curve: Curves.easeOut,
);
});
}
Color _statusColor(String status) {
return statusColors[status.trim()] ?? const Color(0xFF6B7280);
}
Widget _buildClaimStatusCard(ClaimStatusItem item) {
final badgeColor = _statusColor(item.status);
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 4, right: 10, top: 2),
child: ClipOval(
child: Image(
image: AssetImage('assets/chatbot_icon.png'),
width: 30,
height: 30,
fit: BoxFit.cover,
),
),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFFFFCE5),
borderRadius: BorderRadius.circular(14),
boxShadow: const [
BoxShadow(
color: Color(0x22000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
item.claimNumber,
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 18,
color: Color(0xFF111827),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 5),
decoration: BoxDecoration(
color: badgeColor,
borderRadius: BorderRadius.circular(16),
),
child: Text(
item.status,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Text(
item.amount.isNotEmpty
? 'Amount: ${item.amount}'
: '-',
textAlign: TextAlign.start,
style: const TextStyle(
color: Color(0xFF374151),
fontSize: 14,
),
),
),
const SizedBox(width: 8),
Text(
item.date,
textAlign: TextAlign.right,
style: const TextStyle(
color: Color(0xFF6B7280),
fontSize: 13,
),
),
],
),
],
),
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final state = ref.watch(chatbotControllerProvider);
final controller = ref.read(chatbotControllerProvider.notifier);
_scrollToLatestMessage();
final isMobile = Responsive.isMobile(context);
final isTablet = Responsive.isTablet(context);
final topPadding = MediaQuery.of(context).padding.top;
final bottomPadding = MediaQuery.of(context).padding.bottom;
final window = Material(
elevation: isMobile ? 0 : 14,
color: const Color(0xFFF3F7FB),
borderRadius: BorderRadius.circular(isMobile ? 0 : 14),
child: SizedBox(
width: isMobile
? MediaQuery.of(context).size.width
: isTablet
? 420
: 420,
height: isMobile
? MediaQuery.of(context).size.height
: 560,
child: Column(
children: [
Container(
decoration: BoxDecoration(
color: Color(0xFF00999E),
borderRadius: BorderRadius.vertical(
top: Radius.circular(isMobile ? 0 : 14),
),
),
padding: EdgeInsets.fromLTRB(14, isMobile ? topPadding + 8 : 12, 14, 12),
child: Row(
children: [
Expanded(
child: Row(
children: [
ClipOval(
child: Container(
width: 28,
height: 28,
color: Colors.white,
alignment: Alignment.center,
child: Image.asset(
'assets/chatbot_icon.png',
width: 24,
height: 24,
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 8),
const Text(
'Nhance Care',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
],
),
),
InkWell(
onTap: controller.close,
child: const Icon(Icons.close, color: Colors.white),
),
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
child: ListView(
controller: _messagesScrollController,
children: [
...state.messages.map(
(item) => ChatbotMessageCard(
text: item.text,
isBot: item.isBot,
createdAt: item.createdAt,
),
),
if (state.step == ChatbotFlowStep.claimStatusList &&
state.claimStatuses.isNotEmpty)
...state.claimStatuses.map(_buildClaimStatusCard),
if (state.isLoading) const ChatbotTypingIndicator(),
],
),
),
),
Padding(
padding: EdgeInsets.fromLTRB(12, 8, 12, 12 + (isMobile ? bottomPadding : 0)),
child: Column(
children: state.options
.map(
(option) => ChatbotOptionButton(
label: option.label,
onTap: state.isLoading
? () {}
: () => controller.onOptionSelected(context, option),
),
)
.toList(),
),
),
],
),
),
);
if (isMobile) {
return AnimatedSlide(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOut,
offset: state.isOpen ? Offset.zero : const Offset(0, 1),
child: window,
);
}
return AnimatedScale(
duration: const Duration(milliseconds: 180),
scale: state.isOpen ? 1 : 0.9,
alignment: Alignment.bottomRight,
child: window,
);
}
}