UI Enhancements in IT Service Request, Announcements, Workforce and notification fixes

This commit is contained in:
2026-03-22 18:00:10 +08:00
parent 049ab2c794
commit 872c2aab87
15 changed files with 1290 additions and 183 deletions
@@ -1,9 +1,9 @@
import 'dart:async';
import 'dart:math' as math show min;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:skeletonizer/skeletonizer.dart';
import '../../models/it_service_request.dart';
import '../../models/it_service_request_assignment.dart';
@@ -13,6 +13,7 @@ import '../../providers/it_service_request_provider.dart';
import '../../providers/profile_provider.dart';
import '../../providers/realtime_controller.dart';
import '../../providers/tickets_provider.dart';
import '../../theme/m3_motion.dart';
import '../../utils/app_time.dart';
import '../../utils/snackbar.dart';
import '../../widgets/m3_card.dart';
@@ -100,27 +101,46 @@ class _ItServiceRequestsListScreenState
children: [
ResponsiveBody(
maxWidth: double.infinity,
child: Skeletonizer(
enabled: showSkeleton,
child: Builder(
builder: (context) {
if (requestsAsync.hasError && !requestsAsync.hasValue) {
return AppErrorView(
error: requestsAsync.error!,
onRetry: () =>
ref.invalidate(itServiceRequestsProvider),
);
}
final allRequests =
requestsAsync.valueOrNull ?? <ItServiceRequest>[];
if (allRequests.isEmpty && !showSkeleton) {
return const AppEmptyView(
icon: Icons.miscellaneous_services_outlined,
title: 'No service requests yet',
subtitle:
'IT service requests submitted by your team will appear here.',
);
}
child: Builder(
builder: (context) {
if (requestsAsync.hasError && !requestsAsync.hasValue) {
return AppErrorView(
error: requestsAsync.error!,
onRetry: () =>
ref.invalidate(itServiceRequestsProvider),
);
}
if (showSkeleton) {
return Column(
children: [
const AppPageHeader(
title: 'IT Service Requests',
subtitle: 'Manage and track IT support tickets',
),
Expanded(
child: ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
itemCount: 5,
separatorBuilder: (_, _) =>
const SizedBox(height: 8),
itemBuilder: (_, _) =>
_buildIsrShimmerCard(),
),
),
],
);
}
final allRequests =
requestsAsync.valueOrNull ?? <ItServiceRequest>[];
if (allRequests.isEmpty) {
return const AppEmptyView(
icon: Icons.miscellaneous_services_outlined,
title: 'No service requests yet',
subtitle:
'IT service requests submitted by your team will appear here.',
);
}
final offices = officesAsync.valueOrNull ?? <Office>[];
final officesSorted = List<Office>.from(offices)
..sort(
@@ -278,13 +298,12 @@ class _ItServiceRequestsListScreenState
},
),
),
),
// FAB
if (canCreate)
Positioned(
right: 16,
bottom: 16,
child: FloatingActionButton.extended(
child: M3ExpandedFab(
heroTag: 'create_isr',
onPressed: () => _showCreateDialog(context),
icon: const Icon(Icons.add),
@@ -416,6 +435,38 @@ class _ItServiceRequestsListScreenState
if (context.mounted) showErrorSnackBar(context, 'Error: $e');
}
}
Widget _buildIsrShimmerCard() {
return M3Card.elevated(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
M3ShimmerBox(width: 80, height: 13),
const Spacer(),
M3ShimmerBox(width: 72, height: 22, borderRadius: BorderRadius.circular(11)),
],
),
const SizedBox(height: 10),
M3ShimmerBox(height: 16),
const SizedBox(height: 6),
M3ShimmerBox(width: 200, height: 14),
const SizedBox(height: 10),
Row(
children: [
M3ShimmerBox(width: 120, height: 12),
const SizedBox(width: 16),
M3ShimmerBox(width: 80, height: 12),
],
),
],
),
),
);
}
}
// ---------------------------------------------------------------------------
@@ -561,9 +612,10 @@ class _RequestList extends StatelessWidget {
if (requests.isEmpty) {
return const Center(child: Text('No requests match the current filter.'));
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: requests.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final request = requests[index];
final assignedStaff = assignments
@@ -573,10 +625,13 @@ class _RequestList extends StatelessWidget {
final office = request.officeId != null
? officeById[request.officeId]?.name
: null;
return _RequestTile(
request: request,
officeName: office,
assignedStaff: assignedStaff,
return M3FadeSlideIn(
delay: Duration(milliseconds: math.min(index, 8) * 50),
child: _RequestTile(
request: request,
officeName: office,
assignedStaff: assignedStaff,
),
);
},
);