iOS PWA and IT Job Checklist for IT Staff view
This commit is contained in:
@@ -16,12 +16,20 @@ import '../../widgets/m3_card.dart';
|
||||
import '../../widgets/mono_text.dart';
|
||||
import '../../widgets/profile_avatar.dart';
|
||||
|
||||
/// IT Job Checklist tab — visible only to admin/dispatcher.
|
||||
/// IT Job Checklist tab — visible to admin/dispatcher and IT staff.
|
||||
///
|
||||
/// Shows all completed tasks with a checkbox for tracking printed IT Job
|
||||
/// submission status, plus a per-task notification button with 60s cooldown.
|
||||
/// Admin/dispatcher view: shows all completed tasks with a checkbox for
|
||||
/// tracking printed IT Job submission status, plus a per-task notification
|
||||
/// button with 60s cooldown.
|
||||
///
|
||||
/// IT staff view: shows only tasks assigned to the current user, read-only,
|
||||
/// so they can track which IT Jobs still need a physical copy submitted.
|
||||
class ItJobChecklistTab extends ConsumerStatefulWidget {
|
||||
const ItJobChecklistTab({super.key});
|
||||
const ItJobChecklistTab({super.key, this.isAdminView = true});
|
||||
|
||||
/// When true, shows admin controls (checkbox, bell, stats, team filter).
|
||||
/// When false (IT staff), shows only the current user's tasks, read-only.
|
||||
final bool isAdminView;
|
||||
|
||||
@override
|
||||
ConsumerState<ItJobChecklistTab> createState() => _ItJobChecklistTabState();
|
||||
@@ -53,10 +61,21 @@ class _ItJobChecklistTabState extends ConsumerState<ItJobChecklistTab> {
|
||||
final allTasks = tasksAsync.valueOrNull ?? [];
|
||||
final allAssignments = assignmentsAsync.valueOrNull ?? [];
|
||||
final showSkeleton = !tasksAsync.hasValue && !tasksAsync.hasError;
|
||||
final currentUserId = widget.isAdminView
|
||||
? null
|
||||
: ref.watch(currentUserIdProvider);
|
||||
|
||||
// All completed tasks
|
||||
var filtered = allTasks.where((t) => t.status == 'completed').toList();
|
||||
|
||||
// IT staff: restrict to tasks assigned to the current user
|
||||
if (!widget.isAdminView && currentUserId != null) {
|
||||
filtered = filtered.where((t) {
|
||||
return allAssignments
|
||||
.any((a) => a.taskId == t.id && a.userId == currentUserId);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Search by task # or subject
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
final q = _searchQuery.toLowerCase();
|
||||
@@ -66,8 +85,8 @@ class _ItJobChecklistTabState extends ConsumerState<ItJobChecklistTab> {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Filter by team
|
||||
if (_selectedTeamId != null) {
|
||||
// Filter by team (admin/dispatcher only)
|
||||
if (widget.isAdminView && _selectedTeamId != null) {
|
||||
final memberIds = teamMembers
|
||||
.where((m) => m.teamId == _selectedTeamId)
|
||||
.map((m) => m.userId)
|
||||
@@ -93,7 +112,7 @@ class _ItJobChecklistTabState extends ConsumerState<ItJobChecklistTab> {
|
||||
return ta.compareTo(tb);
|
||||
});
|
||||
|
||||
// Stats (always computed over all completed, regardless of filter)
|
||||
// Stats (admin/dispatcher only — computed over all completed tasks)
|
||||
final allCompleted = allTasks.where((t) => t.status == 'completed');
|
||||
final submitted = allCompleted.where((t) => t.itJobPrinted).length;
|
||||
final total = allCompleted.length;
|
||||
@@ -104,7 +123,8 @@ class _ItJobChecklistTabState extends ConsumerState<ItJobChecklistTab> {
|
||||
// the list itself (see below inside Expanded).
|
||||
return Column(
|
||||
children: [
|
||||
// Stats card
|
||||
// Stats card — admin/dispatcher only
|
||||
if (widget.isAdminView)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 12, 0, 4),
|
||||
child: M3Card.filled(
|
||||
@@ -183,34 +203,35 @@ class _ItJobChecklistTabState extends ConsumerState<ItJobChecklistTab> {
|
||||
setState(() => _searchQuery = v.trim()),
|
||||
),
|
||||
),
|
||||
// Team filter
|
||||
SizedBox(
|
||||
width: 180,
|
||||
child: DropdownButtonFormField<String>(
|
||||
key: ValueKey(_selectedTeamId),
|
||||
initialValue: _selectedTeamId,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Team',
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
isExpanded: true,
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text('All Teams'),
|
||||
// Team filter — admin/dispatcher only
|
||||
if (widget.isAdminView)
|
||||
SizedBox(
|
||||
width: 180,
|
||||
child: DropdownButtonFormField<String>(
|
||||
key: ValueKey(_selectedTeamId),
|
||||
initialValue: _selectedTeamId,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Team',
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
...teams.map((t) => DropdownMenuItem<String>(
|
||||
value: t.id,
|
||||
child: Text(t.name,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
)),
|
||||
],
|
||||
onChanged: (v) => setState(() => _selectedTeamId = v),
|
||||
isExpanded: true,
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text('All Teams'),
|
||||
),
|
||||
...teams.map((t) => DropdownMenuItem<String>(
|
||||
value: t.id,
|
||||
child: Text(t.name,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
)),
|
||||
],
|
||||
onChanged: (v) => setState(() => _selectedTeamId = v),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Status filter
|
||||
SizedBox(
|
||||
width: 180,
|
||||
@@ -238,7 +259,7 @@ class _ItJobChecklistTabState extends ConsumerState<ItJobChecklistTab> {
|
||||
),
|
||||
),
|
||||
// Clear button
|
||||
if (_selectedTeamId != null ||
|
||||
if ((widget.isAdminView && _selectedTeamId != null) ||
|
||||
_statusFilter != 'not_submitted' ||
|
||||
_searchQuery.isNotEmpty)
|
||||
TextButton.icon(
|
||||
@@ -300,6 +321,7 @@ class _ItJobChecklistTabState extends ConsumerState<ItJobChecklistTab> {
|
||||
task: task,
|
||||
assignees: assignees,
|
||||
profiles: profiles,
|
||||
isAdminView: widget.isAdminView,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -338,11 +360,13 @@ class _ItJobTile extends ConsumerStatefulWidget {
|
||||
required this.task,
|
||||
required this.assignees,
|
||||
required this.profiles,
|
||||
this.isAdminView = true,
|
||||
});
|
||||
|
||||
final Task task;
|
||||
final List<TaskAssignment> assignees;
|
||||
final List profiles;
|
||||
final bool isAdminView;
|
||||
|
||||
@override
|
||||
ConsumerState<_ItJobTile> createState() => _ItJobTileState();
|
||||
@@ -539,55 +563,58 @@ class _ItJobTileState extends ConsumerState<_ItJobTile> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Compact checkbox — shrinkWrap removes the default 48 px touch
|
||||
// target padding so it doesn't push the Row wider on mobile.
|
||||
Checkbox(
|
||||
value: _isChecked,
|
||||
onChanged: _toggleChecked,
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
// Bell / cooldown button
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: _inCooldown
|
||||
? Tooltip(
|
||||
message: '$_secondsRemaining s',
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(
|
||||
value: _secondsRemaining / _cooldownSeconds,
|
||||
strokeWidth: 2.5,
|
||||
color: cs.primary,
|
||||
backgroundColor:
|
||||
cs.primary.withValues(alpha: 0.15),
|
||||
// Compact checkbox + bell — admin/dispatcher only
|
||||
if (widget.isAdminView) ...[
|
||||
// Compact checkbox — shrinkWrap removes the default 48 px touch
|
||||
// target padding so it doesn't push the Row wider on mobile.
|
||||
Checkbox(
|
||||
value: _isChecked,
|
||||
onChanged: _toggleChecked,
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
// Bell / cooldown button
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: _inCooldown
|
||||
? Tooltip(
|
||||
message: '$_secondsRemaining s',
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(
|
||||
value: _secondsRemaining / _cooldownSeconds,
|
||||
strokeWidth: 2.5,
|
||||
color: cs.primary,
|
||||
backgroundColor:
|
||||
cs.primary.withValues(alpha: 0.15),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$_secondsRemaining',
|
||||
style: tt.labelSmall?.copyWith(
|
||||
color: cs.primary,
|
||||
Text(
|
||||
'$_secondsRemaining',
|
||||
style: tt.labelSmall?.copyWith(
|
||||
color: cs.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
)
|
||||
: IconButton(
|
||||
tooltip: 'Remind assigned staff',
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _sendReminder,
|
||||
icon: Icon(
|
||||
Icons.notifications_active_outlined,
|
||||
color: cs.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
)
|
||||
: IconButton(
|
||||
tooltip: 'Remind assigned staff',
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _sendReminder,
|
||||
icon: Icon(
|
||||
Icons.notifications_active_outlined,
|
||||
color: cs.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user