Announcements and IT Job Checklist

This commit is contained in:
2026-03-21 18:51:04 +08:00
parent 7d9096963a
commit 3fb6fd5c93
19 changed files with 2367 additions and 37 deletions
+103 -8
View File
@@ -33,6 +33,7 @@ import '../../widgets/app_state_view.dart';
import '../../utils/subject_suggestions.dart';
import '../../widgets/gemini_button.dart';
import '../../widgets/gemini_animated_text_field.dart';
import 'it_job_checklist_tab.dart';
// request metadata options used in task creation/editing dialogs
const List<String> _requestTypeOptions = [
@@ -57,14 +58,16 @@ class TasksListScreen extends ConsumerStatefulWidget {
}
class _TasksListScreenState extends ConsumerState<TasksListScreen>
with SingleTickerProviderStateMixin {
with TickerProviderStateMixin {
final TextEditingController _subjectController = TextEditingController();
final TextEditingController _taskNumberController = TextEditingController();
String? _selectedOfficeId;
String? _selectedStatus;
String? _selectedAssigneeId;
DateTimeRange? _selectedDateRange;
late final TabController _tabController;
late TabController _tabController;
bool _showItJobTab = false;
bool _tabInited = false;
@override
void dispose() {
@@ -78,13 +81,31 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
// Rebuild when tab changes so filter header can show/hide the
// "Assigned staff" dropdown for the All Tasks tab.
_tabController.addListener(() {
if (mounted) setState(() {});
});
}
void _ensureTabController(bool shouldShowItJobTab) {
if (_tabInited && _showItJobTab == shouldShowItJobTab) return;
final oldIndex = _tabController.index;
_tabController.removeListener(_onTabChange);
_tabController.dispose();
_tabController = TabController(
length: shouldShowItJobTab ? 3 : 2,
vsync: this,
initialIndex: oldIndex.clamp(0, shouldShowItJobTab ? 2 : 1),
);
_tabController.addListener(_onTabChange);
_tabInited = true;
_showItJobTab = shouldShowItJobTab;
setState(() {});
}
void _onTabChange() {
if (mounted) setState(() {});
}
bool get _hasTaskFilters {
return _subjectController.text.trim().isNotEmpty ||
_taskNumberController.text.trim().isNotEmpty ||
@@ -129,6 +150,13 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
profile.role == 'it_staff'),
orElse: () => false,
);
final role = profileAsync.valueOrNull?.role ?? '';
final shouldShowItJobTab = role == 'admin' || role == 'dispatcher';
if (!_tabInited || _showItJobTab != shouldShowItJobTab) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _ensureTabController(shouldShowItJobTab);
});
}
final ticketById = <String, Ticket>{
for (final ticket in ticketsAsync.valueOrNull ?? <Ticket>[])
@@ -521,9 +549,11 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
children: [
TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'My Tasks'),
Tab(text: 'All Tasks'),
tabs: [
const Tab(text: 'My Tasks'),
const Tab(text: 'All Tasks'),
if (_showItJobTab)
const Tab(text: 'IT Job Checklist'),
],
),
Expanded(
@@ -552,6 +582,8 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
: 'Tasks created for your team will appear here.',
)
: makeList(filteredTasks),
if (_showItJobTab)
const ItJobChecklistTab(),
],
),
),
@@ -564,7 +596,20 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
),
),
),
if (canCreate)
if (_showItJobTab && _tabController.index == 2)
Positioned(
right: 16,
bottom: 16,
child: SafeArea(
child: M3ExpandedFab(
heroTag: 'notify_all_fab',
onPressed: () => _notifyAllPending(ref),
icon: const Icon(Icons.notifications_active),
label: const Text('Notify All Pending'),
),
),
)
else if (canCreate)
Positioned(
right: 16,
bottom: 16,
@@ -581,6 +626,56 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
);
}
Future<void> _notifyAllPending(WidgetRef ref) async {
final tasks = ref.read(tasksProvider).valueOrNull ?? [];
final assignments = ref.read(taskAssignmentsProvider).valueOrNull ?? [];
// All completed tasks with IT Job not yet submitted
final pending = tasks
.where((t) => t.status == 'completed' && !t.itJobPrinted)
.toList();
if (pending.isEmpty) {
if (mounted) showSuccessSnackBar(context, 'No pending IT Jobs to notify');
return;
}
// One notification per unique assignee (not per task)
final userIds = <String>{};
for (final task in pending) {
for (final a in assignments.where((a) => a.taskId == task.id)) {
userIds.add(a.userId);
}
}
if (userIds.isEmpty) {
if (mounted) showErrorSnackBar(context, 'No assigned staff found');
return;
}
try {
final currentUserId = ref.read(currentUserIdProvider);
// Single generic notification per user — no task_id to avoid flooding
await ref.read(notificationsControllerProvider).createNotification(
userIds: userIds.toList(),
type: 'it_job_reminder',
actorId: currentUserId ?? '',
pushTitle: 'IT Job Reminder',
pushBody:
'You have pending IT Job submissions. Please submit your printed copies to the dispatcher.',
pushData: {'navigate_to': '/tasks'},
);
if (mounted) {
showSuccessSnackBar(
context,
'Reminder sent to ${userIds.length} staff member(s)',
);
}
} catch (e) {
if (mounted) showErrorSnackBar(context, 'Failed to send: $e');
}
}
Future<void> _showCreateTaskDialog(
BuildContext context,
WidgetRef ref,