diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 8b5b277..33c0ed6 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,8 +1,10 @@ class ApiEndpoints { - static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; + static const String baseUrl = "https://api.onfieldwork.com/api"; + static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = @@ -145,7 +147,7 @@ class ApiEndpoints { static const String editServiceProjectJob = "/serviceproject/job/edit"; static const String createServiceProjectJob = "/serviceproject/job/create"; static const String serviceProjectUpateJobAttendance = "/serviceproject/job/attendance"; - static const String serviceProjectUpateJobAttendanceLog = "/job/attendance/log"; + static const String serviceProjectUpateJobAttendanceLog = "/serviceproject/job/attendance/log"; static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list"; static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation"; static const String getTeamRoles = "/master/team-roles/list"; diff --git a/lib/view/service_project/service_project_job_detail_screen.dart b/lib/view/service_project/service_project_job_detail_screen.dart index d689b97..cb30c40 100644 --- a/lib/view/service_project/service_project_job_detail_screen.dart +++ b/lib/view/service_project/service_project_job_detail_screen.dart @@ -28,6 +28,8 @@ class JobDetailsScreen extends StatefulWidget { class _JobDetailsScreenState extends State with UIMixin { late final ServiceProjectDetailsController controller; + final RxBool isAttendanceExpanded = false.obs; + RxBool isAttendanceLogLoading = false.obs; final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); @@ -152,7 +154,9 @@ class _JobDetailsScreenState extends State with UIMixin { } final success = await ApiService.editServiceProjectJobApi( - jobId: job.id, operations: operations); + jobId: job.id, + operations: operations, + ); if (success) { Get.snackbar("Success", "Job updated successfully"); @@ -167,16 +171,15 @@ class _JobDetailsScreenState extends State with UIMixin { final job = controller.jobDetail.value?.data; if (job == null) return; - // Determine action based on current/next tagging state final action = job.nextTaggingAction; - File? attachmentFile; - // Step 1: Ask for comment first (optional) + // Step 1: Show comment bottom sheet final comment = await showCommentBottomSheet( context, action == 0 ? "Tag In" : "Tag Out"); + if (comment == null) return; // User cancelled - // Step 2: Ask for image optionally using your custom ConfirmDialog + // Step 2: Ask for optional image await showDialog( context: context, builder: (_) => ConfirmDialog( @@ -196,13 +199,23 @@ class _JobDetailsScreenState extends State with UIMixin { ), ); - // Step 3: Perform attendance using controller + // Step 3: Call attendance update await controller.updateJobAttendance( jobId: job.id, action: action == 0 ? 0 : 1, - comment: comment ?? "", + comment: comment, 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 + } + + // Success + Get.snackbar("Success", controller.attendanceMessage.value); } Widget _buildSectionCard({ @@ -441,52 +454,54 @@ class _JobDetailsScreenState extends State with UIMixin { } Widget _buildAttendanceCard() { - return Obx(() { - final job = controller.jobDetail.value?.data; - final isLoading = controller.isTagging.value; - final action = job?.nextTaggingAction ?? 0; - final RxBool isExpanded = false.obs; - final logs = controller.attendanceLog.value?.data ?? []; + return Obx(() { + final job = controller.jobDetail.value?.data; + final isLoading = controller.isTagging.value; + final action = job?.nextTaggingAction; + final logs = controller.attendanceLog.value?.data ?? []; - return Card( - elevation: 3, - shadowColor: Colors.black12, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - margin: const EdgeInsets.symmetric(vertical: 8), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header - Row( - children: [ - Icon(Icons.access_time_outlined, - size: 20, color: Colors.blueAccent), - const SizedBox(width: 8), - MyText.bodyLarge("Attendance", fontWeight: 700, fontSize: 16), - const Spacer(), - Obx(() => IconButton( - icon: Icon( - isExpanded.value - ? Icons.expand_less - : Icons.expand_more, - color: Colors.grey[600], - ), - onPressed: () async { - isExpanded.value = !isExpanded.value; - // Fetch attendance logs only when expanded - if (isExpanded.value && job != null) { - await controller.fetchJobAttendanceLog(job.attendanceId ?? ''); - } - }, - )), - ], - ), - const SizedBox(height: 8), - const Divider(), + if (job == null) return const SizedBox(); - // Tag In/Tag Out Button + return Card( + elevation: 3, + shadowColor: Colors.black12, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + margin: const EdgeInsets.symmetric(vertical: 8), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Icon(Icons.access_time_outlined, + size: 20, color: Colors.blueAccent), + const SizedBox(width: 8), + MyText.bodyLarge("Attendance", fontWeight: 700, fontSize: 16), + const Spacer(), + Obx(() => IconButton( + icon: Icon( + isAttendanceExpanded.value + ? Icons.expand_less + : Icons.expand_more, + 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( @@ -507,8 +522,7 @@ class _JobDetailsScreenState extends State with UIMixin { fontWeight: 600, color: Colors.white, ), - onPressed: - isLoading || job == null ? null : _handleTagAction, + onPressed: isLoading ? null : _handleTagAction, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 20), shape: RoundedRectangleBorder( @@ -520,86 +534,97 @@ class _JobDetailsScreenState extends State with UIMixin { ), ), - // Attendance Logs List - Obx(() { - if (!isExpanded.value) return Container(); + // Attendance Logs + Obx(() { + if (!isAttendanceExpanded.value) return Container(); - if (logs.isEmpty) { - return Padding( - padding: const EdgeInsets.only(top: 12), - child: MyText.bodyMedium( - "No attendance logs available", - color: Colors.grey[600], - ), - ); - } + if (isAttendanceLogLoading.value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Center(child: CircularProgressIndicator()), + ); + } - return ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), + if (logs.isEmpty) { + return Padding( padding: const EdgeInsets.only(top: 12), - itemCount: logs.length, - separatorBuilder: (_, __) => const SizedBox(height: 12), - itemBuilder: (_, index) { - final log = logs[index]; - final employeeName = - "${log.employee.firstName} ${log.employee.lastName}"; + child: MyText.bodyMedium( + "No attendance logs available", + color: Colors.grey[600], + ), + ); + } - return Container( - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], - ), - padding: const EdgeInsets.all(12), + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(top: 12), + itemCount: logs.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + 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 Card( + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header: Icon + Employee + Date + Time + // Top Row: Icon, Employee, Date, Time Row( children: [ Icon( log.action == 0 ? Icons.login : Icons.logout, - color: - log.action == 0 ? Colors.green : Colors.red, + color: log.action == 0 + ? Colors.green + : Colors.red, + size: 18, ), - const SizedBox(width: 8), + const SizedBox(width: 6), Expanded( - child: MyText.bodyMedium( - "$employeeName | ${DateTimeUtils.convertUtcToLocal(log.markedAt.toIso8601String(), format: 'd MMM yyyy')}", - fontWeight: 600, + child: Text( + employeeName, + style: const TextStyle( + fontWeight: FontWeight.w600), ), ), - MyText.bodySmall( - "Time: ${DateTimeUtils.convertUtcToLocal(log.markedAt.toIso8601String(), format: 'hh:mm a')}", - color: Colors.grey[700], + Text( + "$date | $time", + style: TextStyle( + fontSize: 12, color: Colors.grey[700]), ), ], ), - const SizedBox(height: 8), - const Divider(height: 1, color: Colors.grey), - const SizedBox(height: 8), + const SizedBox(height: 4), - // Comment / Description - MyText.bodySmall( - "Description: ${log.comment?.isNotEmpty == true ? log.comment : 'No description provided'}", - ), - const SizedBox(height: 8), + // Comment + if (log.comment?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + log.comment!, + style: const TextStyle(fontSize: 13), + ), + ), // Location 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))) { @@ -607,68 +632,77 @@ class _JobDetailsScreenState extends State with UIMixin { mode: LaunchMode.externalApplication); } }, - child: Row( - children: [ - Icon(Icons.location_on, - size: 16, color: Colors.blue), - const SizedBox(width: 4), - MyText.bodySmall( - "View Location", - color: Colors.blue, - decoration: TextDecoration.underline, - ), - ], + 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), + ), + ], + ), ), ), - const SizedBox(height: 8), // Attached Image if (log.document != null) - GestureDetector( - onTap: () => showDialog( - context: context, - builder: (_) => Dialog( - child: Image.network( - log.document!.preSignedUrl, - fit: BoxFit.cover, - height: 400, - errorBuilder: (_, __, ___) => const Icon( - Icons.broken_image, - size: 50, - color: Colors.grey, + 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(8), - child: Image.network( - log.document!.thumbPreSignedUrl.isNotEmpty - ? log.document!.thumbPreSignedUrl - : log.document!.preSignedUrl, - height: 60, - width: 60, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => const Icon( - Icons.broken_image, - size: 40, - 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 Widget build(BuildContext context) {