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();