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:
parent
fa767ea201
commit
1e48c686b2
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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!,
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user