Announcements and IT Job Checklist
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user