Implemented subject suggestions in Task Creation and Edit.
Fix editing redirects
This commit is contained in:
parent
b1f5d209a2
commit
01c430c812
|
|
@ -27,6 +27,7 @@ import '../../providers/realtime_controller.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import '../../utils/app_time.dart';
|
import '../../utils/app_time.dart';
|
||||||
import '../../utils/snackbar.dart';
|
import '../../utils/snackbar.dart';
|
||||||
|
import '../../utils/subject_suggestions.dart';
|
||||||
import '../../widgets/app_breakpoints.dart';
|
import '../../widgets/app_breakpoints.dart';
|
||||||
import '../../widgets/mono_text.dart';
|
import '../../widgets/mono_text.dart';
|
||||||
import '../../widgets/responsive_body.dart';
|
import '../../widgets/responsive_body.dart';
|
||||||
|
|
@ -3382,36 +3383,72 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
Task task,
|
Task task,
|
||||||
) async {
|
) async {
|
||||||
|
final dialogShape = AppSurfaces.of(context).dialogShape;
|
||||||
// offices will be watched inside the dialog's Consumer so the dialog
|
// offices will be watched inside the dialog's Consumer so the dialog
|
||||||
// can rebuild independently when the provider completes.
|
// can rebuild independently when the provider completes.
|
||||||
final titleCtrl = TextEditingController(text: task.title);
|
final titleCtrl = TextEditingController(text: task.title);
|
||||||
final descCtrl = TextEditingController(text: task.description);
|
final descCtrl = TextEditingController(text: task.description);
|
||||||
|
final existingSubjects = <String>[
|
||||||
|
...((ref.read(tasksProvider).valueOrNull ?? const <Task>[]).map(
|
||||||
|
(task) => task.title,
|
||||||
|
)),
|
||||||
|
...((ref.read(ticketsProvider).valueOrNull ?? const <Ticket>[]).map(
|
||||||
|
(ticket) => ticket.subject,
|
||||||
|
)),
|
||||||
|
];
|
||||||
String? selectedOffice = task.officeId;
|
String? selectedOffice = task.officeId;
|
||||||
|
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) {
|
builder: (dialogContext) {
|
||||||
|
var saving = false;
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: AppSurfaces.of(context).dialogShape,
|
shape: dialogShape,
|
||||||
title: const Text('Edit Task'),
|
title: const Text('Edit Task'),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
TypeAheadFormField<String>(
|
||||||
|
textFieldConfiguration: TextFieldConfiguration(
|
||||||
controller: titleCtrl,
|
controller: titleCtrl,
|
||||||
|
enabled: !saving,
|
||||||
decoration: const InputDecoration(labelText: 'Title'),
|
decoration: const InputDecoration(labelText: 'Title'),
|
||||||
),
|
),
|
||||||
|
suggestionsCallback: (pattern) async {
|
||||||
|
return SubjectSuggestionEngine.suggest(
|
||||||
|
existingSubjects: existingSubjects,
|
||||||
|
query: pattern,
|
||||||
|
limit: 8,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, suggestion) =>
|
||||||
|
ListTile(dense: true, title: Text(suggestion)),
|
||||||
|
onSuggestionSelected: (suggestion) {
|
||||||
|
titleCtrl
|
||||||
|
..text = suggestion
|
||||||
|
..selection = TextSelection.collapsed(
|
||||||
|
offset: suggestion.length,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextField(
|
TextField(
|
||||||
controller: descCtrl,
|
controller: descCtrl,
|
||||||
decoration: const InputDecoration(labelText: 'Description'),
|
enabled: !saving,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Description',
|
||||||
|
),
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (dialogContext, dialogRef, _) {
|
builder: (dialogContext, dialogRef, _) {
|
||||||
final officesAsync = dialogRef.watch(officesOnceProvider);
|
final officesAsync = dialogRef.watch(
|
||||||
|
officesOnceProvider,
|
||||||
|
);
|
||||||
return officesAsync.when(
|
return officesAsync.when(
|
||||||
data: (offices) {
|
data: (offices) {
|
||||||
final officesSorted = List<Office>.from(offices)
|
final officesSorted = List<Office>.from(offices)
|
||||||
|
|
@ -3436,11 +3473,13 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
child: Text(o.name),
|
child: Text(o.name),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onChanged: (v) => selectedOffice = v,
|
onChanged: saving
|
||||||
|
? null
|
||||||
|
: (v) => setDialogState(
|
||||||
|
() => selectedOffice = v,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
// Show a linear progress indicator while offices are being
|
|
||||||
// retrieved so the user sees that the dialog is loading
|
|
||||||
loading: () => const Padding(
|
loading: () => const Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 12),
|
padding: EdgeInsets.symmetric(vertical: 12),
|
||||||
child: LinearProgressIndicator(),
|
child: LinearProgressIndicator(),
|
||||||
|
|
@ -3454,14 +3493,21 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
onPressed: saving
|
||||||
|
? null
|
||||||
|
: () => Navigator.of(dialogContext).pop(),
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: saving
|
||||||
final outerContext = context;
|
? null
|
||||||
final title = titleCtrl.text.trim();
|
: () async {
|
||||||
|
final title =
|
||||||
|
SubjectSuggestionEngine.normalizeDisplay(
|
||||||
|
titleCtrl.text.trim(),
|
||||||
|
);
|
||||||
final desc = descCtrl.text.trim();
|
final desc = descCtrl.text.trim();
|
||||||
|
setDialogState(() => saving = true);
|
||||||
try {
|
try {
|
||||||
await ref
|
await ref
|
||||||
.read(tasksControllerProvider)
|
.read(tasksControllerProvider)
|
||||||
|
|
@ -3471,20 +3517,37 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
description: desc.isEmpty ? null : desc,
|
description: desc.isEmpty ? null : desc,
|
||||||
officeId: selectedOffice,
|
officeId: selectedOffice,
|
||||||
);
|
);
|
||||||
|
ref.invalidate(tasksProvider);
|
||||||
|
ref.invalidate(taskByIdProvider(task.id));
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(outerContext).pop();
|
Navigator.of(dialogContext).pop();
|
||||||
showSuccessSnackBar(outerContext, 'Task updated');
|
showSuccessSnackBar(context, 'Task updated');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
showErrorSnackBar(outerContext, 'Failed to update task: $e');
|
showErrorSnackBar(
|
||||||
|
context,
|
||||||
|
'Failed to update task: $e',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (dialogContext.mounted) {
|
||||||
|
setDialogState(() => saving = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('Save'),
|
child: saving
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Save'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _canAssignStaff(String role) {
|
bool _canAssignStaff(String role) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user