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 '../../utils/app_time.dart';
|
||||
import '../../utils/snackbar.dart';
|
||||
import '../../utils/subject_suggestions.dart';
|
||||
import '../../widgets/app_breakpoints.dart';
|
||||
import '../../widgets/mono_text.dart';
|
||||
import '../../widgets/responsive_body.dart';
|
||||
|
|
@ -3382,106 +3383,168 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
|||
WidgetRef ref,
|
||||
Task task,
|
||||
) async {
|
||||
final dialogShape = AppSurfaces.of(context).dialogShape;
|
||||
// offices will be watched inside the dialog's Consumer so the dialog
|
||||
// can rebuild independently when the provider completes.
|
||||
final titleCtrl = TextEditingController(text: task.title);
|
||||
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;
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
shape: AppSurfaces.of(context).dialogShape,
|
||||
title: const Text('Edit Task'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: titleCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Title'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: descCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Description'),
|
||||
maxLines: 4,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Consumer(
|
||||
builder: (dialogContext, dialogRef, _) {
|
||||
final officesAsync = dialogRef.watch(officesOnceProvider);
|
||||
return officesAsync.when(
|
||||
data: (offices) {
|
||||
final officesSorted = List<Office>.from(offices)
|
||||
..sort(
|
||||
(a, b) => a.name.toLowerCase().compareTo(
|
||||
b.name.toLowerCase(),
|
||||
),
|
||||
);
|
||||
return DropdownButtonFormField<String?>(
|
||||
initialValue: selectedOffice,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Office',
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem(
|
||||
value: null,
|
||||
child: Text('Unassigned'),
|
||||
),
|
||||
for (final o in officesSorted)
|
||||
DropdownMenuItem(
|
||||
value: o.id,
|
||||
child: Text(o.name),
|
||||
),
|
||||
],
|
||||
onChanged: (v) => selectedOffice = v,
|
||||
var saving = false;
|
||||
return StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
return AlertDialog(
|
||||
shape: dialogShape,
|
||||
title: const Text('Edit Task'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TypeAheadFormField<String>(
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
controller: titleCtrl,
|
||||
enabled: !saving,
|
||||
decoration: const InputDecoration(labelText: 'Title'),
|
||||
),
|
||||
suggestionsCallback: (pattern) async {
|
||||
return SubjectSuggestionEngine.suggest(
|
||||
existingSubjects: existingSubjects,
|
||||
query: pattern,
|
||||
limit: 8,
|
||||
);
|
||||
},
|
||||
// Show a linear progress indicator while offices are being
|
||||
// retrieved so the user sees that the dialog is loading
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: LinearProgressIndicator(),
|
||||
itemBuilder: (context, suggestion) =>
|
||||
ListTile(dense: true, title: Text(suggestion)),
|
||||
onSuggestionSelected: (suggestion) {
|
||||
titleCtrl
|
||||
..text = suggestion
|
||||
..selection = TextSelection.collapsed(
|
||||
offset: suggestion.length,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: descCtrl,
|
||||
enabled: !saving,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description',
|
||||
),
|
||||
error: (error, _) => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
maxLines: 4,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Consumer(
|
||||
builder: (dialogContext, dialogRef, _) {
|
||||
final officesAsync = dialogRef.watch(
|
||||
officesOnceProvider,
|
||||
);
|
||||
return officesAsync.when(
|
||||
data: (offices) {
|
||||
final officesSorted = List<Office>.from(offices)
|
||||
..sort(
|
||||
(a, b) => a.name.toLowerCase().compareTo(
|
||||
b.name.toLowerCase(),
|
||||
),
|
||||
);
|
||||
return DropdownButtonFormField<String?>(
|
||||
initialValue: selectedOffice,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Office',
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem(
|
||||
value: null,
|
||||
child: Text('Unassigned'),
|
||||
),
|
||||
for (final o in officesSorted)
|
||||
DropdownMenuItem(
|
||||
value: o.id,
|
||||
child: Text(o.name),
|
||||
),
|
||||
],
|
||||
onChanged: saving
|
||||
? null
|
||||
: (v) => setDialogState(
|
||||
() => selectedOffice = v,
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
error: (error, _) => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: saving
|
||||
? null
|
||||
: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: saving
|
||||
? null
|
||||
: () async {
|
||||
final title =
|
||||
SubjectSuggestionEngine.normalizeDisplay(
|
||||
titleCtrl.text.trim(),
|
||||
);
|
||||
final desc = descCtrl.text.trim();
|
||||
setDialogState(() => saving = true);
|
||||
try {
|
||||
await ref
|
||||
.read(tasksControllerProvider)
|
||||
.updateTaskFields(
|
||||
taskId: task.id,
|
||||
title: title.isEmpty ? null : title,
|
||||
description: desc.isEmpty ? null : desc,
|
||||
officeId: selectedOffice,
|
||||
);
|
||||
ref.invalidate(tasksProvider);
|
||||
ref.invalidate(taskByIdProvider(task.id));
|
||||
if (!mounted) return;
|
||||
Navigator.of(dialogContext).pop();
|
||||
showSuccessSnackBar(context, 'Task updated');
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
showErrorSnackBar(
|
||||
context,
|
||||
'Failed to update task: $e',
|
||||
);
|
||||
} finally {
|
||||
if (dialogContext.mounted) {
|
||||
setDialogState(() => saving = false);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: saving
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final outerContext = context;
|
||||
final title = titleCtrl.text.trim();
|
||||
final desc = descCtrl.text.trim();
|
||||
try {
|
||||
await ref
|
||||
.read(tasksControllerProvider)
|
||||
.updateTaskFields(
|
||||
taskId: task.id,
|
||||
title: title.isEmpty ? null : title,
|
||||
description: desc.isEmpty ? null : desc,
|
||||
officeId: selectedOffice,
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.of(outerContext).pop();
|
||||
showSuccessSnackBar(outerContext, 'Task updated');
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
showErrorSnackBar(outerContext, 'Failed to update task: $e');
|
||||
}
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user