Compare commits

...

1 Commits

10 changed files with 572 additions and 135 deletions

View File

@ -4,4 +4,7 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>

View File

@ -1,4 +1,7 @@
<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
android:label="marco"
android:name="${applicationName}"

View File

@ -1,8 +1,11 @@
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.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/employee_model.dart'; // Assuming you have an EmployeeModel for the employees.
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/AttendanceLogModel.dart';
class AttendanceController extends GetxController {
List<AttendanceModel> attendances = [];
@ -17,26 +20,28 @@ class AttendanceController extends GetxController {
}
// Fetch projects from API
Future<void> fetchProjects() async {
var response = await ApiService.getProjects(); // Call the project API
Future<void> fetchProjects() async {
var response = await ApiService.getProjects(); // Call the project API
if (response != null) {
projects = response
.map<ProjectModel>((json) => ProjectModel.fromJson(json))
.toList();
if (response != null) {
projects = response
.map<ProjectModel>((json) => ProjectModel.fromJson(json))
.toList();
// Set default to the first project if available
if (projects.isNotEmpty) {
selectedProjectId = projects.first.id.toString();
await fetchEmployeesByProject(selectedProjectId); // Fetch employees for the first project
// Set default to the first project if available
if (projects.isNotEmpty) {
selectedProjectId = projects.first.id.toString();
await fetchEmployeesByProject(
selectedProjectId); // Fetch employees for the first project
}
update([
'attendance_dashboard_controller'
]); // Notify GetBuilder with your tag
} else {
print("No projects data found or failed to fetch data.");
}
update(['attendance_dashboard_controller']); // Notify GetBuilder with your tag
} else {
print("No projects data found or failed to fetch data.");
}
}
// Fetch employees by project ID
Future<void> fetchEmployeesByProject(String? projectId) async {
@ -53,4 +58,53 @@ Future<void> fetchProjects() async {
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.");
}
}
}

View File

@ -1,6 +1,8 @@
import 'dart:convert';
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:intl/intl.dart';
class ApiService {
static const String baseUrl = "https://api.marcoaiot.com/api";
@ -50,7 +52,8 @@ class ApiService {
}
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: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $jwtToken',
@ -74,4 +77,122 @@ class ApiService {
}
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;
}
}

View 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,
);
}
}

View File

@ -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/register_account_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/error_404_screen.dart';
import 'package:marco/view/error_pages/error_500_screen.dart';
@ -20,7 +20,7 @@ class AuthMiddleware extends GetMiddleware {
getPageRoute() {
var routes = [
GetPage(name: '/', page: () => const EcommerceScreen(), middlewares: [AuthMiddleware()]),
GetPage(name: '/', page: () => const AttendanceScreen(), middlewares: [AuthMiddleware()]),
// Dashboard
GetPage(name: '/dashboard/attendance', page: () => AttendanceScreen(), middlewares: [AuthMiddleware()]),

View File

@ -16,6 +16,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:intl/intl.dart';
class AttendanceScreen extends StatefulWidget {
const AttendanceScreen({super.key});
@ -62,7 +63,45 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
MySpacing.height(flexSpacing),
MyFlex(
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,125 +114,193 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
);
}
Widget attendanceTableCard() {
return MyCard.bordered(
borderRadiusAll: 4,
border: Border.all(color: Colors.grey.withAlpha(50)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 24,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: MyContainer.bordered(
padding: MySpacing.xy(8, 4),
child: PopupMenuButton<String>(
onSelected: (value) {
setState(() {
attendanceController.selectedProjectId = value;
attendanceController.fetchEmployeesByProject(value);
});
},
itemBuilder: (BuildContext context) {
return attendanceController.projects.map((project) {
return PopupMenuItem<String>(
value: project.id.toString(),
height: 32,
child: MyText.bodySmall(
project.name,
color: theme.colorScheme.onSurface,
fontWeight: 600,
),
);
}).toList();
},
color: theme.cardTheme.color,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.labelSmall(
attendanceController.selectedProjectId != null
? attendanceController.projects
.firstWhereOrNull((proj) =>
proj.id.toString() ==
attendanceController
.selectedProjectId)
?.name ??
'Select a Project'
: 'Select a Project',
Widget employeeListTab() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: MyContainer.bordered(
padding: MySpacing.xy(4, 8),
child: PopupMenuButton<String>(
onSelected: (value) {
setState(() {
attendanceController.selectedProjectId = value;
attendanceController.fetchEmployeesByProject(value);
attendanceController.fetchAttendanceLogs(value);
});
},
itemBuilder: (BuildContext context) {
return attendanceController.projects.map((project) {
return PopupMenuItem<String>(
value: project.id.toString(),
height: 32,
child: MyText.bodySmall(
project.name,
color: theme.colorScheme.onSurface,
fontWeight: 600,
),
Icon(LucideIcons.chevron_down,
size: 16, color: theme.colorScheme.onSurface),
],
),
);
}).toList();
},
color: theme.cardTheme.color,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.labelSmall(
attendanceController.selectedProjectId != null
? attendanceController.projects
.firstWhereOrNull((proj) =>
proj.id.toString() ==
attendanceController.selectedProjectId)
?.name ??
'Select a Project'
: 'Select a Project',
color: theme.colorScheme.onSurface,
),
Icon(LucideIcons.chevron_down,
size: 16, color: theme.colorScheme.onSurface),
],
),
),
),
],
),
MySpacing.height(24),
attendanceController.employees.isEmpty
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
sortAscending: true,
columnSpacing: 88,
onSelectAll: (_) => {},
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('ID',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Name',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Designation',
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('Actions',
color: contentTheme.primary)),
],
rows: attendanceController.employees
.mapIndexed((index, employee) => DataRow(cells: [
DataCell(MyText.bodyMedium(employee.id.toString(),
fontWeight: 600)),
DataCell(MyText.bodyMedium(employee.name,
fontWeight: 600)),
DataCell(MyText.bodyMedium(employee.designation,
fontWeight: 600)),
DataCell(MyText.bodyMedium(employee.checkIn,
fontWeight: 600)),
DataCell(MyText.bodyMedium(employee.checkOut,
fontWeight: 600)),
DataCell(MyText.bodyMedium(employee.actions.toString(),
fontWeight: 600)),
]))
.toList(),
),
],
),
MySpacing.height(24),
attendanceController.employees.isEmpty
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
sortAscending: true,
columnSpacing: 15,
onSelectAll: (_) => {},
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('Designation',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Actions',
color: contentTheme.primary)),
],
rows: attendanceController.employees
.mapIndexed((index, employee) => DataRow(cells: [
DataCell(MyText.bodyMedium(employee.name,
fontWeight: 600)),
DataCell(MyText.bodyMedium(employee.designation,
fontWeight: 600)),
DataCell(
ElevatedButton(
onPressed: () async {
final success = await attendanceController
.captureAndUploadAttendance(
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(),
),
),
],
);
}
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(),
),
);
}

View File

@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation
import file_picker
import file_selector_macos
import geolocator_apple
import path_provider_foundation
import quill_native_bridge_macos
@ -14,6 +15,7 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin"))

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.2"
archive:
dependency: transitive
description:
name: archive
sha256: a7f37ff061d7abc2fcf213554b9dcaca713c5853afa5c065c44888bc9ccaf813
url: "https://pub.dev"
source: hosted
version: "4.0.6"
args:
dependency: transitive
description:
@ -169,6 +177,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -437,6 +453,78 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@ -517,6 +605,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nested:
dependency: transitive
description:
@ -629,6 +725,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
platform:
dependency: transitive
description:
@ -645,6 +749,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
url: "https://pub.dev"
source: hosted
version: "6.0.2"
provider:
dependency: "direct main"
description:
@ -1058,6 +1170,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.0"

View File

@ -61,6 +61,8 @@ dependencies:
http: ^1.2.2
geolocator: ^9.0.1
permission_handler: ^11.3.0
image: ^4.0.17
image_picker: ^1.0.7
dev_dependencies:
flutter_test: