Fix queue loading, null-safe models, autofill, and add outbound dialer
All checks were successful
Create Release / build (push) Successful in 4s
All checks were successful
Create Release / build (push) Successful in 4s
- Fix queue queries in mobile API and SSE to use twp_group_members (matching browser phone) instead of twp_queue_assignments - Auto-create personal queues if user has no extension - Make all model JSON parsing null-safe (handle null, string ints, bools) - Add AutofillGroup and autofill hints to login form - Add outbound calling with dialpad bottom sheet on dashboard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
import '../providers/agent_provider.dart';
|
||||
import '../providers/call_provider.dart';
|
||||
import '../widgets/agent_status_toggle.dart';
|
||||
import '../widgets/dialpad.dart';
|
||||
import '../widgets/queue_card.dart';
|
||||
import 'active_call_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
@@ -23,6 +24,90 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void _showDialer(BuildContext context) {
|
||||
final numberController = TextEditingController();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (ctx) {
|
||||
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),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
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.isNotEmpty) {
|
||||
context.read<CallProvider>().makeCall(number);
|
||||
Navigator.pop(ctx);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final agent = context.watch<AgentProvider>();
|
||||
@@ -58,6 +143,10 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showDialer(context),
|
||||
child: const Icon(Icons.phone),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () => agent.refresh(),
|
||||
child: ListView(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
@@ -39,6 +40,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
serverUrl = 'https://$serverUrl';
|
||||
}
|
||||
|
||||
TextInput.finishAutofillContext();
|
||||
context.read<AuthProvider>().login(
|
||||
serverUrl,
|
||||
_usernameController.text.trim(),
|
||||
@@ -55,7 +57,8 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
child: AutofillGroup(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -80,6 +83,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
autofillHints: const [AutofillHints.url],
|
||||
validator: (v) =>
|
||||
v == null || v.trim().isEmpty ? 'Required' : null,
|
||||
),
|
||||
@@ -91,6 +95,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
autofillHints: const [AutofillHints.username],
|
||||
validator: (v) =>
|
||||
v == null || v.trim().isEmpty ? 'Required' : null,
|
||||
),
|
||||
@@ -110,6 +115,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
),
|
||||
),
|
||||
obscureText: _obscurePassword,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
validator: (v) =>
|
||||
v == null || v.isEmpty ? 'Required' : null,
|
||||
),
|
||||
@@ -142,6 +148,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user