* Push Notification Setup and attempt

* Office Ordering
* Allow editing of Task and Ticket Details after creation
This commit is contained in:
2026-02-24 21:06:46 +08:00
parent cc6fda0e79
commit 5979a04254
25 changed files with 1130 additions and 91 deletions
+132 -8
View File
@@ -14,6 +14,7 @@ import '../../providers/profile_provider.dart';
import '../../providers/tasks_provider.dart';
import '../../providers/tickets_provider.dart';
import '../../providers/typing_provider.dart';
import '../../utils/snackbar.dart';
import '../../widgets/app_breakpoints.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart';
@@ -95,14 +96,40 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
final detailsContent = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Align(
alignment: Alignment.center,
child: Text(
ticket.subject,
textAlign: TextAlign.center,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
ticket.subject,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(width: 8),
Builder(
builder: (ctx) {
final profile = currentProfileAsync.maybeWhen(
data: (p) => p,
orElse: () => null,
);
final canEdit =
profile != null &&
(profile.role == 'admin' ||
profile.role == 'dispatcher' ||
profile.role == 'it_staff' ||
profile.id == ticket.creatorId);
if (!canEdit) return const SizedBox.shrink();
return IconButton(
tooltip: 'Edit ticket',
onPressed: () =>
_showEditTicketDialog(ctx, ref, ticket),
icon: const Icon(Icons.edit),
);
},
),
],
),
),
const SizedBox(height: 6),
@@ -853,6 +880,103 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
);
}
Future<void> _showEditTicketDialog(
BuildContext context,
WidgetRef ref,
Ticket ticket,
) async {
final officesAsync = ref.watch(officesOnceProvider);
final subjectCtrl = TextEditingController(text: ticket.subject);
final descCtrl = TextEditingController(text: ticket.description);
String? selectedOffice = ticket.officeId;
await showDialog<void>(
context: context,
builder: (dialogContext) {
return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Edit Ticket'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: subjectCtrl,
decoration: const InputDecoration(labelText: 'Subject'),
),
const SizedBox(height: 8),
TextField(
controller: descCtrl,
decoration: const InputDecoration(labelText: 'Description'),
maxLines: 4,
),
const SizedBox(height: 8),
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,
);
},
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
final outerContext = context;
final subject = subjectCtrl.text.trim();
final desc = descCtrl.text.trim();
try {
await ref
.read(ticketsControllerProvider)
.updateTicket(
ticketId: ticket.id,
subject: subject.isEmpty ? null : subject,
description: desc.isEmpty ? null : desc,
officeId: selectedOffice,
);
if (!mounted) return;
Navigator.of(outerContext).pop();
showSuccessSnackBar(outerContext, 'Ticket updated');
} catch (e) {
if (!mounted) return;
showErrorSnackBar(
outerContext,
'Failed to update ticket: $e',
);
}
},
child: const Text('Save'),
),
],
);
},
);
}
Widget _timelineRow(String label, DateTime? value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),