refactor: update application ID and improve attendance upload functionality

- Changed application ID from "com.marco.aiotstage" to "com.marco.aiot".
- Made markTime and date parameters required in uploadAttendanceImage method.
- Added logic to handle date selection and ensure attendance logs are uploaded with the correct date.
- Enhanced UI components for better user experience in attendance and directory views.
This commit is contained in:
Vaibhav Surve 2025-08-18 15:32:17 +05:30
parent fa767ea201
commit 1e48c686b2
8 changed files with 177 additions and 135 deletions

View File

@ -34,7 +34,7 @@ android {
// Default configuration for your application // Default configuration for your application
defaultConfig { defaultConfig {
// Specify your unique Application ID. This identifies your app on Google Play. // 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 // Set minimum and target SDK versions based on Flutter's configuration
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion

View File

@ -3,16 +3,12 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application <application
android:label="Marco_Stage" android:label="Marco"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

View File

@ -108,7 +108,8 @@ class AttendanceController extends GetxController {
String comment = "Marked via mobile app", String comment = "Marked via mobile app",
required int action, required int action,
bool imageCapture = true, bool imageCapture = true,
String? markTime, String? markTime, // still optional in controller
String? date, // new optional param
}) async { }) async {
try { try {
uploadingStates[employeeId]?.value = true; uploadingStates[employeeId]?.value = true;
@ -141,6 +142,29 @@ class AttendanceController extends GetxController {
? ApiService.generateImageName(employeeId, employees.length + 1) ? ApiService.generateImageName(employeeId, employees.length + 1)
: ""; : "";
// ---------------- DATE / TIME LOGIC ----------------
final now = DateTime.now();
// Default effectiveDate = now
DateTime effectiveDate = now;
if (action == 1) {
// Checkout
// Try to find today's open log for this employee
final log = attendanceLogs.firstWhereOrNull(
(log) => 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( final result = await ApiService.uploadAttendanceImage(
id, id,
employeeId, employeeId,
@ -152,10 +176,12 @@ class AttendanceController extends GetxController {
comment: comment, comment: comment,
action: action, action: action,
imageCapture: imageCapture, 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; return result;
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error uploading attendance", logSafe("Error uploading attendance",

View File

@ -182,22 +182,16 @@ class DirectoryController extends GetxController {
final bucketMatch = selectedBuckets.isEmpty || final bucketMatch = selectedBuckets.isEmpty ||
contact.bucketIds.any((id) => selectedBuckets.contains(id)); contact.bucketIds.any((id) => selectedBuckets.contains(id));
// Name, org, email, phone, tags
final nameMatch = contact.name.toLowerCase().contains(query); final nameMatch = contact.name.toLowerCase().contains(query);
final orgMatch = contact.organization.toLowerCase().contains(query); final orgMatch = contact.organization.toLowerCase().contains(query);
final emailMatch = contact.contactEmails final emailMatch = contact.contactEmails
.any((e) => e.emailAddress.toLowerCase().contains(query)); .any((e) => e.emailAddress.toLowerCase().contains(query));
final phoneMatch = contact.contactPhones final phoneMatch = contact.contactPhones
.any((p) => p.phoneNumber.toLowerCase().contains(query)); .any((p) => p.phoneNumber.toLowerCase().contains(query));
final tagMatch = final tagMatch =
contact.tags.any((tag) => tag.name.toLowerCase().contains(query)); contact.tags.any((tag) => tag.name.toLowerCase().contains(query));
final categoryNameMatch = final categoryNameMatch =
contact.contactCategory?.name.toLowerCase().contains(query) ?? false; contact.contactCategory?.name.toLowerCase().contains(query) ?? false;
final bucketNameMatch = contact.bucketIds.any((id) { final bucketNameMatch = contact.bucketIds.any((id) {
final bucketName = contactBuckets final bucketName = contactBuckets
.firstWhereOrNull((b) => b.id == id) .firstWhereOrNull((b) => b.id == id)
@ -218,6 +212,10 @@ class DirectoryController extends GetxController {
return categoryMatch && bucketMatch && searchMatch; return categoryMatch && bucketMatch && searchMatch;
}).toList(); }).toList();
// 🔑 Ensure results are always alphabetically sorted
filteredContacts
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
} }
void toggleCategory(String categoryId) { void toggleCategory(String categoryId) {

View File

@ -1085,64 +1085,66 @@ class ApiService {
? _parseResponse(res, label: 'Regularization Logs') ? _parseResponse(res, label: 'Regularization Logs')
: null); : null);
static Future<bool> uploadAttendanceImage( static Future<bool> uploadAttendanceImage(
String id, String id,
String employeeId, String employeeId,
XFile? imageFile, XFile? imageFile,
double latitude, double latitude,
double longitude, { double longitude, {
required String imageName, required String imageName,
required String projectId, required String projectId,
String comment = "", String comment = "",
required int action, required int action,
bool imageCapture = true, bool imageCapture = true,
String? markTime, required String markTime, // 👈 now required
}) async { required String date, // 👈 new required param
final now = DateTime.now(); }) async {
final body = { final body = {
"id": id, "id": id,
"employeeId": employeeId, "employeeId": employeeId,
"projectId": projectId, "projectId": projectId,
"markTime": markTime ?? DateFormat('hh:mm a').format(now), "markTime": markTime, // 👈 directly from UI
"comment": comment, "comment": comment,
"action": action, "action": action,
"date": DateFormat('yyyy-MM-dd').format(now), "date": date, // 👈 directly from UI
if (imageCapture) "latitude": '$latitude', if (imageCapture) "latitude": '$latitude',
if (imageCapture) "longitude": '$longitude', if (imageCapture) "longitude": '$longitude',
}; };
if (imageCapture && imageFile != null) { if (imageCapture && imageFile != null) {
try { try {
final bytes = await imageFile.readAsBytes(); final bytes = await imageFile.readAsBytes();
final fileSize = await imageFile.length(); final fileSize = await imageFile.length();
final contentType = "image/${imageFile.path.split('.').last}"; final contentType = "image/${imageFile.path.split('.').last}";
body["image"] = { body["image"] = {
"fileName": imageName, "fileName": imageName,
"contentType": contentType, "contentType": contentType,
"fileSize": fileSize, "fileSize": fileSize,
"description": "Employee attendance photo", "description": "Employee attendance photo",
"base64Data": base64Encode(bytes), "base64Data": base64Encode(bytes),
}; };
} catch (e) { } catch (e) {
logSafe("Image encoding error: $e", level: LogLevel.error); logSafe("Image encoding error: $e", level: LogLevel.error);
return false; 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) { static String generateImageName(String employeeId, int count) {
final now = DateTime.now(); final now = DateTime.now();
final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now); final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now);

View File

@ -49,77 +49,77 @@ class BaseBottomSheet extends StatelessWidget {
), ),
], ],
), ),
child: Padding( child: SafeArea(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), // 👈 prevents overlap with nav bar
child: Column( top: false,
mainAxisSize: MainAxisSize.min, child: Padding(
children: [ padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
MySpacing.height(5), child: Column(
Container( mainAxisSize: MainAxisSize.min,
width: 40, children: [
height: 5, MySpacing.height(5),
decoration: BoxDecoration( Container(
color: Colors.grey.shade300, width: 40,
borderRadius: BorderRadius.circular(10), height: 5,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(10),
),
), ),
), MySpacing.height(12),
MySpacing.height(12), MyText.titleLarge(title, fontWeight: 700),
MyText.titleLarge(title, fontWeight: 700), MySpacing.height(12),
MySpacing.height(12), child,
child, MySpacing.height(12),
if (showButtons) ...[
MySpacing.height(12), Row(
children: [
// 👇 Buttons (if enabled) Expanded(
if (showButtons) ...[ child: ElevatedButton.icon(
Row( onPressed: onCancel,
children: [ icon: const Icon(Icons.close, color: Colors.white),
Expanded( label: MyText.bodyMedium(
child: ElevatedButton.icon( "Cancel",
onPressed: onCancel, color: Colors.white,
icon: const Icon(Icons.close, color: Colors.white), fontWeight: 600,
label: MyText.bodyMedium( ),
"Cancel", style: ElevatedButton.styleFrom(
color: Colors.white, backgroundColor: Colors.grey,
fontWeight: 600, shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(12),
style: ElevatedButton.styleFrom( ),
backgroundColor: Colors.grey, padding: const EdgeInsets.symmetric(vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
), ),
padding: const EdgeInsets.symmetric(vertical: 8),
), ),
), ),
), const SizedBox(width: 12),
const SizedBox(width: 12), Expanded(
Expanded( child: ElevatedButton.icon(
child: ElevatedButton.icon( onPressed: isSubmitting ? null : onSubmit,
onPressed: isSubmitting ? null : onSubmit, icon: Icon(submitIcon, color: Colors.white),
icon: Icon(submitIcon, color: Colors.white), label: MyText.bodyMedium(
label: MyText.bodyMedium( isSubmitting ? "Submitting..." : submitText,
isSubmitting ? "Submitting..." : submitText, color: Colors.white,
color: Colors.white, fontWeight: 600,
fontWeight: 600, ),
), style: ElevatedButton.styleFrom(
style: ElevatedButton.styleFrom( backgroundColor: submitColor,
backgroundColor: submitColor, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12),
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!,
], ],
], ],
], ),
), ),
), ),
), ),

View File

@ -94,10 +94,13 @@ class _AttendanceFilterBottomSheetState
), ),
InkWell( InkWell(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
onTap: () => widget.controller.selectDateRangeForAttendance( onTap: () async {
context, await widget.controller.selectDateRangeForAttendance(
widget.controller, context,
), widget.controller,
);
setState(() {}); // rebuild UI after date range is updated
},
child: Ink( child: Ink(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,

View File

@ -173,6 +173,23 @@ class _DirectoryViewState extends State<DirectoryView> {
const EdgeInsets.symmetric(horizontal: 12), const EdgeInsets.symmetric(horizontal: 12),
prefixIcon: const Icon(Icons.search, prefixIcon: const Icon(Icons.search,
size: 20, color: Colors.grey), size: 20, color: Colors.grey),
suffixIcon: ValueListenableBuilder<TextEditingValue>(
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...', hintText: 'Search contacts...',
filled: true, filled: true,
fillColor: Colors.white, fillColor: Colors.white,
@ -187,7 +204,7 @@ class _DirectoryViewState extends State<DirectoryView> {
), ),
), ),
), ),
), ),
MySpacing.width(8), MySpacing.width(8),
Obx(() { Obx(() {
final isFilterActive = controller.hasActiveFilters(); final isFilterActive = controller.hasActiveFilters();