feat: Enhance attendance functionality with image capture and location tracking
This commit is contained in:
parent
753bfcad8a
commit
15ae6a75fc
@ -4,4 +4,7 @@
|
|||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="marco"
|
android:label="marco"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/attendance_model.dart';
|
import 'package:marco/model/attendance_model.dart';
|
||||||
import 'package:marco/model/project_model.dart'; // Assuming you have a ProjectModel for the projects.
|
import 'package:marco/model/project_model.dart'; // Assuming you have a ProjectModel for the projects.
|
||||||
import 'package:marco/model/employee_model.dart'; // Assuming you have an EmployeeModel for the employees.
|
import 'package:marco/model/employee_model.dart'; // Assuming you have an EmployeeModel for the employees.
|
||||||
|
import 'package:marco/model/AttendanceLogModel.dart';
|
||||||
|
|
||||||
class AttendanceController extends GetxController {
|
class AttendanceController extends GetxController {
|
||||||
List<AttendanceModel> attendances = [];
|
List<AttendanceModel> attendances = [];
|
||||||
@ -17,7 +20,7 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch projects from API
|
// Fetch projects from API
|
||||||
Future<void> fetchProjects() async {
|
Future<void> fetchProjects() async {
|
||||||
var response = await ApiService.getProjects(); // Call the project API
|
var response = await ApiService.getProjects(); // Call the project API
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
@ -28,15 +31,17 @@ Future<void> fetchProjects() async {
|
|||||||
// Set default to the first project if available
|
// Set default to the first project if available
|
||||||
if (projects.isNotEmpty) {
|
if (projects.isNotEmpty) {
|
||||||
selectedProjectId = projects.first.id.toString();
|
selectedProjectId = projects.first.id.toString();
|
||||||
await fetchEmployeesByProject(selectedProjectId); // Fetch employees for the first project
|
await fetchEmployeesByProject(
|
||||||
|
selectedProjectId); // Fetch employees for the first project
|
||||||
}
|
}
|
||||||
|
|
||||||
update(['attendance_dashboard_controller']); // Notify GetBuilder with your tag
|
update([
|
||||||
|
'attendance_dashboard_controller'
|
||||||
|
]); // Notify GetBuilder with your tag
|
||||||
} else {
|
} else {
|
||||||
print("No projects data found or failed to fetch data.");
|
print("No projects data found or failed to fetch data.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Fetch employees by project ID
|
// Fetch employees by project ID
|
||||||
Future<void> fetchEmployeesByProject(String? projectId) async {
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
@ -53,4 +58,53 @@ Future<void> fetchProjects() async {
|
|||||||
print("Failed to fetch employees for project $projectId.");
|
print("Failed to fetch employees for project $projectId.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> captureAndUploadAttendance(int employeeId, int projectId,
|
||||||
|
{String comment = "Marked via mobile app"}) async {
|
||||||
|
try {
|
||||||
|
final XFile? image = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 80,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (image == null) return false;
|
||||||
|
|
||||||
|
final position = await Geolocator.getCurrentPosition(
|
||||||
|
desiredAccuracy: LocationAccuracy.high);
|
||||||
|
|
||||||
|
String imageName = ApiService.generateImageName(
|
||||||
|
employeeId,
|
||||||
|
employees.length + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await ApiService.uploadAttendanceImage(
|
||||||
|
employeeId,
|
||||||
|
image,
|
||||||
|
position.latitude,
|
||||||
|
position.longitude,
|
||||||
|
imageName: imageName,
|
||||||
|
projectId: projectId,
|
||||||
|
comment: comment,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print("Error capturing or uploading: $e");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AttendanceLogModel> attendanceLogs = [];
|
||||||
|
Future<void> fetchAttendanceLogs(String? projectId) async {
|
||||||
|
if (projectId == null) return;
|
||||||
|
|
||||||
|
var response = await ApiService.getAttendanceLogs(int.parse(projectId));
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
attendanceLogs = response
|
||||||
|
.map<AttendanceLogModel>((json) => AttendanceLogModel.fromJson(json))
|
||||||
|
.toList();
|
||||||
|
update();
|
||||||
|
} else {
|
||||||
|
print("Failed to fetch logs for project $projectId.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
static const String baseUrl = "https://api.marcoaiot.com/api";
|
static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||||
@ -50,7 +52,8 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final response = await http.get(
|
final response = await http.get(
|
||||||
Uri.parse("$baseUrl/attendance/project/team?projectId=$projectId"), // Ensure correct endpoint
|
Uri.parse(
|
||||||
|
"$baseUrl/attendance/project/team?projectId=$projectId"), // Ensure correct endpoint
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer $jwtToken',
|
'Authorization': 'Bearer $jwtToken',
|
||||||
@ -74,4 +77,122 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String generateImageName(int employeeId, int count) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final formattedDate = "${now.year.toString().padLeft(4, '0')}"
|
||||||
|
"${now.month.toString().padLeft(2, '0')}"
|
||||||
|
"${now.day.toString().padLeft(2, '0')}_"
|
||||||
|
"${now.hour.toString().padLeft(2, '0')}"
|
||||||
|
"${now.minute.toString().padLeft(2, '0')}"
|
||||||
|
"${now.second.toString().padLeft(2, '0')}";
|
||||||
|
final imageNumber = count.toString().padLeft(3, '0');
|
||||||
|
return "${employeeId}_${formattedDate}_$imageNumber.jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> uploadAttendanceImage(
|
||||||
|
int employeeId, XFile imageFile, double latitude, double longitude,
|
||||||
|
{required String imageName,
|
||||||
|
required int projectId,
|
||||||
|
String comment = "",
|
||||||
|
int action = 0}) async {
|
||||||
|
try {
|
||||||
|
String? jwtToken = LocalStorage.getJwtToken();
|
||||||
|
if (jwtToken == null) {
|
||||||
|
print("No JWT token found. Please log in.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final bytes = await imageFile.readAsBytes();
|
||||||
|
final base64Image = base64Encode(bytes);
|
||||||
|
final fileSize = await imageFile.length();
|
||||||
|
final contentType = "image/${imageFile.path.split('.').last}";
|
||||||
|
|
||||||
|
final imageObject = {
|
||||||
|
"FileName": imageName,
|
||||||
|
"Base64Data": base64Image,
|
||||||
|
"ContentType": contentType,
|
||||||
|
"FileSize": fileSize,
|
||||||
|
"Description": "Employee attendance photo"
|
||||||
|
};
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
// You can now include the attendance record directly in the main body
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse("$baseUrl/attendance/record"),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $jwtToken',
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
"ID": null,
|
||||||
|
"employeeId": employeeId,
|
||||||
|
"projectId": projectId,
|
||||||
|
"markTime": DateFormat('hh:mm a').format(now),
|
||||||
|
"comment": comment,
|
||||||
|
"action": action,
|
||||||
|
"date": DateFormat('yyyy-MM-dd').format(now),
|
||||||
|
"latitude": latitude,
|
||||||
|
"longitude": longitude,
|
||||||
|
"image": [imageObject], // Directly included in the body
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
print('body: ${jsonEncode({
|
||||||
|
"employeeId": employeeId,
|
||||||
|
"projectId": projectId,
|
||||||
|
"markTime": DateFormat('hh:mm a').format(now),
|
||||||
|
"comment": comment,
|
||||||
|
"action": action,
|
||||||
|
"date": DateFormat('yyyy-MM-dd').format(now),
|
||||||
|
"latitude": latitude,
|
||||||
|
"longitude": longitude,
|
||||||
|
"image": [imageObject],
|
||||||
|
})}');
|
||||||
|
print('uploadAttendanceImage: $baseUrl/attendance/record');
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
return json['success'] == true;
|
||||||
|
} else {
|
||||||
|
print("Error uploading image: ${response.statusCode}");
|
||||||
|
print("Response: ${response.body}");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Exception during image upload: $e");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<dynamic>?> getAttendanceLogs(int projectId) async {
|
||||||
|
try {
|
||||||
|
String? jwtToken = LocalStorage.getJwtToken();
|
||||||
|
if (jwtToken == null) {
|
||||||
|
print("No JWT token found. Please log in.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse(
|
||||||
|
"$baseUrl/attendance/project/team?projectId=$projectId"),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $jwtToken',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
if (json['success'] == true) {
|
||||||
|
return json['data'];
|
||||||
|
} else {
|
||||||
|
print("Error: ${json['message']}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("Error fetching logs: ${response.statusCode}");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Exception while fetching logs: $e");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
25
lib/model/AttendanceLogModel.dart
Normal file
25
lib/model/AttendanceLogModel.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
class AttendanceLogModel {
|
||||||
|
final String name;
|
||||||
|
final String role;
|
||||||
|
final DateTime? checkIn;
|
||||||
|
final DateTime? checkOut;
|
||||||
|
final int activity;
|
||||||
|
|
||||||
|
AttendanceLogModel({
|
||||||
|
required this.name,
|
||||||
|
required this.role,
|
||||||
|
this.checkIn,
|
||||||
|
this.checkOut,
|
||||||
|
required this.activity,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AttendanceLogModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return AttendanceLogModel(
|
||||||
|
name: "${json['firstName'] ?? ''} ${json['lastName'] ?? ''}".trim(),
|
||||||
|
role: json['jobRoleName'] ?? '',
|
||||||
|
checkIn: json['checkInTime'] != null ? DateTime.tryParse(json['checkInTime']) : null,
|
||||||
|
checkOut: json['checkOutTime'] != null ? DateTime.tryParse(json['checkOutTime']) : null,
|
||||||
|
activity: json['activity'] ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ import 'package:marco/view/auth/forgot_password_screen.dart';
|
|||||||
import 'package:marco/view/auth/login_screen.dart';
|
import 'package:marco/view/auth/login_screen.dart';
|
||||||
import 'package:marco/view/auth/register_account_screen.dart';
|
import 'package:marco/view/auth/register_account_screen.dart';
|
||||||
import 'package:marco/view/auth/reset_password_screen.dart';
|
import 'package:marco/view/auth/reset_password_screen.dart';
|
||||||
import 'package:marco/view/dashboard/ecommerce_screen.dart';
|
// import 'package:marco/view/dashboard/ecommerce_screen.dart';
|
||||||
import 'package:marco/view/error_pages/coming_soon_screen.dart';
|
import 'package:marco/view/error_pages/coming_soon_screen.dart';
|
||||||
import 'package:marco/view/error_pages/error_404_screen.dart';
|
import 'package:marco/view/error_pages/error_404_screen.dart';
|
||||||
import 'package:marco/view/error_pages/error_500_screen.dart';
|
import 'package:marco/view/error_pages/error_500_screen.dart';
|
||||||
@ -20,7 +20,7 @@ class AuthMiddleware extends GetMiddleware {
|
|||||||
|
|
||||||
getPageRoute() {
|
getPageRoute() {
|
||||||
var routes = [
|
var routes = [
|
||||||
GetPage(name: '/', page: () => const EcommerceScreen(), middlewares: [AuthMiddleware()]),
|
GetPage(name: '/', page: () => const AttendanceScreen(), middlewares: [AuthMiddleware()]),
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
GetPage(name: '/dashboard/attendance', page: () => AttendanceScreen(), middlewares: [AuthMiddleware()]),
|
GetPage(name: '/dashboard/attendance', page: () => AttendanceScreen(), middlewares: [AuthMiddleware()]),
|
||||||
|
@ -16,6 +16,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/view/layouts/layout.dart';
|
import 'package:marco/view/layouts/layout.dart';
|
||||||
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class AttendanceScreen extends StatefulWidget {
|
class AttendanceScreen extends StatefulWidget {
|
||||||
const AttendanceScreen({super.key});
|
const AttendanceScreen({super.key});
|
||||||
@ -62,7 +63,45 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
MySpacing.height(flexSpacing),
|
MySpacing.height(flexSpacing),
|
||||||
MyFlex(
|
MyFlex(
|
||||||
children: [
|
children: [
|
||||||
MyFlexItem(child: attendanceTableCard()),
|
MyFlexItem(
|
||||||
|
child: DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
child: MyCard.bordered(
|
||||||
|
borderRadiusAll: 4,
|
||||||
|
border:
|
||||||
|
Border.all(color: Colors.grey.withAlpha(50)),
|
||||||
|
shadow: MyShadow(
|
||||||
|
elevation: 1,
|
||||||
|
position: MyShadowPosition.bottom),
|
||||||
|
paddingAll: 10,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TabBar(
|
||||||
|
labelColor: theme.colorScheme.primary,
|
||||||
|
unselectedLabelColor: theme
|
||||||
|
.colorScheme.onSurface
|
||||||
|
.withAlpha(150),
|
||||||
|
tabs: const [
|
||||||
|
Tab(text: 'Employee List'),
|
||||||
|
Tab(text: 'Logs'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
SizedBox(
|
||||||
|
height: 500,
|
||||||
|
child: TabBarView(
|
||||||
|
children: [
|
||||||
|
employeeListTab(),
|
||||||
|
reportsTab(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -75,13 +114,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget attendanceTableCard() {
|
Widget employeeListTab() {
|
||||||
return MyCard.bordered(
|
return Column(
|
||||||
borderRadiusAll: 4,
|
|
||||||
border: Border.all(color: Colors.grey.withAlpha(50)),
|
|
||||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
|
||||||
paddingAll: 24,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@ -89,12 +123,13 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: MyContainer.bordered(
|
child: MyContainer.bordered(
|
||||||
padding: MySpacing.xy(8, 4),
|
padding: MySpacing.xy(4, 8),
|
||||||
child: PopupMenuButton<String>(
|
child: PopupMenuButton<String>(
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
attendanceController.selectedProjectId = value;
|
attendanceController.selectedProjectId = value;
|
||||||
attendanceController.fetchEmployeesByProject(value);
|
attendanceController.fetchEmployeesByProject(value);
|
||||||
|
attendanceController.fetchAttendanceLogs(value);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
itemBuilder: (BuildContext context) {
|
itemBuilder: (BuildContext context) {
|
||||||
@ -119,8 +154,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
? attendanceController.projects
|
? attendanceController.projects
|
||||||
.firstWhereOrNull((proj) =>
|
.firstWhereOrNull((proj) =>
|
||||||
proj.id.toString() ==
|
proj.id.toString() ==
|
||||||
attendanceController
|
attendanceController.selectedProjectId)
|
||||||
.selectedProjectId)
|
|
||||||
?.name ??
|
?.name ??
|
||||||
'Select a Project'
|
'Select a Project'
|
||||||
: 'Select a Project',
|
: 'Select a Project',
|
||||||
@ -142,7 +176,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: DataTable(
|
child: DataTable(
|
||||||
sortAscending: true,
|
sortAscending: true,
|
||||||
columnSpacing: 88,
|
columnSpacing: 15,
|
||||||
onSelectAll: (_) => {},
|
onSelectAll: (_) => {},
|
||||||
headingRowColor: WidgetStatePropertyAll(
|
headingRowColor: WidgetStatePropertyAll(
|
||||||
contentTheme.primary.withAlpha(40)),
|
contentTheme.primary.withAlpha(40)),
|
||||||
@ -156,9 +190,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
columns: [
|
columns: [
|
||||||
DataColumn(
|
|
||||||
label: MyText.labelLarge('ID',
|
|
||||||
color: contentTheme.primary)),
|
|
||||||
DataColumn(
|
DataColumn(
|
||||||
label: MyText.labelLarge('Name',
|
label: MyText.labelLarge('Name',
|
||||||
color: contentTheme.primary)),
|
color: contentTheme.primary)),
|
||||||
@ -166,34 +197,110 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
label: MyText.labelLarge('Designation',
|
label: MyText.labelLarge('Designation',
|
||||||
color: contentTheme.primary)),
|
color: contentTheme.primary)),
|
||||||
DataColumn(
|
DataColumn(
|
||||||
label: MyText.labelLarge('Check In',
|
|
||||||
color: contentTheme.primary)),
|
|
||||||
DataColumn(
|
|
||||||
label: MyText.labelLarge('Check Out',
|
|
||||||
color: contentTheme.primary)),
|
|
||||||
DataColumn(
|
|
||||||
label: MyText.labelLarge('Actions',
|
label: MyText.labelLarge('Actions',
|
||||||
color: contentTheme.primary)),
|
color: contentTheme.primary)),
|
||||||
],
|
],
|
||||||
rows: attendanceController.employees
|
rows: attendanceController.employees
|
||||||
.mapIndexed((index, employee) => DataRow(cells: [
|
.mapIndexed((index, employee) => DataRow(cells: [
|
||||||
DataCell(MyText.bodyMedium(employee.id.toString(),
|
|
||||||
fontWeight: 600)),
|
|
||||||
DataCell(MyText.bodyMedium(employee.name,
|
DataCell(MyText.bodyMedium(employee.name,
|
||||||
fontWeight: 600)),
|
fontWeight: 600)),
|
||||||
DataCell(MyText.bodyMedium(employee.designation,
|
DataCell(MyText.bodyMedium(employee.designation,
|
||||||
fontWeight: 600)),
|
fontWeight: 600)),
|
||||||
DataCell(MyText.bodyMedium(employee.checkIn,
|
DataCell(
|
||||||
fontWeight: 600)),
|
ElevatedButton(
|
||||||
DataCell(MyText.bodyMedium(employee.checkOut,
|
onPressed: () async {
|
||||||
fontWeight: 600)),
|
final success = await attendanceController
|
||||||
DataCell(MyText.bodyMedium(employee.actions.toString(),
|
.captureAndUploadAttendance(
|
||||||
fontWeight: 600)),
|
employee.id,
|
||||||
|
int.parse(attendanceController
|
||||||
|
.selectedProjectId ??
|
||||||
|
"0"),
|
||||||
|
comment: "Checked in via app",
|
||||||
|
);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
success
|
||||||
|
? 'Image uploaded successfully!'
|
||||||
|
: 'Image upload failed.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Check In'),
|
||||||
|
),
|
||||||
|
),
|
||||||
]))
|
]))
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget reportsTab() {
|
||||||
|
if (attendanceController.attendanceLogs.isEmpty) {
|
||||||
|
attendanceController
|
||||||
|
.fetchAttendanceLogs(attendanceController.selectedProjectId);
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: DataTable(
|
||||||
|
sortAscending: true,
|
||||||
|
columnSpacing: 15,
|
||||||
|
headingRowColor:
|
||||||
|
WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)),
|
||||||
|
dataRowMaxHeight: 60,
|
||||||
|
showBottomBorder: true,
|
||||||
|
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||||
|
border: TableBorder.all(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
style: BorderStyle.solid,
|
||||||
|
width: 0.4,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
columns: [
|
||||||
|
DataColumn(
|
||||||
|
label: MyText.labelLarge('Name', color: contentTheme.primary)),
|
||||||
|
DataColumn(
|
||||||
|
label: MyText.labelLarge('Role', color: contentTheme.primary)),
|
||||||
|
DataColumn(
|
||||||
|
label:
|
||||||
|
MyText.labelLarge('Check-In', color: contentTheme.primary)),
|
||||||
|
DataColumn(
|
||||||
|
label:
|
||||||
|
MyText.labelLarge('Check-Out', color: contentTheme.primary)),
|
||||||
|
DataColumn(
|
||||||
|
label: MyText.labelLarge('Action', color: contentTheme.primary)),
|
||||||
|
],
|
||||||
|
rows: attendanceController.attendanceLogs
|
||||||
|
.mapIndexed((index, log) => DataRow(cells: [
|
||||||
|
DataCell(MyText.bodyMedium(log.name, fontWeight: 600)),
|
||||||
|
DataCell(MyText.bodyMedium(log.role, fontWeight: 600)),
|
||||||
|
DataCell(MyText.bodyMedium(
|
||||||
|
log.checkIn != null
|
||||||
|
? DateFormat('dd MMM yyyy hh:mm a').format(log.checkIn!)
|
||||||
|
: '-',
|
||||||
|
fontWeight: 600,
|
||||||
|
)),
|
||||||
|
DataCell(MyText.bodyMedium(
|
||||||
|
log.checkOut != null
|
||||||
|
? DateFormat('dd MMM yyyy hh:mm a')
|
||||||
|
.format(log.checkOut!)
|
||||||
|
: '-',
|
||||||
|
fontWeight: 600,
|
||||||
|
)),
|
||||||
|
DataCell(IconButton(
|
||||||
|
icon: Icon(Icons.info_outline, color: contentTheme.primary),
|
||||||
|
onPressed: () {
|
||||||
|
// Action logic here
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
]))
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import file_picker
|
import file_picker
|
||||||
|
import file_selector_macos
|
||||||
import geolocator_apple
|
import geolocator_apple
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import quill_native_bridge_macos
|
import quill_native_bridge_macos
|
||||||
@ -14,6 +15,7 @@ import url_launcher_macos
|
|||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin"))
|
QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin"))
|
||||||
|
122
pubspec.lock
122
pubspec.lock
@ -9,6 +9,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.2"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: a7f37ff061d7abc2fcf213554b9dcaca713c5853afa5c065c44888bc9ccaf813
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.6"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -169,6 +177,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+2"
|
version: "0.9.3+2"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4+2"
|
||||||
file_selector_platform_interface:
|
file_selector_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -437,6 +453,78 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
|
image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.4"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.12+23"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.6"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.12+2"
|
||||||
|
image_picker_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_linux
|
||||||
|
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1+2"
|
||||||
|
image_picker_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_macos
|
||||||
|
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1+2"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.10.1"
|
||||||
|
image_picker_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_windows
|
||||||
|
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1+1"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -517,6 +605,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -629,6 +725,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1"
|
version: "0.2.1"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -645,6 +749,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.2"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1058,6 +1170,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.0-0 <4.0.0"
|
dart: ">=3.7.0-0 <4.0.0"
|
||||||
flutter: ">=3.24.0"
|
flutter: ">=3.27.0"
|
||||||
|
@ -61,6 +61,8 @@ dependencies:
|
|||||||
http: ^1.2.2
|
http: ^1.2.2
|
||||||
geolocator: ^9.0.1
|
geolocator: ^9.0.1
|
||||||
permission_handler: ^11.3.0
|
permission_handler: ^11.3.0
|
||||||
|
image: ^4.0.17
|
||||||
|
image_picker: ^1.0.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user