feat: integrate snackbar notifications for document and job updates in user document controller and job detail screen
This commit is contained in:
parent
92c739045c
commit
ec6a45ed43
@ -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),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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({
|
||||||
@ -454,255 +459,261 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAttendanceCard() {
|
Widget _buildAttendanceCard() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final job = controller.jobDetail.value?.data;
|
final job = controller.jobDetail.value?.data;
|
||||||
final isLoading = controller.isTagging.value;
|
final isLoading = controller.isTagging.value;
|
||||||
final action = job?.nextTaggingAction;
|
final action = job?.nextTaggingAction;
|
||||||
final logs = controller.attendanceLog.value?.data ?? [];
|
final logs = controller.attendanceLog.value?.data ?? [];
|
||||||
|
|
||||||
if (job == null) return const SizedBox();
|
if (job == null) return const SizedBox();
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
shadowColor: Colors.black12,
|
shadowColor: Colors.black12,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header
|
// Header
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.access_time_outlined,
|
Icon(Icons.access_time_outlined,
|
||||||
size: 20, color: Colors.blueAccent),
|
size: 20, color: Colors.blueAccent),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
MyText.bodyLarge("Attendance", fontWeight: 700, fontSize: 16),
|
MyText.bodyLarge("Attendance", fontWeight: 700, fontSize: 16),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Obx(() => IconButton(
|
Obx(() => IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isAttendanceExpanded.value
|
isAttendanceExpanded.value
|
||||||
? Icons.expand_less
|
? Icons.expand_less
|
||||||
: Icons.expand_more,
|
: Icons.expand_more,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
isAttendanceExpanded.value =
|
||||||
|
!isAttendanceExpanded.value;
|
||||||
|
if (isAttendanceExpanded.value && job != null) {
|
||||||
|
await controller
|
||||||
|
.fetchJobAttendanceLog(job.attendanceId ?? '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Divider(),
|
||||||
|
|
||||||
|
// Tag In/Tag Out Button
|
||||||
|
if (action != null)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 36,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
icon: isLoading
|
||||||
|
? SizedBox(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(action == 0 ? Icons.login : Icons.logout),
|
||||||
|
label: MyText.bodyMedium(
|
||||||
|
action == 0 ? "Tag In" : "Tag Out",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: isLoading ? null : _handleTagAction,
|
||||||
isAttendanceExpanded.value = !isAttendanceExpanded.value;
|
style: ElevatedButton.styleFrom(
|
||||||
if (isAttendanceExpanded.value && job != null) {
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
await controller.fetchJobAttendanceLog(
|
shape: RoundedRectangleBorder(
|
||||||
job.attendanceId ?? '');
|
borderRadius: BorderRadius.circular(5),
|
||||||
}
|
),
|
||||||
},
|
backgroundColor:
|
||||||
))
|
action == 0 ? Colors.green : Colors.red,
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
const Divider(),
|
|
||||||
|
|
||||||
// Tag In/Tag Out Button
|
|
||||||
if (action != null)
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 36,
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
icon: isLoading
|
|
||||||
? SizedBox(
|
|
||||||
height: 16,
|
|
||||||
width: 16,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Icon(action == 0 ? Icons.login : Icons.logout),
|
|
||||||
label: MyText.bodyMedium(
|
|
||||||
action == 0 ? "Tag In" : "Tag Out",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
onPressed: isLoading ? null : _handleTagAction,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
),
|
||||||
backgroundColor: action == 0 ? Colors.green : Colors.red,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// Attendance Logs
|
// Attendance Logs
|
||||||
Obx(() {
|
Obx(() {
|
||||||
if (!isAttendanceExpanded.value) return Container();
|
if (!isAttendanceExpanded.value) return Container();
|
||||||
|
|
||||||
if (isAttendanceLogLoading.value) {
|
if (isAttendanceLogLoading.value) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
child: Center(child: CircularProgressIndicator()),
|
child: Center(child: CircularProgressIndicator()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logs.isEmpty) {
|
if (logs.isEmpty) {
|
||||||
return Padding(
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: MyText.bodyMedium(
|
||||||
|
"No attendance logs available",
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
padding: const EdgeInsets.only(top: 12),
|
padding: const EdgeInsets.only(top: 12),
|
||||||
child: MyText.bodyMedium(
|
itemCount: logs.length,
|
||||||
"No attendance logs available",
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
color: Colors.grey[600],
|
itemBuilder: (_, index) {
|
||||||
),
|
final log = logs[index];
|
||||||
);
|
final employeeName =
|
||||||
}
|
"${log.employee.firstName} ${log.employee.lastName}";
|
||||||
|
final date = DateTimeUtils.convertUtcToLocal(
|
||||||
|
log.markedAt.toIso8601String(),
|
||||||
|
format: 'd MMM yyyy');
|
||||||
|
final time = DateTimeUtils.convertUtcToLocal(
|
||||||
|
log.markedAt.toIso8601String(),
|
||||||
|
format: 'hh:mm a');
|
||||||
|
|
||||||
return ListView.separated(
|
return Card(
|
||||||
shrinkWrap: true,
|
elevation: 1,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
shape: RoundedRectangleBorder(
|
||||||
padding: const EdgeInsets.only(top: 12),
|
borderRadius: BorderRadius.circular(8)),
|
||||||
itemCount: logs.length,
|
child: Padding(
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
padding: const EdgeInsets.symmetric(
|
||||||
itemBuilder: (_, index) {
|
horizontal: 12, vertical: 8),
|
||||||
final log = logs[index];
|
child: Column(
|
||||||
final employeeName =
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
"${log.employee.firstName} ${log.employee.lastName}";
|
children: [
|
||||||
final date =
|
// Top Row: Icon, Employee, Date, Time
|
||||||
DateTimeUtils.convertUtcToLocal(log.markedAt.toIso8601String(),
|
Row(
|
||||||
format: 'd MMM yyyy');
|
children: [
|
||||||
final time =
|
Icon(
|
||||||
DateTimeUtils.convertUtcToLocal(log.markedAt.toIso8601String(),
|
log.action == 0 ? Icons.login : Icons.logout,
|
||||||
format: 'hh:mm a');
|
color: log.action == 0
|
||||||
|
? Colors.green
|
||||||
return Card(
|
: Colors.red,
|
||||||
elevation: 1,
|
size: 18,
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 8),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Top Row: Icon, Employee, Date, Time
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
log.action == 0 ? Icons.login : Icons.logout,
|
|
||||||
color: log.action == 0
|
|
||||||
? Colors.green
|
|
||||||
: Colors.red,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
employeeName,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w600),
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Expanded(
|
||||||
"$date | $time",
|
child: Text(
|
||||||
style: TextStyle(
|
employeeName,
|
||||||
fontSize: 12, color: Colors.grey[700]),
|
style: const TextStyle(
|
||||||
),
|
fontWeight: FontWeight.w600),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
Text(
|
||||||
|
"$date | $time",
|
||||||
// Comment
|
style: TextStyle(
|
||||||
if (log.comment?.isNotEmpty == true)
|
fontSize: 12, color: Colors.grey[700]),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(top: 4),
|
],
|
||||||
child: Text(
|
|
||||||
log.comment!,
|
|
||||||
style: const TextStyle(fontSize: 13),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
// Location
|
// Comment
|
||||||
if (log.latitude != null && log.longitude != null)
|
if (log.comment?.isNotEmpty == true)
|
||||||
GestureDetector(
|
Padding(
|
||||||
onTap: () async {
|
|
||||||
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))) {
|
|
||||||
await launchUrl(Uri.parse(url),
|
|
||||||
mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Row(
|
child: Text(
|
||||||
mainAxisSize: MainAxisSize.min,
|
log.comment!,
|
||||||
children: const [
|
style: const TextStyle(fontSize: 13),
|
||||||
Icon(Icons.location_on,
|
|
||||||
size: 14, color: Colors.blue),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
"View Location",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.blue,
|
|
||||||
decoration: TextDecoration.underline),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// Attached Image
|
// Location
|
||||||
if (log.document != null)
|
if (log.latitude != null && log.longitude != null)
|
||||||
Padding(
|
GestureDetector(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
onTap: () async {
|
||||||
child: GestureDetector(
|
final lat =
|
||||||
onTap: () => showDialog(
|
double.tryParse(log.latitude!) ?? 0.0;
|
||||||
context: context,
|
final lon =
|
||||||
builder: (_) => Dialog(
|
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))) {
|
||||||
|
await launchUrl(Uri.parse(url),
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: const [
|
||||||
|
Icon(Icons.location_on,
|
||||||
|
size: 14, color: Colors.blue),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
"View Location",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration:
|
||||||
|
TextDecoration.underline),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Attached Image
|
||||||
|
if (log.document != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => Dialog(
|
||||||
|
child: Image.network(
|
||||||
|
log.document!.preSignedUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
height: 250,
|
||||||
|
errorBuilder: (_, __, ___) =>
|
||||||
|
const Icon(
|
||||||
|
Icons.broken_image,
|
||||||
|
size: 50,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
child: Image.network(
|
child: Image.network(
|
||||||
log.document!.preSignedUrl,
|
log.document!.thumbPreSignedUrl.isNotEmpty
|
||||||
|
? log.document!.thumbPreSignedUrl
|
||||||
|
: log.document!.preSignedUrl,
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
height: 250,
|
|
||||||
errorBuilder: (_, __, ___) => const Icon(
|
errorBuilder: (_, __, ___) => const Icon(
|
||||||
Icons.broken_image,
|
Icons.broken_image,
|
||||||
size: 50,
|
size: 40,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
child: Image.network(
|
|
||||||
log.document!.thumbPreSignedUrl.isNotEmpty
|
|
||||||
? log.document!.thumbPreSignedUrl
|
|
||||||
: log.document!.preSignedUrl,
|
|
||||||
height: 50,
|
|
||||||
width: 50,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, __, ___) => const Icon(
|
|
||||||
Icons.broken_image,
|
|
||||||
size: 40,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
}),
|
||||||
}),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user