Offline Support

This commit is contained in:
2026-04-27 06:20:39 +08:00
parent 7d8851a94a
commit 48cd3bcd60
121 changed files with 14798 additions and 2214 deletions
@@ -1335,7 +1335,7 @@ class _ItServiceRequestDetailScreenState
_finishSave(success: true);
} catch (e) {
debugPrint('Save field error: $e');
_finishSave(success: false);
_finishSave(success: isOfflineSaveError(e));
}
}
@@ -1350,7 +1350,7 @@ class _ItServiceRequestDetailScreenState
_finishSave(success: true);
} catch (e) {
debugPrint('Save datetime error: $e');
_finishSave(success: false);
_finishSave(success: isOfflineSaveError(e));
}
}
@@ -1485,7 +1485,13 @@ class _ItServiceRequestDetailScreenState
if (mounted) showSuccessSnackBar(context, 'Text improved successfully');
} catch (e) {
if (mounted) showErrorSnackBar(context, 'Error: $e');
if (mounted) {
if (isOfflineSaveError(e)) {
showSuccessSnackBar(context, 'Saved offline — will sync when connected.');
} else {
showErrorSnackBar(context, 'Error: $e');
}
}
} finally {
setProcessing(false);
}
@@ -1525,7 +1531,13 @@ class _ItServiceRequestDetailScreenState
if (mounted) showSuccessSnackBar(context, 'Request approved');
} catch (e) {
if (mounted) showErrorSnackBar(context, 'Error: $e');
if (mounted) {
if (isOfflineSaveError(e)) {
showSuccessSnackBar(context, 'Approval saved offline — will sync when connected.');
} else {
showErrorSnackBar(context, 'Error: $e');
}
}
}
}
@@ -1630,7 +1642,13 @@ class _ItServiceRequestDetailScreenState
);
}
} catch (e) {
if (mounted) showErrorSnackBar(context, 'Error: $e');
if (mounted) {
if (isOfflineSaveError(e)) {
showSuccessSnackBar(context, 'Status change saved offline — will sync when connected.');
} else {
showErrorSnackBar(context, 'Error: $e');
}
}
}
}
@@ -9,6 +9,7 @@ import '../../models/it_service_request.dart';
import '../../models/it_service_request_assignment.dart';
import '../../models/office.dart';
import '../../models/profile.dart';
import '../../brick/cache_helpers.dart';
import '../../providers/it_service_request_provider.dart';
import '../../providers/profile_provider.dart';
import '../../providers/realtime_controller.dart';
@@ -23,6 +24,7 @@ import '../../widgets/app_page_header.dart';
import '../../widgets/app_state_view.dart';
import '../../widgets/responsive_body.dart';
import '../../widgets/status_pill.dart';
import '../../widgets/sync_pending_badge.dart';
class ItServiceRequestsListScreen extends ConsumerStatefulWidget {
const ItServiceRequestsListScreen({super.key});
@@ -67,6 +69,26 @@ class _ItServiceRequestsListScreenState
@override
Widget build(BuildContext context) {
final requestsAsync = ref.watch(itServiceRequestsProvider);
final offlinePending = ref.watch(offlinePendingItServiceRequestsProvider);
final pendingUpdates =
ref.watch(offlinePendingItServiceRequestUpdatesProvider);
final isrPendingIds = {
...offlinePending.map((r) => r.id),
...pendingUpdates.keys,
};
ref.listen<AsyncValue<List<ItServiceRequest>>>(itServiceRequestsProvider, (_, next) {
final live = next.valueOrNull;
if (live == null || live.isEmpty) return;
final pending = ref.read(offlinePendingItServiceRequestsProvider);
if (pending.isEmpty) return;
final liveIds = live.map((r) => r.id).toSet();
final stillPending = pending.where((r) => !liveIds.contains(r.id)).toList();
if (stillPending.length != pending.length) {
ref.read(offlinePendingItServiceRequestsProvider.notifier).state = stillPending;
}
});
final profileAsync = ref.watch(currentProfileProvider);
final profilesAsync = ref.watch(profilesProvider);
final officesAsync = ref.watch(officesProvider);
@@ -131,8 +153,12 @@ class _ItServiceRequestsListScreenState
],
);
}
final allRequests =
final liveRequests =
requestsAsync.valueOrNull ?? <ItServiceRequest>[];
final liveIds = liveRequests.map((r) => r.id).toSet();
final pendingOnly =
offlinePending.where((r) => !liveIds.contains(r.id)).toList();
final allRequests = [...pendingOnly, ...liveRequests];
if (allRequests.isEmpty) {
return const AppEmptyView(
icon: Icons.miscellaneous_services_outlined,
@@ -277,12 +303,14 @@ class _ItServiceRequestsListScreenState
officeById: officeById,
profileById: profileById,
assignments: assignments,
pendingIds: isrPendingIds,
),
_RequestList(
requests: _applyFilters(allRequests),
officeById: officeById,
profileById: profileById,
assignments: assignments,
pendingIds: isrPendingIds,
),
],
)
@@ -291,6 +319,7 @@ class _ItServiceRequestsListScreenState
officeById: officeById,
profileById: profileById,
assignments: assignments,
pendingIds: isrPendingIds,
),
),
],
@@ -427,12 +456,31 @@ class _ItServiceRequestsListScreenState
requestedByUserId: profile?.id,
status: (profile?.role == 'standard') ? 'pending_approval' : 'draft',
);
if (context.mounted) {
final isOffline = data['pending'] == true;
if (isOffline) {
final localId = data['id'] as String;
final allCached = await cachedListFromBrick<ItServiceRequest>();
final local = allCached.where((r) => r.id == localId).firstOrNull;
if (local != null && context.mounted) {
final current = ref.read(offlinePendingItServiceRequestsProvider);
ref.read(offlinePendingItServiceRequestsProvider.notifier).state = [
local,
...current,
];
}
if (context.mounted) {
showSuccessSnackBar(
context,
'Request saved offline — will sync when connected.',
);
}
} else if (context.mounted) {
showSuccessSnackBar(context, 'Request created');
context.go('/it-service-requests/${data['id']}');
}
} catch (e) {
if (context.mounted) showErrorSnackBar(context, 'Error: $e');
if (!context.mounted) return;
showErrorSnackBar(context, 'Error: $e');
}
}
@@ -600,12 +648,14 @@ class _RequestList extends StatelessWidget {
required this.officeById,
required this.profileById,
required this.assignments,
required this.pendingIds,
});
final List<ItServiceRequest> requests;
final Map<String, Office> officeById;
final Map<String, Profile> profileById;
final List<ItServiceRequestAssignment> assignments;
final Set<String> pendingIds;
@override
Widget build(BuildContext context) {
@@ -631,6 +681,7 @@ class _RequestList extends StatelessWidget {
request: request,
officeName: office,
assignedStaff: assignedStaff,
isPending: pendingIds.contains(request.id),
),
);
},
@@ -643,11 +694,13 @@ class _RequestTile extends StatelessWidget {
required this.request,
this.officeName,
required this.assignedStaff,
this.isPending = false,
});
final ItServiceRequest request;
final String? officeName;
final List<String> assignedStaff;
final bool isPending;
@override
Widget build(BuildContext context) {
@@ -669,11 +722,22 @@ class _RequestTile extends StatelessWidget {
],
),
const SizedBox(height: 8),
Text(
request.eventName,
style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600),
maxLines: 2,
overflow: TextOverflow.ellipsis,
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
request.eventName,
style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (isPending) ...[
const SizedBox(width: 8),
SyncPendingBadge(isPending: true, compact: true),
],
],
),
const SizedBox(height: 4),
if (request.services.isNotEmpty)