Implemented subject suggestions in Task Creation and Edit.

Fix editing redirects
This commit is contained in:
Marc Rejohn Castillano 2026-03-03 18:51:58 +08:00
parent b1f5d209a2
commit 01c430c812

View File

@ -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,106 +3383,168 @@ 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) {
return AlertDialog( var saving = false;
shape: AppSurfaces.of(context).dialogShape, return StatefulBuilder(
title: const Text('Edit Task'), builder: (context, setDialogState) {
content: SingleChildScrollView( return AlertDialog(
child: Column( shape: dialogShape,
mainAxisSize: MainAxisSize.min, title: const Text('Edit Task'),
children: [ content: SingleChildScrollView(
TextField( child: Column(
controller: titleCtrl, mainAxisSize: MainAxisSize.min,
decoration: const InputDecoration(labelText: 'Title'), children: [
), TypeAheadFormField<String>(
const SizedBox(height: 8), textFieldConfiguration: TextFieldConfiguration(
TextField( controller: titleCtrl,
controller: descCtrl, enabled: !saving,
decoration: const InputDecoration(labelText: 'Description'), decoration: const InputDecoration(labelText: 'Title'),
maxLines: 4, ),
), suggestionsCallback: (pattern) async {
const SizedBox(height: 8), return SubjectSuggestionEngine.suggest(
Consumer( existingSubjects: existingSubjects,
builder: (dialogContext, dialogRef, _) { query: pattern,
final officesAsync = dialogRef.watch(officesOnceProvider); limit: 8,
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,
); );
}, },
// Show a linear progress indicator while offices are being itemBuilder: (context, suggestion) =>
// retrieved so the user sees that the dialog is loading ListTile(dense: true, title: Text(suggestion)),
loading: () => const Padding( onSuggestionSelected: (suggestion) {
padding: EdgeInsets.symmetric(vertical: 12), titleCtrl
child: LinearProgressIndicator(), ..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'),
),
],
); );
}, },
); );