import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:provider/provider.dart'; import 'package:twilio_voice/twilio_voice.dart'; import '../models/queue_state.dart'; import '../providers/agent_provider.dart'; import '../providers/auth_provider.dart'; import '../providers/call_provider.dart'; import '../widgets/agent_status_toggle.dart'; import '../widgets/dialpad.dart'; import '../widgets/queue_card.dart'; import 'settings_screen.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @override State createState() => _DashboardScreenState(); } class _DashboardScreenState extends State { bool _phoneAccountEnabled = true; // assume true until checked @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().refresh(); _checkPhoneAccount(); }); } Future _checkPhoneAccount() async { if (!kIsWeb && Platform.isAndroid) { final enabled = await TwilioVoice.instance.isPhoneAccountEnabled(); if (mounted && !enabled) { setState(() => _phoneAccountEnabled = false); _showPhoneAccountDialog(); } else if (mounted) { setState(() => _phoneAccountEnabled = true); } } } void _showPhoneAccountDialog() { showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( title: const Text('Enable Phone Account'), content: const Text( 'TWP Softphone needs to be enabled as a calling account to make and receive calls.\n\n' 'Tap "Open Settings" below, then find "TWP Softphone" in the list and toggle it ON.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Later'), ), FilledButton( onPressed: () async { Navigator.pop(ctx); await TwilioVoice.instance.openPhoneAccountSettings(); // Poll until enabled or user comes back for (int i = 0; i < 30; i++) { await Future.delayed(const Duration(seconds: 1)); if (!mounted) return; final enabled = await TwilioVoice.instance.isPhoneAccountEnabled(); if (enabled) { setState(() => _phoneAccountEnabled = true); return; } } // Re-check one more time when coming back _checkPhoneAccount(); }, child: const Text('Open Settings'), ), ], ), ); } void _showDialer(BuildContext context) { final numberController = TextEditingController(); final phoneNumbers = context.read().phoneNumbers; // Auto-select first phone number as caller ID String? selectedCallerId = phoneNumbers.isNotEmpty ? phoneNumbers.first.phoneNumber : null; showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (ctx) { return StatefulBuilder( builder: (ctx, setSheetState) { return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(ctx).viewInsets.bottom, top: 16, left: 16, right: 16, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Number display TextField( controller: numberController, keyboardType: TextInputType.phone, autofillHints: const [AutofillHints.telephoneNumber], textAlign: TextAlign.center, style: Theme.of(ctx).textTheme.headlineSmall, decoration: InputDecoration( hintText: 'Enter phone number', suffixIcon: IconButton( icon: const Icon(Icons.backspace_outlined), onPressed: () { final text = numberController.text; if (text.isNotEmpty) { numberController.text = text.substring(0, text.length - 1); numberController.selection = TextSelection.fromPosition( TextPosition(offset: numberController.text.length), ); } }, ), ), ), // Caller ID selector (only if multiple numbers) if (phoneNumbers.length > 1) ...[ const SizedBox(height: 12), DropdownButtonFormField( initialValue: selectedCallerId, decoration: const InputDecoration( labelText: 'Caller ID', isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), items: phoneNumbers.map((p) => DropdownMenuItem( value: p.phoneNumber, child: Text('${p.friendlyName} (${p.phoneNumber})'), )).toList(), onChanged: (value) { setSheetState(() { selectedCallerId = value; }); }, ), ] else if (phoneNumbers.length == 1) ...[ const SizedBox(height: 8), Text( 'Caller ID: ${phoneNumbers.first.phoneNumber}', style: Theme.of(ctx).textTheme.bodySmall?.copyWith( color: Theme.of(ctx).colorScheme.onSurfaceVariant, ), ), ], const SizedBox(height: 16), // Dialpad Dialpad( onDigit: (digit) { numberController.text += digit; numberController.selection = TextSelection.fromPosition( TextPosition(offset: numberController.text.length), ); }, onClose: () => Navigator.pop(ctx), ), const SizedBox(height: 8), // Call button ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), ), icon: const Icon(Icons.call), label: const Text('Call'), onPressed: () { final number = numberController.text.trim(); if (number.isEmpty) return; if (selectedCallerId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No caller ID available. Add a phone number first.')), ); return; } context.read().makeCall(number, callerId: selectedCallerId); Navigator.pop(ctx); }, ), const SizedBox(height: 16), ], ), ); }, ); }, ); } void _showQueueCalls(BuildContext context, QueueInfo queue) { final voiceService = context.read().voiceService; final callProvider = context.read(); showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (ctx) { return FutureBuilder>>( future: voiceService.getQueueCalls(queue.id), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Padding( padding: EdgeInsets.all(32), child: Center(child: CircularProgressIndicator()), ); } if (snapshot.hasError) { return Padding( padding: const EdgeInsets.all(24), child: Center( child: Text('Error loading calls: ${snapshot.error}'), ), ); } final calls = (snapshot.data ?? []) .map((c) => QueueCall.fromJson(c)) .toList(); if (calls.isEmpty) { return const Padding( padding: EdgeInsets.all(24), child: Center(child: Text('No calls waiting')), ); } return Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( '${queue.name} - Waiting Calls', style: Theme.of(context).textTheme.titleMedium, ), ), const SizedBox(height: 8), ...calls.map((call) => ListTile( leading: const CircleAvatar( child: Icon(Icons.phone_in_talk), ), title: Text(call.fromNumber), subtitle: Text('Waiting ${_formatWaitTime(call.waitTime)}'), trailing: FilledButton.icon( icon: const Icon(Icons.call, size: 18), label: const Text('Accept'), onPressed: () { Navigator.pop(ctx); callProvider.acceptQueueCall(call.callSid); // Cancel queue alert notification FlutterLocalNotificationsPlugin().cancel(9001); }, ), )), ], ), ); }, ); }, ); } String _formatWaitTime(int seconds) { if (seconds < 60) return '${seconds}s'; final minutes = seconds ~/ 60; final secs = seconds % 60; return '${minutes}m ${secs}s'; } @override Widget build(BuildContext context) { final agent = context.watch(); // Android Telecom framework handles the call UI via the native InCallUI, // so we don't navigate to our own ActiveCallScreen. return Scaffold( appBar: AppBar( title: const Text('TWP Softphone'), actions: [ // SSE connection indicator Padding( padding: const EdgeInsets.only(right: 8), child: Icon( Icons.circle, size: 12, color: agent.sseConnected ? Colors.green : Colors.red, ), ), IconButton( icon: const Icon(Icons.settings), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () => _showDialer(context), child: const Icon(Icons.phone), ), body: RefreshIndicator( onRefresh: () => agent.refresh(), child: ListView( padding: const EdgeInsets.all(16), children: [ if (!_phoneAccountEnabled) Card( color: Colors.orange.shade50, child: ListTile( leading: Icon(Icons.warning, color: Colors.orange.shade700), title: const Text('Phone Account Not Enabled'), subtitle: const Text('Tap to enable calling in settings'), trailing: const Icon(Icons.chevron_right), onTap: () => _showPhoneAccountDialog(), ), ), if (!_phoneAccountEnabled) const SizedBox(height: 8), const AgentStatusToggle(), const SizedBox(height: 24), Text('Queues', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), if (agent.queues.isEmpty) const Card( child: Padding( padding: EdgeInsets.all(24), child: Center(child: Text('No queues assigned')), ), ) else ...agent.queues.map((q) => Padding( padding: const EdgeInsets.only(bottom: 8), child: QueueCard( queue: q, onTap: q.waitingCount > 0 ? () => _showQueueCalls(context, q) : null, ), )), ], ), ), ); } }