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

View File

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