diff --git a/android/app/build.gradle b/android/app/build.gradle index 84581b2..953b77a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -34,7 +34,7 @@ android { // Default configuration for your application defaultConfig { // Specify your unique Application ID. This identifies your app on Google Play. - applicationId = "com.marco.aiotstage" + applicationId = "com.marco.aiot" // Set minimum and target SDK versions based on Flutter's configuration minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1e9ad59..d6ea985 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,16 +3,12 @@ - - - - log.employeeId == employeeId && log.checkOut == null, + ); + if (log?.checkIn != null) { + effectiveDate = log!.checkIn!; // use check-in date + } + } + + final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now); + + final formattedDate = + date ?? DateFormat('yyyy-MM-dd').format(effectiveDate); + + // ---------------- API CALL ---------------- final result = await ApiService.uploadAttendanceImage( id, employeeId, @@ -152,10 +176,12 @@ class AttendanceController extends GetxController { comment: comment, action: action, imageCapture: imageCapture, - markTime: markTime, + markTime: formattedMarkTime, + date: formattedDate, ); - logSafe("Attendance uploaded for $employeeId, action: $action"); + logSafe( + "Attendance uploaded for $employeeId, action: $action, date: $formattedDate"); return result; } catch (e, stacktrace) { logSafe("Error uploading attendance", diff --git a/lib/controller/directory/directory_controller.dart b/lib/controller/directory/directory_controller.dart index 9e70d51..4b1e887 100644 --- a/lib/controller/directory/directory_controller.dart +++ b/lib/controller/directory/directory_controller.dart @@ -182,22 +182,16 @@ class DirectoryController extends GetxController { final bucketMatch = selectedBuckets.isEmpty || contact.bucketIds.any((id) => selectedBuckets.contains(id)); - // Name, org, email, phone, tags final nameMatch = contact.name.toLowerCase().contains(query); final orgMatch = contact.organization.toLowerCase().contains(query); - final emailMatch = contact.contactEmails .any((e) => e.emailAddress.toLowerCase().contains(query)); - final phoneMatch = contact.contactPhones .any((p) => p.phoneNumber.toLowerCase().contains(query)); - final tagMatch = contact.tags.any((tag) => tag.name.toLowerCase().contains(query)); - final categoryNameMatch = contact.contactCategory?.name.toLowerCase().contains(query) ?? false; - final bucketNameMatch = contact.bucketIds.any((id) { final bucketName = contactBuckets .firstWhereOrNull((b) => b.id == id) @@ -218,6 +212,10 @@ class DirectoryController extends GetxController { return categoryMatch && bucketMatch && searchMatch; }).toList(); + + // 🔑 Ensure results are always alphabetically sorted + filteredContacts + .sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); } void toggleCategory(String categoryId) { diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 87e74a0..52e6f46 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -1085,64 +1085,66 @@ class ApiService { ? _parseResponse(res, label: 'Regularization Logs') : null); - static Future uploadAttendanceImage( - String id, - String employeeId, - XFile? imageFile, - double latitude, - double longitude, { - required String imageName, - required String projectId, - String comment = "", - required int action, - bool imageCapture = true, - String? markTime, - }) async { - final now = DateTime.now(); - final body = { - "id": id, - "employeeId": employeeId, - "projectId": projectId, - "markTime": markTime ?? DateFormat('hh:mm a').format(now), - "comment": comment, - "action": action, - "date": DateFormat('yyyy-MM-dd').format(now), - if (imageCapture) "latitude": '$latitude', - if (imageCapture) "longitude": '$longitude', - }; +static Future uploadAttendanceImage( + String id, + String employeeId, + XFile? imageFile, + double latitude, + double longitude, { + required String imageName, + required String projectId, + String comment = "", + required int action, + bool imageCapture = true, + required String markTime, // 👈 now required + required String date, // 👈 new required param +}) async { + final body = { + "id": id, + "employeeId": employeeId, + "projectId": projectId, + "markTime": markTime, // 👈 directly from UI + "comment": comment, + "action": action, + "date": date, // 👈 directly from UI + if (imageCapture) "latitude": '$latitude', + if (imageCapture) "longitude": '$longitude', + }; - if (imageCapture && imageFile != null) { - try { - final bytes = await imageFile.readAsBytes(); - final fileSize = await imageFile.length(); - final contentType = "image/${imageFile.path.split('.').last}"; - body["image"] = { - "fileName": imageName, - "contentType": contentType, - "fileSize": fileSize, - "description": "Employee attendance photo", - "base64Data": base64Encode(bytes), - }; - } catch (e) { - logSafe("Image encoding error: $e", level: LogLevel.error); - return false; - } + if (imageCapture && imageFile != null) { + try { + final bytes = await imageFile.readAsBytes(); + final fileSize = await imageFile.length(); + final contentType = "image/${imageFile.path.split('.').last}"; + body["image"] = { + "fileName": imageName, + "contentType": contentType, + "fileSize": fileSize, + "description": "Employee attendance photo", + "base64Data": base64Encode(bytes), + }; + } catch (e) { + logSafe("Image encoding error: $e", level: LogLevel.error); + return false; } - - final response = await _postRequest( - ApiEndpoints.uploadAttendanceImage, - body, - customTimeout: extendedTimeout, - ); - if (response == null) return false; - - final json = jsonDecode(response.body); - if (response.statusCode == 200 && json['success'] == true) return true; - - logSafe("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); - return false; } + final response = await _postRequest( + ApiEndpoints.uploadAttendanceImage, + body, + customTimeout: extendedTimeout, + ); + + if (response == null) return false; + + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) return true; + + logSafe("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); + return false; +} + + static String generateImageName(String employeeId, int count) { final now = DateTime.now(); final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now); diff --git a/lib/helpers/utils/base_bottom_sheet.dart b/lib/helpers/utils/base_bottom_sheet.dart index 06f32a4..359f82c 100644 --- a/lib/helpers/utils/base_bottom_sheet.dart +++ b/lib/helpers/utils/base_bottom_sheet.dart @@ -49,77 +49,77 @@ class BaseBottomSheet extends StatelessWidget { ), ], ), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - MySpacing.height(5), - Container( - width: 40, - height: 5, - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(10), + child: SafeArea( + // 👈 prevents overlap with nav bar + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MySpacing.height(5), + Container( + width: 40, + height: 5, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(10), + ), ), - ), - MySpacing.height(12), - MyText.titleLarge(title, fontWeight: 700), - MySpacing.height(12), - child, - - MySpacing.height(12), - - // 👇 Buttons (if enabled) - if (showButtons) ...[ - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: onCancel, - icon: const Icon(Icons.close, color: Colors.white), - label: MyText.bodyMedium( - "Cancel", - color: Colors.white, - fontWeight: 600, - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + MySpacing.height(12), + MyText.titleLarge(title, fontWeight: 700), + MySpacing.height(12), + child, + MySpacing.height(12), + if (showButtons) ...[ + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: onCancel, + icon: const Icon(Icons.close, color: Colors.white), + label: MyText.bodyMedium( + "Cancel", + color: Colors.white, + fontWeight: 600, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 8), ), - padding: const EdgeInsets.symmetric(vertical: 8), ), ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton.icon( - onPressed: isSubmitting ? null : onSubmit, - icon: Icon(submitIcon, color: Colors.white), - label: MyText.bodyMedium( - isSubmitting ? "Submitting..." : submitText, - color: Colors.white, - fontWeight: 600, - ), - style: ElevatedButton.styleFrom( - backgroundColor: submitColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: isSubmitting ? null : onSubmit, + icon: Icon(submitIcon, color: Colors.white), + label: MyText.bodyMedium( + isSubmitting ? "Submitting..." : submitText, + color: Colors.white, + fontWeight: 600, + ), + style: ElevatedButton.styleFrom( + backgroundColor: submitColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 8), ), - padding: const EdgeInsets.symmetric(vertical: 8), ), ), - ), + ], + ), + if (bottomContent != null) ...[ + MySpacing.height(12), + bottomContent!, ], - ), - // 👇 Optional Bottom Content - if (bottomContent != null) ...[ - MySpacing.height(12), - bottomContent!, ], ], - ], + ), ), ), ), diff --git a/lib/model/attendance/attendence_filter_sheet.dart b/lib/model/attendance/attendence_filter_sheet.dart index 53d371a..c275eb1 100644 --- a/lib/model/attendance/attendence_filter_sheet.dart +++ b/lib/model/attendance/attendence_filter_sheet.dart @@ -94,10 +94,13 @@ class _AttendanceFilterBottomSheetState ), InkWell( borderRadius: BorderRadius.circular(10), - onTap: () => widget.controller.selectDateRangeForAttendance( - context, - widget.controller, - ), + onTap: () async { + await widget.controller.selectDateRangeForAttendance( + context, + widget.controller, + ); + setState(() {}); // rebuild UI after date range is updated + }, child: Ink( decoration: BoxDecoration( color: Colors.white, diff --git a/lib/view/directory/directory_view.dart b/lib/view/directory/directory_view.dart index bfe12f0..54c4fee 100644 --- a/lib/view/directory/directory_view.dart +++ b/lib/view/directory/directory_view.dart @@ -173,6 +173,23 @@ class _DirectoryViewState extends State { const EdgeInsets.symmetric(horizontal: 12), prefixIcon: const Icon(Icons.search, size: 20, color: Colors.grey), + suffixIcon: ValueListenableBuilder( + valueListenable: searchController, + builder: (context, value, _) { + if (value.text.isEmpty) { + return const SizedBox.shrink(); + } + return IconButton( + icon: const Icon(Icons.clear, + size: 20, color: Colors.grey), + onPressed: () { + searchController.clear(); + controller.searchQuery.value = ''; + controller.applyFilters(); + }, + ); + }, + ), hintText: 'Search contacts...', filled: true, fillColor: Colors.white, @@ -187,7 +204,7 @@ class _DirectoryViewState extends State { ), ), ), - ), + ), MySpacing.width(8), Obx(() { final isFilterActive = controller.hasActiveFilters();