feat: integrate snackbar notifications for document and job updates in user document controller and job detail screen

This commit is contained in:
Vaibhav Surve 2025-11-19 17:29:10 +05:30
parent 92c739045c
commit ec6a45ed43
2 changed files with 272 additions and 248 deletions

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/document/document_filter_model.dart';
import 'package:marco/model/document/documents_list_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class DocumentController extends GetxController {
// ==================== Observables ====================
@ -38,7 +39,6 @@ class DocumentController extends GetxController {
final endDate = Rxn<DateTime>();
// ==================== Lifecycle ====================
@override
void onClose() {
// Don't dispose searchController here - it's managed by the page
@ -87,13 +87,23 @@ class DocumentController extends GetxController {
entityId: entityId,
reset: true,
);
// Show success snackbar
showAppSnackbar(
title: 'Success',
message: isActive ? 'Document deactivated' : 'Document activated',
type: SnackbarType.success,
);
return true;
} else {
errorMessage.value = 'Failed to update document state';
_showError('Failed to update document state');
return false;
}
} catch (e) {
errorMessage.value = 'Error updating document: $e';
_showError('Error updating document: $e');
debugPrint('❌ Error toggling document state: $e');
return false;
} finally {
@ -110,17 +120,13 @@ class DocumentController extends GetxController {
bool reset = false,
}) async {
try {
// Reset pagination if needed
if (reset) {
pageNumber.value = 1;
documents.clear();
hasMore.value = true;
}
// Don't fetch if no more data
if (!hasMore.value && !reset) return;
// Prevent duplicate requests
if (isLoading.value) return;
isLoading.value = true;
@ -147,12 +153,24 @@ class DocumentController extends GetxController {
errorMessage.value = response?.message ?? 'Failed to fetch documents';
if (documents.isEmpty) {
_showError('Failed to load documents');
} else {
showAppSnackbar(
title: 'Warning',
message: 'No more documents to load',
type: SnackbarType.warning,
);
}
}
} catch (e) {
errorMessage.value = 'Error fetching documents: $e';
if (documents.isEmpty) {
_showError('Error loading documents');
} else {
showAppSnackbar(
title: 'Error',
message: 'Error fetching additional documents',
type: SnackbarType.error,
);
}
debugPrint('❌ Error fetching documents: $e');
} finally {
@ -185,17 +203,12 @@ class DocumentController extends GetxController {
isVerified.value != null;
}
/// Show error message
/// Show error message via snackbar
void _showError(String message) {
Get.snackbar(
'Error',
message,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.shade100,
colorText: Colors.red.shade900,
margin: const EdgeInsets.all(16),
borderRadius: 8,
duration: const Duration(seconds: 3),
showAppSnackbar(
title: 'Error',
message: message,
type: SnackbarType.error,
);
}

View File

@ -17,6 +17,7 @@ import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class JobDetailsScreen extends StatefulWidget {
final String jobId;
@ -128,7 +129,6 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
{"op": "replace", "path": "/assignees", "value": assigneesPayload});
final originalTags = job.tags;
final replaceTagsPayload = originalTags.map((t) {
final isSelected = _selectedTags.any((s) => s.id == t.id);
return {"id": t.id, "name": t.name, "isActive": isSelected};
@ -149,7 +149,10 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
}
if (operations.isEmpty) {
Get.snackbar("Info", "No changes detected to save.");
showAppSnackbar(
title: "Info",
message: "No changes detected to save.",
type: SnackbarType.info);
return;
}
@ -159,11 +162,17 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
);
if (success) {
Get.snackbar("Success", "Job updated successfully");
showAppSnackbar(
title: "Success",
message: "Job updated successfully",
type: SnackbarType.success);
await controller.fetchJobDetail(widget.jobId);
isEditing.value = false;
} else {
Get.snackbar("Error", "Failed to update job. Check inputs or try again.");
showAppSnackbar(
title: "Error",
message: "Failed to update job. Check inputs or try again.",
type: SnackbarType.error);
}
}
@ -174,12 +183,10 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
final action = job.nextTaggingAction;
File? attachmentFile;
// Step 1: Show comment bottom sheet
final comment = await showCommentBottomSheet(
context, action == 0 ? "Tag In" : "Tag Out");
if (comment == null) return; // User cancelled
if (comment == null) return;
// Step 2: Ask for optional image
await showDialog(
context: context,
builder: (_) => ConfirmDialog(
@ -199,7 +206,6 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
),
);
// Step 3: Call attendance update
await controller.updateJobAttendance(
jobId: job.id,
action: action == 0 ? 0 : 1,
@ -207,15 +213,14 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
attachment: attachmentFile,
);
// Step 4: Check message to detect failure
if (controller.attendanceMessage.value.toLowerCase().contains("failed") ||
controller.attendanceMessage.value.toLowerCase().contains("error")) {
Get.snackbar("Error", controller.attendanceMessage.value);
return; // Do NOT close bottom sheet
final msg = controller.attendanceMessage.value;
if (msg.toLowerCase().contains("failed") ||
msg.toLowerCase().contains("error")) {
showAppSnackbar(title: "Error", message: msg, type: SnackbarType.error);
return;
}
// Success
Get.snackbar("Success", controller.attendanceMessage.value);
showAppSnackbar(title: "Success", message: msg, type: SnackbarType.success);
}
Widget _buildSectionCard({
@ -488,10 +493,11 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
color: Colors.grey[600],
),
onPressed: () async {
isAttendanceExpanded.value = !isAttendanceExpanded.value;
isAttendanceExpanded.value =
!isAttendanceExpanded.value;
if (isAttendanceExpanded.value && job != null) {
await controller.fetchJobAttendanceLog(
job.attendanceId ?? '');
await controller
.fetchJobAttendanceLog(job.attendanceId ?? '');
}
},
))
@ -528,7 +534,8 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
backgroundColor: action == 0 ? Colors.green : Colors.red,
backgroundColor:
action == 0 ? Colors.green : Colors.red,
),
),
),
@ -565,11 +572,11 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
final log = logs[index];
final employeeName =
"${log.employee.firstName} ${log.employee.lastName}";
final date =
DateTimeUtils.convertUtcToLocal(log.markedAt.toIso8601String(),
final date = DateTimeUtils.convertUtcToLocal(
log.markedAt.toIso8601String(),
format: 'd MMM yyyy');
final time =
DateTimeUtils.convertUtcToLocal(log.markedAt.toIso8601String(),
final time = DateTimeUtils.convertUtcToLocal(
log.markedAt.toIso8601String(),
format: 'hh:mm a');
return Card(
@ -623,8 +630,10 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
if (log.latitude != null && log.longitude != null)
GestureDetector(
onTap: () async {
final lat = double.tryParse(log.latitude!) ?? 0.0;
final lon = double.tryParse(log.longitude!) ?? 0.0;
final lat =
double.tryParse(log.latitude!) ?? 0.0;
final lon =
double.tryParse(log.longitude!) ?? 0.0;
final url =
'https://www.google.com/maps/search/?api=1&query=$lat,$lon';
if (await canLaunchUrl(Uri.parse(url))) {
@ -645,7 +654,8 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
style: TextStyle(
fontSize: 12,
color: Colors.blue,
decoration: TextDecoration.underline),
decoration:
TextDecoration.underline),
),
],
),
@ -664,7 +674,8 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
log.document!.preSignedUrl,
fit: BoxFit.cover,
height: 250,
errorBuilder: (_, __, ___) => const Icon(
errorBuilder: (_, __, ___) =>
const Icon(
Icons.broken_image,
size: 50,
color: Colors.grey,