Compare commits
1 Commits
main
...
Feature_Lo
| Author | SHA1 | Date | |
|---|---|---|---|
| 15ae6a75fc |
144
.gitignore
vendored
144
.gitignore
vendored
@ -1,5 +1,9 @@
|
|||||||
|
# Do not remove or rename entries in this file, only add new ones
|
||||||
|
# See https://github.com/flutter/flutter/issues/128635 for more context.
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
*.class
|
*.class
|
||||||
|
*.lock
|
||||||
*.log
|
*.log
|
||||||
*.pyc
|
*.pyc
|
||||||
*.swp
|
*.swp
|
||||||
@ -16,10 +20,46 @@ migrate_working_dir/
|
|||||||
*.iws
|
*.iws
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# The .vscode folder contains launch configuration and tasks you configure in
|
# Visual Studio Code related
|
||||||
# VS Code which you may wish to be included in version control, so this line
|
.classpath
|
||||||
# is commented out by default.
|
.project
|
||||||
#.vscode/
|
.settings/
|
||||||
|
.vscode/*
|
||||||
|
.ccls-cache
|
||||||
|
|
||||||
|
# This file, on the master branch, should never exist or be checked-in.
|
||||||
|
#
|
||||||
|
# On a *final* release branch, that is, what will ship to stable or beta, the
|
||||||
|
# file can be force added (git add --force) and checked-in in order to effectively
|
||||||
|
# "pin" the engine artifact version so the flutter tool does not need to use git
|
||||||
|
# to determine the engine artifacts.
|
||||||
|
#
|
||||||
|
# See https://github.com/flutter/flutter/blob/main/docs/tool/Engine-artifacts.md.
|
||||||
|
/bin/internal/engine.version
|
||||||
|
|
||||||
|
# Flutter repo-specific
|
||||||
|
/bin/cache/
|
||||||
|
/bin/internal/bootstrap.bat
|
||||||
|
/bin/internal/bootstrap.sh
|
||||||
|
/bin/internal/engine.realm
|
||||||
|
/bin/mingit/
|
||||||
|
/dev/benchmarks/mega_gallery/
|
||||||
|
/dev/bots/.recipe_deps
|
||||||
|
/dev/bots/android_tools/
|
||||||
|
/dev/devicelab/ABresults*.json
|
||||||
|
/dev/docs/doc/
|
||||||
|
/dev/docs/api_docs.zip
|
||||||
|
/dev/docs/flutter.docs.zip
|
||||||
|
/dev/docs/lib/
|
||||||
|
/dev/docs/pubspec.yaml
|
||||||
|
/dev/integration_tests/**/xcuserdata
|
||||||
|
/dev/integration_tests/**/Pods
|
||||||
|
/packages/flutter/coverage/
|
||||||
|
version
|
||||||
|
analysis_benchmark.json
|
||||||
|
|
||||||
|
# packages file containing multi-root paths
|
||||||
|
.packages.generated
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
# Flutter/Dart/Pub related
|
||||||
**/doc/api/
|
**/doc/api/
|
||||||
@ -27,17 +67,97 @@ migrate_working_dir/
|
|||||||
.dart_tool/
|
.dart_tool/
|
||||||
.flutter-plugins
|
.flutter-plugins
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
|
**/generated_plugin_registrant.dart
|
||||||
|
.packages
|
||||||
|
.pub-preload-cache/
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
build/
|
||||||
|
flutter_*.png
|
||||||
|
linked_*.ds
|
||||||
|
unlinked.ds
|
||||||
|
unlinked_spec.ds
|
||||||
|
|
||||||
# Symbolication related
|
# Android related
|
||||||
|
**/android/**/gradle-wrapper.jar
|
||||||
|
.gradle/
|
||||||
|
**/android/captures/
|
||||||
|
**/android/gradlew
|
||||||
|
**/android/gradlew.bat
|
||||||
|
**/android/**/GeneratedPluginRegistrant.java
|
||||||
|
**/android/key.properties
|
||||||
|
*.jks
|
||||||
|
local.properties
|
||||||
|
**/.cxx/
|
||||||
|
|
||||||
|
# iOS/XCode related
|
||||||
|
**/ios/**/*.mode1v3
|
||||||
|
**/ios/**/*.mode2v3
|
||||||
|
**/ios/**/*.moved-aside
|
||||||
|
**/ios/**/*.pbxuser
|
||||||
|
**/ios/**/*.perspectivev3
|
||||||
|
**/ios/**/*sync/
|
||||||
|
**/ios/**/.sconsign.dblite
|
||||||
|
**/ios/**/.tags*
|
||||||
|
**/ios/**/.vagrant/
|
||||||
|
**/ios/**/DerivedData/
|
||||||
|
**/ios/**/Icon?
|
||||||
|
**/ios/**/Pods/
|
||||||
|
**/ios/**/.symlinks/
|
||||||
|
**/ios/**/profile
|
||||||
|
**/ios/**/xcuserdata
|
||||||
|
**/ios/.generated/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
**/ios/Flutter/App.framework
|
||||||
|
**/ios/Flutter/Flutter.framework
|
||||||
|
**/ios/Flutter/Flutter.podspec
|
||||||
|
**/ios/Flutter/Generated.xcconfig
|
||||||
|
**/ios/Flutter/ephemeral
|
||||||
|
**/ios/Flutter/app.flx
|
||||||
|
**/ios/Flutter/app.zip
|
||||||
|
**/ios/Flutter/flutter_assets/
|
||||||
|
**/ios/Flutter/flutter_export_environment.sh
|
||||||
|
**/ios/ServiceDefinitions.json
|
||||||
|
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
**/Flutter/ephemeral/
|
||||||
|
**/Pods/
|
||||||
|
**/macos/Flutter/GeneratedPluginRegistrant.swift
|
||||||
|
**/macos/Flutter/ephemeral
|
||||||
|
**/xcuserdata/
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
**/windows/flutter/ephemeral/
|
||||||
|
**/windows/flutter/generated_plugin_registrant.cc
|
||||||
|
**/windows/flutter/generated_plugin_registrant.h
|
||||||
|
**/windows/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
**/linux/flutter/ephemeral/
|
||||||
|
**/linux/flutter/generated_plugin_registrant.cc
|
||||||
|
**/linux/flutter/generated_plugin_registrant.h
|
||||||
|
**/linux/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Symbols
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
|
||||||
# Obfuscation related
|
# Exceptions to above rules.
|
||||||
app.*.map.json
|
!**/ios/**/default.mode1v3
|
||||||
|
!**/ios/**/default.mode2v3
|
||||||
|
!**/ios/**/default.pbxuser
|
||||||
|
!**/ios/**/default.perspectivev3
|
||||||
|
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||||
|
!/dev/ci/**/Gemfile.lock
|
||||||
|
!.vscode/settings.json
|
||||||
|
|
||||||
# Android Studio will place build artifacts here
|
# Monorepo
|
||||||
/android/app/debug
|
.cipd
|
||||||
/android/app/profile
|
.gclient
|
||||||
/android/app/release
|
.gclient_entries
|
||||||
|
.python-version
|
||||||
|
.gclient_previous_custom_vars
|
||||||
|
.gclient_previous_sync_commits
|
||||||
@ -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}"
|
||||||
|
|||||||
@ -18,7 +18,7 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.1.0" apply false
|
id "com.android.application" version "8.2.1" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 970 KiB After Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@ -10,12 +10,12 @@ class LoginController extends MyController {
|
|||||||
|
|
||||||
bool showPassword = false, isChecked = false;
|
bool showPassword = false, isChecked = false;
|
||||||
|
|
||||||
final String _dummyEmail = "demo@example.com";
|
final String _dummyEmail = "admin@marcobms.com";
|
||||||
final String _dummyPassword = "1234567";
|
final String _dummyPassword = "User@123";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
basicValidator.addField('email', required: true, label: "Email", validators: [MyEmailValidator()], controller: TextEditingController(text: _dummyEmail));
|
basicValidator.addField('username', required: true, label: "User_Name", validators: [MyEmailValidator()], controller: TextEditingController(text: _dummyEmail));
|
||||||
|
|
||||||
basicValidator.addField('password',
|
basicValidator.addField('password',
|
||||||
required: true, label: "Password", validators: [MyLengthValidator(min: 6, max: 10)], controller: TextEditingController(text: _dummyPassword));
|
required: true, label: "Password", validators: [MyLengthValidator(min: 6, max: 10)], controller: TextEditingController(text: _dummyPassword));
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
import 'package:marco/controller/my_controller.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
|
||||||
import 'package:marco/model/chart_model.dart';
|
|
||||||
import 'package:marco/model/job_recent_application_model.dart';
|
|
||||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
|
||||||
|
|
||||||
class AttendanceController extends MyController {
|
|
||||||
int isSelectedListingPerformanceTime = 0;
|
|
||||||
List<ChartSampleData>? chartData;
|
|
||||||
TooltipBehavior? columnToolTip;
|
|
||||||
List<JobRecentApplicationModel> recentApplication = [];
|
|
||||||
List<String> dummyTexts = List.generate(12, (index) => MyTextUtils.getDummyText(60));
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
chartData = <ChartSampleData>[
|
|
||||||
ChartSampleData(x: 'Jan', y: 4, secondSeriesYValue: 8),
|
|
||||||
ChartSampleData(x: 'Feb', y: 9, secondSeriesYValue: 7),
|
|
||||||
ChartSampleData(x: 'Mar', y: 6, secondSeriesYValue: 5),
|
|
||||||
ChartSampleData(x: 'Apr', y: 8, secondSeriesYValue: 3),
|
|
||||||
ChartSampleData(x: 'May', y: 7, secondSeriesYValue: 9),
|
|
||||||
ChartSampleData(x: 'Jun', y: 10, secondSeriesYValue: 6),
|
|
||||||
ChartSampleData(x: 'Jul', y: 5, secondSeriesYValue: 4),
|
|
||||||
ChartSampleData(x: 'Aug', y: 3, secondSeriesYValue: 2),
|
|
||||||
ChartSampleData(x: 'Sep', y: 6, secondSeriesYValue: 10),
|
|
||||||
ChartSampleData(x: 'Oct', y: 4, secondSeriesYValue: 8),
|
|
||||||
ChartSampleData(x: 'Nov', y: 9, secondSeriesYValue: 6),
|
|
||||||
ChartSampleData(x: 'Dec', y: 7, secondSeriesYValue: 5),
|
|
||||||
];
|
|
||||||
columnToolTip = TooltipBehavior(enable: true);
|
|
||||||
JobRecentApplicationModel.dummyList.then((value) {
|
|
||||||
recentApplication = value.sublist(0, 5);
|
|
||||||
update();
|
|
||||||
});
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void onSelectListingPerformanceTimeToggle(index) {
|
|
||||||
isSelectedListingPerformanceTime = index;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
110
lib/controller/dashboard/attendance_screen_controller.dart
Normal file
110
lib/controller/dashboard/attendance_screen_controller.dart
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
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/AttendanceLogModel.dart';
|
||||||
|
|
||||||
|
class AttendanceController extends GetxController {
|
||||||
|
List<AttendanceModel> attendances = [];
|
||||||
|
List<ProjectModel> projects = []; // List of projects
|
||||||
|
String? selectedProjectId; // Currently selected project ID
|
||||||
|
List<EmployeeModel> employees = []; // Employees of the selected project
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchProjects(); // Fetch projects when initializing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch projects from 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();
|
||||||
|
|
||||||
|
// 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch employees by project ID
|
||||||
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
|
if (projectId == null) return;
|
||||||
|
|
||||||
|
var response = await ApiService.getEmployeesByProject(int.parse(projectId));
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
employees = response
|
||||||
|
.map<EmployeeModel>((json) => EmployeeModel.fromJson(json))
|
||||||
|
.toList();
|
||||||
|
update(); // Trigger UI rebuild
|
||||||
|
} else {
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
198
lib/helpers/services/api_service.dart
Normal file
198
lib/helpers/services/api_service.dart
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
// Fetch the list of projects
|
||||||
|
static Future<List<dynamic>?> getProjects() 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/project/list"),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $jwtToken', // Add Authorization header
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
print("Response body: ${response.body}");
|
||||||
|
if (json['success'] == true) {
|
||||||
|
return json['data']; // Return the data if success is true
|
||||||
|
} else {
|
||||||
|
print("Error: ${json['message']}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("Error fetching projects: ${response.statusCode}");
|
||||||
|
print("Response body: ${response.body}");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Exception while fetching projects: $e");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch employees by project ID
|
||||||
|
static Future<List<dynamic>?> getEmployeesByProject(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"), // Ensure correct endpoint
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $jwtToken',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
print("Response body: ${response.body}");
|
||||||
|
if (json['success'] == true) {
|
||||||
|
return json['data']; // Return employee data
|
||||||
|
} else {
|
||||||
|
print("Error: ${json['message']}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("Error fetching employees: ${response.statusCode}");
|
||||||
|
print("Response body: ${response.body}");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Exception while fetching employees: $e");
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,22 +1,50 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/model/user.dart';
|
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
static bool isLoggedIn = false;
|
static bool isLoggedIn = false;
|
||||||
|
|
||||||
static User get dummyUser => User(-1, "demo@example.com", "Denish", "Navadiya");
|
static Future<Map<String, String>?> loginUser(Map<String, dynamic> data) async {
|
||||||
|
try {
|
||||||
static Future<Map<String, String>?> loginUser(
|
final response = await http.post(
|
||||||
Map<String, dynamic> data) async {
|
Uri.parse('https://api.marcoaiot.com/api/auth/login'),
|
||||||
await Future.delayed(Duration(seconds: 1));
|
headers: {'Content-Type': 'application/json'},
|
||||||
if (data['email'] != dummyUser.email) {
|
body: jsonEncode(data),
|
||||||
return {"email": "This email is not registered"};
|
);
|
||||||
} else if (data['password'] != "1234567") {
|
|
||||||
return {"password": "Password is incorrect"};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
isLoggedIn = true;
|
isLoggedIn = true;
|
||||||
|
|
||||||
|
// Parse the response to get the JWT and refresh tokens
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
// Adjusted for the actual response structure
|
||||||
|
final jwtToken = responseData['data']['token']; // Ensure this matches your actual response
|
||||||
|
|
||||||
|
// Save the JWT token in local storage
|
||||||
|
await LocalStorage.setJwtToken(jwtToken);
|
||||||
|
print("JWT Token: $jwtToken");
|
||||||
|
|
||||||
|
// Optionally save refresh token if available
|
||||||
|
final refreshToken = responseData['data']['refreshToken'];
|
||||||
|
if (refreshToken != null) {
|
||||||
|
await LocalStorage.setRefreshToken(refreshToken);
|
||||||
|
print("Refresh Token: $refreshToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the login state in local storage
|
||||||
await LocalStorage.setLoggedInUser(true);
|
await LocalStorage.setLoggedInUser(true);
|
||||||
|
|
||||||
|
// Return null to indicate success
|
||||||
return null;
|
return null;
|
||||||
|
} else if (response.statusCode == 401) {
|
||||||
|
return {"password": "Invalid email or password"};
|
||||||
|
} else {
|
||||||
|
return {"error": "Something went wrong. Please try again."};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {"error": "Network error. Please check your connection."};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,8 @@ class LocalStorage {
|
|||||||
static const String _loggedInUserKey = "user";
|
static const String _loggedInUserKey = "user";
|
||||||
static const String _themeCustomizerKey = "theme_customizer";
|
static const String _themeCustomizerKey = "theme_customizer";
|
||||||
static const String _languageKey = "lang_code";
|
static const String _languageKey = "lang_code";
|
||||||
|
static const String _jwtTokenKey = "jwt_token";
|
||||||
|
static const String _refreshTokenKey = "refresh_token";
|
||||||
|
|
||||||
static SharedPreferences? _preferencesInstance;
|
static SharedPreferences? _preferencesInstance;
|
||||||
|
|
||||||
@ -47,4 +49,34 @@ class LocalStorage {
|
|||||||
static Future<bool> removeLoggedInUser() async {
|
static Future<bool> removeLoggedInUser() async {
|
||||||
return preferences.remove(_loggedInUserKey);
|
return preferences.remove(_loggedInUserKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add methods to handle JWT and Refresh Token
|
||||||
|
static Future<bool> setToken(String key, String token) {
|
||||||
|
return preferences.setString(key, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? getToken(String key) {
|
||||||
|
return preferences.getString(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> removeToken(String key) {
|
||||||
|
return preferences.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods for getting the JWT and Refresh tokens
|
||||||
|
static String? getJwtToken() {
|
||||||
|
return getToken(_jwtTokenKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? getRefreshToken() {
|
||||||
|
return getToken(_refreshTokenKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> setJwtToken(String jwtToken) {
|
||||||
|
return setToken(_jwtTokenKey, jwtToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> setRefreshToken(String refreshToken) {
|
||||||
|
return setToken(_refreshTokenKey, refreshToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
lib/model/attendance_model.dart
Normal file
37
lib/model/attendance_model.dart
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
class AttendanceModel {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final String projectAddress;
|
||||||
|
final String contactPerson;
|
||||||
|
final DateTime startDate;
|
||||||
|
final DateTime endDate;
|
||||||
|
final int teamSize;
|
||||||
|
final int completedWork;
|
||||||
|
final int plannedWork;
|
||||||
|
|
||||||
|
AttendanceModel({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.projectAddress,
|
||||||
|
required this.contactPerson,
|
||||||
|
required this.startDate,
|
||||||
|
required this.endDate,
|
||||||
|
required this.teamSize,
|
||||||
|
required this.completedWork,
|
||||||
|
required this.plannedWork,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AttendanceModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return AttendanceModel(
|
||||||
|
id: int.tryParse(json['id'].toString()) ?? 0,
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
projectAddress: json['projectAddress'] ?? '',
|
||||||
|
contactPerson: json['contactPerson'] ?? '',
|
||||||
|
startDate: DateTime.tryParse(json['startDate'].toString()) ?? DateTime.now(),
|
||||||
|
endDate: DateTime.tryParse(json['endDate'].toString()) ?? DateTime.now(),
|
||||||
|
teamSize: int.tryParse(json['teamSize'].toString()) ?? 0,
|
||||||
|
completedWork: int.tryParse(json['completedWork'].toString()) ?? 0,
|
||||||
|
plannedWork: int.tryParse(json['plannedWork'].toString()) ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
lib/model/employee_model.dart
Normal file
28
lib/model/employee_model.dart
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
class EmployeeModel {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final String designation;
|
||||||
|
final String checkIn;
|
||||||
|
final String checkOut;
|
||||||
|
final int actions;
|
||||||
|
|
||||||
|
EmployeeModel({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.designation,
|
||||||
|
required this.checkIn,
|
||||||
|
required this.checkOut,
|
||||||
|
required this.actions,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory EmployeeModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return EmployeeModel(
|
||||||
|
id: json['employeeId'] ?? 0,
|
||||||
|
name: '${json['firstName']} ${json['lastName']}',
|
||||||
|
designation: json['jobRoleName'] ?? '',
|
||||||
|
checkIn: json['checkIn'] ?? '-', // Make sure your API returns this field
|
||||||
|
checkOut: json['checkOut'] ?? '-',
|
||||||
|
actions: json['actions'] ?? 0, // Make sure your API returns this field
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
lib/model/project_model.dart
Normal file
62
lib/model/project_model.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
class ProjectModel {
|
||||||
|
final int id; // Unique identifier for the project
|
||||||
|
final String name; // Name of the project
|
||||||
|
final String projectAddress; // Address of the project
|
||||||
|
final String contactPerson; // Contact person for the project
|
||||||
|
final DateTime startDate; // Start date of the project
|
||||||
|
final DateTime endDate; // End date of the project
|
||||||
|
final int teamSize; // Number of people in the team
|
||||||
|
final int completedWork; // Completed work percentage
|
||||||
|
final int plannedWork; // Planned work for the project
|
||||||
|
final int projectStatusId; // Status ID for the project
|
||||||
|
final int tenantId; // Tenant ID associated with the project
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
ProjectModel({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.projectAddress,
|
||||||
|
required this.contactPerson,
|
||||||
|
required this.startDate,
|
||||||
|
required this.endDate,
|
||||||
|
required this.teamSize,
|
||||||
|
required this.completedWork,
|
||||||
|
required this.plannedWork,
|
||||||
|
required this.projectStatusId,
|
||||||
|
required this.tenantId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Factory method to create an instance of ProjectModel from a JSON object
|
||||||
|
factory ProjectModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ProjectModel(
|
||||||
|
id: json['id'],
|
||||||
|
name: json['name'],
|
||||||
|
projectAddress: json['projectAddress'],
|
||||||
|
contactPerson: json['contactPerson'],
|
||||||
|
startDate: DateTime.parse(json['startDate']),
|
||||||
|
endDate: DateTime.parse(json['endDate']),
|
||||||
|
teamSize: json['teamSize'],
|
||||||
|
completedWork: json['completedWork'],
|
||||||
|
plannedWork: json['plannedWork'],
|
||||||
|
projectStatusId: json['projectStatusId'],
|
||||||
|
tenantId: json['tenantId'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to convert the ProjectModel instance back to a JSON object
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'projectAddress': projectAddress,
|
||||||
|
'contactPerson': contactPerson,
|
||||||
|
'startDate': startDate.toIso8601String(),
|
||||||
|
'endDate': endDate.toIso8601String(),
|
||||||
|
'teamSize': teamSize,
|
||||||
|
'completedWork': completedWork,
|
||||||
|
'plannedWork': plannedWork,
|
||||||
|
'projectStatusId': projectStatusId,
|
||||||
|
'tenantId': tenantId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:marco/model/identifier_model.dart';
|
import 'package:marco/model/identifier_model.dart';
|
||||||
|
|
||||||
class User extends IdentifierModel {
|
class User extends IdentifierModel {
|
||||||
final String email, firstName, lastName;
|
final String username, firstName, lastName;
|
||||||
|
|
||||||
User(super.id, this.email, this.firstName, this.lastName);
|
User(super.id, this.username, this.firstName, this.lastName);
|
||||||
|
|
||||||
String get name => "$firstName $lastName";
|
String get name => "$firstName $lastName";
|
||||||
|
|
||||||
|
|||||||
11
lib/res/assets_res.dart
Normal file
11
lib/res/assets_res.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Generated file. Do not edit.
|
||||||
|
// This file is generated by the iFlutter
|
||||||
|
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
class AssetsRes {
|
||||||
|
AssetsRes._();
|
||||||
|
|
||||||
|
static const String PROJECT_NAME = 'marco';
|
||||||
|
static const String PROJECT_VERSION = '1.0.0+1';
|
||||||
|
}
|
||||||
@ -5,12 +5,12 @@ 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';
|
||||||
import 'package:marco/view/dashboard/attendance_screen.dart';
|
// import 'package:marco/view/dashboard/attendance_screen.dart';
|
||||||
|
import 'package:marco/view/dashboard/attendanceScreen.dart';
|
||||||
class AuthMiddleware extends GetMiddleware {
|
class AuthMiddleware extends GetMiddleware {
|
||||||
@override
|
@override
|
||||||
RouteSettings? redirect(String? route) {
|
RouteSettings? redirect(String? route) {
|
||||||
@ -20,10 +20,10 @@ 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: () => const AttendanceScreen(), middlewares: [AuthMiddleware()]),
|
GetPage(name: '/dashboard/attendance', page: () => AttendanceScreen(), middlewares: [AuthMiddleware()]),
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
GetPage(name: '/auth/login', page: () => LoginScreen()),
|
GetPage(name: '/auth/login', page: () => LoginScreen()),
|
||||||
|
|||||||
@ -9,6 +9,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/helpers/widgets/my_text_style.dart';
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
import 'package:marco/view/layouts/auth_layout.dart';
|
import 'package:marco/view/layouts/auth_layout.dart';
|
||||||
|
import 'package:marco/images.dart';
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({super.key});
|
const LoginScreen({super.key});
|
||||||
@ -29,76 +30,139 @@ class _LoginScreenState extends State<LoginScreen> with UIMixin{
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AuthLayout(
|
return AuthLayout(
|
||||||
child: GetBuilder(
|
child: GetBuilder<LoginController>(
|
||||||
init: controller,
|
init: controller,
|
||||||
tag: 'login_controller',
|
tag: 'login_controller',
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
return Form(
|
return Form(
|
||||||
key: controller.basicValidator.formKey,
|
key: controller.basicValidator.formKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: MySpacing.xy(2, 40),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: MySpacing.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary.withOpacity(0.02),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: contentTheme.primary.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
MyText.titleLarge("Sign in with email", fontWeight: 600),
|
/// Logo
|
||||||
MySpacing.height(12),
|
Center(
|
||||||
MyText.bodyMedium("Make a new doc to bring your words, data and terms together. For free", fontWeight: 600, xMuted: true),
|
child: Image.asset(
|
||||||
MySpacing.height(12),
|
Images.logoDark,
|
||||||
TextFormField(
|
height: 120,
|
||||||
validator: controller.basicValidator.getValidation('email'),
|
fit: BoxFit.contain,
|
||||||
controller: controller.basicValidator.getController('email'),
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(20),
|
||||||
|
|
||||||
|
/// Welcome Text
|
||||||
|
Center(
|
||||||
|
child: MyText.bodyLarge("Welcome Back!", fontWeight: 600),
|
||||||
|
),
|
||||||
|
MySpacing.height(4),
|
||||||
|
Center(
|
||||||
|
child: MyText.bodySmall("Please sign in to continue."),
|
||||||
|
),
|
||||||
|
MySpacing.height(20),
|
||||||
|
|
||||||
|
/// Email Field
|
||||||
|
MyText.bodySmall("Email Address", fontWeight: 600),
|
||||||
|
MySpacing.height(8),
|
||||||
|
Material(
|
||||||
|
elevation: 2,
|
||||||
|
shadowColor: contentTheme.secondary.withAlpha(30),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: TextFormField(
|
||||||
|
validator:
|
||||||
|
controller.basicValidator.getValidation('username'),
|
||||||
|
controller:
|
||||||
|
controller.basicValidator.getController('username'),
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
style: MyTextStyle.labelMedium(),
|
style: MyTextStyle.labelMedium(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Email Address",
|
hintText: "Enter your email",
|
||||||
labelStyle: MyTextStyle.bodySmall(xMuted: true),
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||||
border: OutlineInputBorder(borderSide: BorderSide.none),
|
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: contentTheme.secondary.withAlpha(36),
|
fillColor: theme.cardColor,
|
||||||
prefixIcon: const Icon(LucideIcons.mail, size: 16),
|
border: OutlineInputBorder(
|
||||||
contentPadding: MySpacing.all(14),
|
borderRadius: BorderRadius.circular(2),
|
||||||
isDense: true,
|
borderSide: BorderSide.none,
|
||||||
isCollapsed: true,
|
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never),
|
|
||||||
),
|
),
|
||||||
MySpacing.height(20),
|
prefixIcon: const Icon(LucideIcons.mail, size: 18),
|
||||||
TextFormField(
|
contentPadding: MySpacing.xy(12, 16),
|
||||||
validator: controller.basicValidator.getValidation('password'),
|
),
|
||||||
controller: controller.basicValidator.getController('password'),
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
|
||||||
|
/// Password Field Label
|
||||||
|
MyText.bodySmall("Password", fontWeight: 600),
|
||||||
|
MySpacing.height(8),
|
||||||
|
Material(
|
||||||
|
elevation: 2,
|
||||||
|
shadowColor: contentTheme.secondary.withAlpha(25),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: TextFormField(
|
||||||
|
validator:
|
||||||
|
controller.basicValidator.getValidation('password'),
|
||||||
|
controller:
|
||||||
|
controller.basicValidator.getController('password'),
|
||||||
keyboardType: TextInputType.visiblePassword,
|
keyboardType: TextInputType.visiblePassword,
|
||||||
obscureText: !controller.showPassword,
|
obscureText: !controller.showPassword,
|
||||||
style: MyTextStyle.labelMedium(),
|
style: MyTextStyle.labelMedium(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Password",
|
hintText: "Enter your password",
|
||||||
labelStyle: MyTextStyle.bodySmall(xMuted: true),
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||||
border: OutlineInputBorder(borderSide: BorderSide.none),
|
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: contentTheme.secondary.withAlpha(36),
|
fillColor: theme.cardColor,
|
||||||
prefixIcon: const Icon(LucideIcons.mail, size: 16),
|
border: OutlineInputBorder(
|
||||||
contentPadding: MySpacing.all(16),
|
borderRadius: BorderRadius.circular(2),
|
||||||
isCollapsed: true,
|
borderSide: BorderSide.none,
|
||||||
isDense: true,
|
),
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
prefixIcon: const Icon(LucideIcons.lock, size: 18),
|
||||||
suffixIcon: InkWell(
|
suffixIcon: InkWell(
|
||||||
onTap: controller.onChangeShowPassword,
|
onTap: controller.onChangeShowPassword,
|
||||||
child: Icon(controller.showPassword ? LucideIcons.eye : LucideIcons.eye_off, size: 16),
|
child: Icon(
|
||||||
)),
|
controller.showPassword
|
||||||
|
? LucideIcons.eye
|
||||||
|
: LucideIcons.eye_off,
|
||||||
|
size: 18,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
contentPadding: MySpacing.all(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
MySpacing.height(16),
|
||||||
|
|
||||||
|
/// Remember Me + Forgot Password
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => controller.onChangeCheckBox(!controller.isChecked),
|
onTap: () => controller
|
||||||
|
.onChangeCheckBox(!controller.isChecked),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
onChanged: controller.onChangeCheckBox,
|
onChanged: controller.onChangeCheckBox,
|
||||||
value: controller.isChecked,
|
value: controller.isChecked,
|
||||||
fillColor: WidgetStatePropertyAll(Colors.white),
|
shape: RoundedRectangleBorder(
|
||||||
activeColor: theme.colorScheme.primary,
|
borderRadius: BorderRadius.circular(4),
|
||||||
checkColor: contentTheme.primary,
|
),
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
fillColor: WidgetStatePropertyAll(
|
||||||
|
contentTheme.secondary),
|
||||||
|
checkColor: contentTheme.onPrimary,
|
||||||
visualDensity: getCompactDensity,
|
visualDensity: getCompactDensity,
|
||||||
|
materialTapTargetSize:
|
||||||
|
MaterialTapTargetSize.shrinkWrap,
|
||||||
),
|
),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
MyText.bodySmall("Remember Me"),
|
MyText.bodySmall("Remember Me"),
|
||||||
@ -110,33 +174,54 @@ class _LoginScreenState extends State<LoginScreen> with UIMixin{
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
padding: MySpacing.xy(8, 0),
|
padding: MySpacing.xy(8, 0),
|
||||||
splashColor: contentTheme.secondary.withAlpha(36),
|
splashColor: contentTheme.secondary.withAlpha(36),
|
||||||
child: MyText.bodySmall('Forgot password?', color: contentTheme.secondary),
|
child: MyText.bodySmall(
|
||||||
|
'Forgot password?',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: contentTheme.secondary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
MySpacing.height(28),
|
MySpacing.height(28),
|
||||||
|
|
||||||
|
/// Login Button
|
||||||
Center(
|
Center(
|
||||||
child: MyButton.rounded(
|
child: MyButton.rounded(
|
||||||
onPressed: controller.onLogin,
|
onPressed: controller.onLogin,
|
||||||
elevation: 0,
|
elevation: 2,
|
||||||
padding: MySpacing.xy(20, 16),
|
padding: MySpacing.xy(24, 16),
|
||||||
|
borderRadiusAll: 16,
|
||||||
backgroundColor: contentTheme.primary,
|
backgroundColor: contentTheme.primary,
|
||||||
child: MyText.labelMedium('Login', color: contentTheme.onPrimary),
|
child: MyText.labelMedium(
|
||||||
|
'Login',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: contentTheme.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
|
||||||
|
/// Register Link
|
||||||
Center(
|
Center(
|
||||||
child: MyButton.text(
|
child: MyButton.text(
|
||||||
onPressed: controller.gotoRegister,
|
onPressed: controller.gotoRegister,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
padding: MySpacing.x(16),
|
padding: MySpacing.xy(12, 8),
|
||||||
splashColor: contentTheme.secondary.withValues(alpha:0.1),
|
splashColor: contentTheme.secondary.withAlpha(30),
|
||||||
child: MyText.bodySmall('I haven\'t account'),
|
child: MyText.bodySmall(
|
||||||
|
"Request a Demo",
|
||||||
|
color: contentTheme.secondary,
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},),
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
307
lib/view/dashboard/attendanceScreen.dart
Normal file
307
lib/view/dashboard/attendanceScreen.dart
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:marco/helpers/theme/app_theme.dart';
|
||||||
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||||
|
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||||
|
import 'package:marco/helpers/utils/utils.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_card.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_container.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_list_extension.dart';
|
||||||
|
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});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AttendanceScreen> createState() => _AttendanceScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||||
|
AttendanceController attendanceController = Get.put(AttendanceController());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Layout(
|
||||||
|
child: GetBuilder(
|
||||||
|
init: attendanceController,
|
||||||
|
tag: 'attendance_dashboard_controller',
|
||||||
|
builder: (controller) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: MySpacing.x(flexSpacing),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
MyText.titleMedium("Attendance",
|
||||||
|
fontSize: 18, fontWeight: 600),
|
||||||
|
MyBreadcrumb(
|
||||||
|
children: [
|
||||||
|
MyBreadcrumbItem(name: 'Dashboard'),
|
||||||
|
MyBreadcrumbItem(name: 'Attendance', active: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(flexSpacing),
|
||||||
|
Padding(
|
||||||
|
padding: MySpacing.x(flexSpacing / 2),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MySpacing.height(flexSpacing),
|
||||||
|
MyFlex(
|
||||||
|
children: [
|
||||||
|
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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).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: 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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,435 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/helpers/theme/app_theme.dart';
|
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|
||||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
|
||||||
import 'package:marco/helpers/utils/utils.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_card.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_container.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_list_extension.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/images.dart';
|
|
||||||
import 'package:marco/model/chart_model.dart';
|
|
||||||
import 'package:marco/view/layouts/layout.dart';
|
|
||||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
|
||||||
import 'package:marco/controller/dashboard/attendance_controller.dart';
|
|
||||||
class AttendanceScreen extends StatefulWidget {
|
|
||||||
const AttendanceScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AttendanceScreen> createState() => _AttendanceScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|
||||||
AttendanceController controller = Get.put(AttendanceController());
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Layout(
|
|
||||||
child: GetBuilder(
|
|
||||||
init: controller,
|
|
||||||
tag: 'attendance_dashboard_controller',
|
|
||||||
builder: (controller) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: MySpacing.x(flexSpacing),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
MyText.titleMedium("Attendance", fontSize: 18, fontWeight: 600),
|
|
||||||
MyBreadcrumb(
|
|
||||||
children: [
|
|
||||||
MyBreadcrumbItem(name: 'Dashboard'),
|
|
||||||
MyBreadcrumbItem(name: 'Attendance', active: true),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(flexSpacing),
|
|
||||||
Padding(
|
|
||||||
padding: MySpacing.x(flexSpacing / 2),
|
|
||||||
child: MyFlex(
|
|
||||||
children: [
|
|
||||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.briefcase, '245', 'EMPLOYEES IN SYSTEM', contentTheme.primary)),
|
|
||||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.file_text, '3201', 'CANDIDATES IN DATA', contentTheme.secondary)),
|
|
||||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.map_pin, '56', 'LOCATIONS SERVED', contentTheme.success)),
|
|
||||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.user_plus, '312', 'RECRUITER NETWORK', contentTheme.info)),
|
|
||||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.credit_card, '689', 'ACTIVE SUBSCRIPTIONS', contentTheme.purple)),
|
|
||||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.cloud_upload, '82%', 'RESUME UPLOAD RATE', contentTheme.pink)),
|
|
||||||
MyFlexItem(sizes: 'lg-4', child: workingFormat()),
|
|
||||||
MyFlexItem(sizes: 'lg-8 md-6', child: listingPerformance()),
|
|
||||||
MyFlexItem(sizes: 'lg-4 md-6', child: recentCandidate()),
|
|
||||||
MyFlexItem(sizes: 'lg-4 md-6', child: mostViewedCVs()),
|
|
||||||
MyFlexItem(sizes: 'lg-4 md-6', child: recentChat()),
|
|
||||||
MyFlexItem(child: recentApplication()),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget stats(IconData? icon, String title, String subTitle, Color color) {
|
|
||||||
return MyCard.bordered(
|
|
||||||
borderRadiusAll: 4,
|
|
||||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
|
||||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
|
||||||
paddingAll: 24,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
MyContainer(
|
|
||||||
paddingAll: 12,
|
|
||||||
color: color,
|
|
||||||
child: Icon(icon, color: contentTheme.light, size: 16),
|
|
||||||
),
|
|
||||||
MySpacing.width(16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.titleMedium(title, fontWeight: 600),
|
|
||||||
MySpacing.height(4),
|
|
||||||
MyText.labelSmall(subTitle, xMuted: true, maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget workingFormat() {
|
|
||||||
return MyCard.bordered(
|
|
||||||
borderRadiusAll: 4,
|
|
||||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
|
||||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
|
||||||
paddingAll: 24,
|
|
||||||
height: 408,
|
|
||||||
child: Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
||||||
MyText.bodyMedium("Working Format", height: .8, fontWeight: 600),
|
|
||||||
SfCircularChart(legend: Legend(isVisible: true, position: LegendPosition.bottom, overflowMode: LegendItemOverflowMode.wrap), series: [
|
|
||||||
DoughnutSeries<ChartSampleData, String>(
|
|
||||||
explode: true,
|
|
||||||
dataSource: <ChartSampleData>[
|
|
||||||
ChartSampleData(x: 'OnSite', y: 55, text: '55%'),
|
|
||||||
ChartSampleData(x: 'Remote', y: 31, text: '31%'),
|
|
||||||
ChartSampleData(x: 'Hybrid', y: 7.7, text: '7.7%'),
|
|
||||||
],
|
|
||||||
xValueMapper: (ChartSampleData data, _) => data.x as String,
|
|
||||||
yValueMapper: (ChartSampleData data, _) => data.y,
|
|
||||||
dataLabelMapper: (ChartSampleData data, _) => data.text,
|
|
||||||
dataLabelSettings: DataLabelSettings(isVisible: true))
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget listingPerformance() {
|
|
||||||
Widget isSelectTime(String title, int index) {
|
|
||||||
bool isSelect = controller.isSelectedListingPerformanceTime == index;
|
|
||||||
return MyCard.bordered(
|
|
||||||
borderRadiusAll: 4,
|
|
||||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
|
||||||
shadow: MyShadow(elevation: 0, position: MyShadowPosition.bottom),
|
|
||||||
paddingAll: 4,
|
|
||||||
color: isSelect ? contentTheme.secondary.withValues(alpha:0.15) : null,
|
|
||||||
onTap: () => controller.onSelectListingPerformanceTimeToggle(index),
|
|
||||||
child: MyText.labelSmall(title, fontWeight: 600, color: isSelect ? contentTheme.secondary : null),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return MyCard.bordered(
|
|
||||||
borderRadiusAll: 4,
|
|
||||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
|
||||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
|
||||||
paddingAll: 24,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: MyText.bodyMedium("Listing Performance", fontWeight: 600, overflow: TextOverflow.ellipsis),
|
|
||||||
),
|
|
||||||
isSelectTime("Day", 0),
|
|
||||||
MySpacing.width(12),
|
|
||||||
isSelectTime("Week", 1),
|
|
||||||
MySpacing.width(12),
|
|
||||||
isSelectTime("Month", 2),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(24),
|
|
||||||
SizedBox(
|
|
||||||
height: 310,
|
|
||||||
child: SfCartesianChart(
|
|
||||||
margin: MySpacing.zero,
|
|
||||||
plotAreaBorderWidth: 0,
|
|
||||||
primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)),
|
|
||||||
primaryYAxis: NumericAxis(
|
|
||||||
maximum: 20,
|
|
||||||
minimum: 0,
|
|
||||||
interval: 4,
|
|
||||||
axisLine: AxisLine(width: 0),
|
|
||||||
majorTickLines: MajorTickLines(size: 0),
|
|
||||||
),
|
|
||||||
series: [
|
|
||||||
ColumnSeries<ChartSampleData, String>(
|
|
||||||
width: .7,
|
|
||||||
spacing: .2,
|
|
||||||
dataSource: controller.chartData,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
|
||||||
yValueMapper: (ChartSampleData sales, _) => sales.y,
|
|
||||||
name: 'Views'),
|
|
||||||
ColumnSeries<ChartSampleData, String>(
|
|
||||||
dataSource: controller.chartData,
|
|
||||||
width: .7,
|
|
||||||
spacing: .2,
|
|
||||||
color: theme.colorScheme.secondary,
|
|
||||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
|
||||||
yValueMapper: (ChartSampleData sales, _) => sales.secondSeriesYValue,
|
|
||||||
name: 'Application')
|
|
||||||
],
|
|
||||||
legend: Legend(isVisible: true, position: LegendPosition.bottom),
|
|
||||||
tooltipBehavior: controller.columnToolTip),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget recentCandidate() {
|
|
||||||
Widget candidatesData(String image, title, subtitle) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
MyContainer.rounded(
|
|
||||||
paddingAll: 0,
|
|
||||||
height: 44,
|
|
||||||
width: 44,
|
|
||||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
|
||||||
child: Image.asset(image, fit: BoxFit.cover),
|
|
||||||
),
|
|
||||||
MySpacing.width(12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.bodyMedium(title, fontWeight: 600),
|
|
||||||
MyText.bodySmall(subtitle, fontWeight: 600, xMuted: true, maxLines: 1, overflow: TextOverflow.visible)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return MyCard.bordered(
|
|
||||||
borderRadiusAll: 4,
|
|
||||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
|
||||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
|
||||||
paddingAll: 24,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.bodyMedium("Recent Candidate", fontWeight: 600),
|
|
||||||
MySpacing.height(24),
|
|
||||||
candidatesData(Images.avatars[3], "Sophia Williams", controller.dummyTexts[0]),
|
|
||||||
MySpacing.height(24),
|
|
||||||
candidatesData(Images.avatars[4], "Ethan Johnson", controller.dummyTexts[1]),
|
|
||||||
MySpacing.height(24),
|
|
||||||
candidatesData(Images.avatars[5], "Olivia Martinez", controller.dummyTexts[2]),
|
|
||||||
MySpacing.height(24),
|
|
||||||
candidatesData(Images.avatars[6], "Liam Brown", controller.dummyTexts[3]),
|
|
||||||
MySpacing.height(24),
|
|
||||||
candidatesData(Images.avatars[7], "Ava Davis", controller.dummyTexts[4]),
|
|
||||||
MySpacing.height(24),
|
|
||||||
candidatesData(Images.avatars[8], "Mason Lee", controller.dummyTexts[5]),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget mostViewedCVs() {
|
|
||||||
Widget cv(String title) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
MyContainer.rounded(
|
|
||||||
paddingAll: 0,
|
|
||||||
height: 44,
|
|
||||||
width: 44,
|
|
||||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
|
||||||
color: contentTheme.primary.withAlpha(40),
|
|
||||||
child: Icon(LucideIcons.file_text, color: contentTheme.primary),
|
|
||||||
),
|
|
||||||
MySpacing.width(12),
|
|
||||||
Expanded(child: MyText.bodyMedium(title, fontWeight: 600, overflow: TextOverflow.ellipsis)),
|
|
||||||
InkWell(onTap: () {}, child: Icon(LucideIcons.download))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return MyCard.bordered(
|
|
||||||
borderRadiusAll: 4,
|
|
||||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
|
||||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
|
||||||
paddingAll: 24,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.bodyMedium("Most Viewed CV's", fontWeight: 600),
|
|
||||||
MySpacing.height(24),
|
|
||||||
cv("Isabella Green"),
|
|
||||||
MySpacing.height(24),
|
|
||||||
cv("James Turner"),
|
|
||||||
MySpacing.height(24),
|
|
||||||
cv("Charlotte Scott"),
|
|
||||||
MySpacing.height(24),
|
|
||||||
cv("Oliver King"),
|
|
||||||
MySpacing.height(24),
|
|
||||||
cv("Lucas Carter"),
|
|
||||||
MySpacing.height(24),
|
|
||||||
cv("Mia Brooks"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget recentChat() {
|
|
||||||
Widget chat(String image, name, message) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
MyContainer.rounded(
|
|
||||||
paddingAll: 0,
|
|
||||||
height: 44,
|
|
||||||
width: 44,
|
|
||||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
|
||||||
child: Image.asset(image, fit: BoxFit.cover),
|
|
||||||
),
|
|
||||||
MySpacing.width(16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.bodyMedium(name, fontWeight: 600),
|
|
||||||
MyText.labelSmall(message, fontWeight: 600, maxLines: 1, muted: true, overflow: TextOverflow.ellipsis),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
MySpacing.width(28),
|
|
||||||
Icon(LucideIcons.message_square, size: 20)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return MyCard.bordered(
|
|
||||||
borderRadiusAll: 4,
|
|
||||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
|
||||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
|
||||||
paddingAll: 24,
|
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
||||||
MyText.bodyMedium("Recent Chat", fontWeight: 600),
|
|
||||||
MySpacing.height(24),
|
|
||||||
chat(Images.avatars[0], "Sophia", controller.dummyTexts[6]),
|
|
||||||
MySpacing.height(24),
|
|
||||||
chat(Images.avatars[1], "Liam", controller.dummyTexts[5]),
|
|
||||||
MySpacing.height(24),
|
|
||||||
chat(Images.avatars[2], "Charlotte", controller.dummyTexts[4]),
|
|
||||||
MySpacing.height(24),
|
|
||||||
chat(Images.avatars[3], "Oliver", controller.dummyTexts[3]),
|
|
||||||
MySpacing.height(24),
|
|
||||||
chat(Images.avatars[4], "Amelia", controller.dummyTexts[2]),
|
|
||||||
MySpacing.height(24),
|
|
||||||
chat(Images.avatars[5], "James", controller.dummyTexts[1])
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget recentApplication() {
|
|
||||||
return MyCard.bordered(
|
|
||||||
borderRadiusAll: 4,
|
|
||||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
|
||||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
|
||||||
paddingAll: 24,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.bodyMedium("Recent Application", fontWeight: 600),
|
|
||||||
MySpacing.height(24),
|
|
||||||
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: .4, color: Colors.grey),
|
|
||||||
columns: [
|
|
||||||
DataColumn(label: MyText.labelLarge('S.No', color: contentTheme.primary)),
|
|
||||||
DataColumn(label: MyText.labelLarge('Candidate', color: contentTheme.primary)),
|
|
||||||
DataColumn(label: MyText.labelLarge('Category', color: contentTheme.primary)),
|
|
||||||
DataColumn(label: MyText.labelLarge('Designation', color: contentTheme.primary)),
|
|
||||||
DataColumn(label: MyText.labelLarge('Mail', color: contentTheme.primary)),
|
|
||||||
DataColumn(label: MyText.labelLarge('Location', color: contentTheme.primary)),
|
|
||||||
DataColumn(label: MyText.labelLarge('Date', color: contentTheme.primary)),
|
|
||||||
DataColumn(label: MyText.labelLarge('Type', color: contentTheme.primary)),
|
|
||||||
DataColumn(label: MyText.labelLarge('Action', color: contentTheme.primary)),
|
|
||||||
],
|
|
||||||
rows: controller.recentApplication
|
|
||||||
.mapIndexed((index, data) => DataRow(cells: [
|
|
||||||
DataCell(MyText.bodyMedium("#${data.id}", fontWeight: 600)),
|
|
||||||
DataCell(Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
MyContainer(
|
|
||||||
height: 40,
|
|
||||||
width: 40,
|
|
||||||
paddingAll: 0,
|
|
||||||
child: Image.asset(Images.avatars[index % Images.avatars.length], fit: BoxFit.cover),
|
|
||||||
),
|
|
||||||
MySpacing.width(24),
|
|
||||||
MyText.labelMedium(data.candidate, fontWeight: 600)
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
DataCell(MyText.labelMedium(data.category, fontWeight: 600)),
|
|
||||||
DataCell(MyText.labelMedium(data.designation, fontWeight: 600)),
|
|
||||||
DataCell(MyText.labelMedium(data.mail, fontWeight: 600)),
|
|
||||||
DataCell(MyText.labelMedium(data.location, fontWeight: 600)),
|
|
||||||
DataCell(MyText.labelMedium("${Utils.getDateStringFromDateTime(data.date)}", fontWeight: 600)),
|
|
||||||
DataCell(MyText.labelMedium(data.type, fontWeight: 600)),
|
|
||||||
DataCell(Row(
|
|
||||||
children: [
|
|
||||||
MyContainer(
|
|
||||||
onTap: () {},
|
|
||||||
color: contentTheme.primary,
|
|
||||||
paddingAll: 8,
|
|
||||||
child: Icon(LucideIcons.download, size: 16, color: contentTheme.onPrimary),
|
|
||||||
),
|
|
||||||
MySpacing.width(12),
|
|
||||||
MyContainer(
|
|
||||||
onTap: () {},
|
|
||||||
color: contentTheme.secondary,
|
|
||||||
paddingAll: 8,
|
|
||||||
child: Icon(LucideIcons.pencil, size: 16, color: contentTheme.onPrimary),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
))
|
|
||||||
]))
|
|
||||||
.toList()),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,12 +5,18 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import file_picker
|
||||||
|
import file_selector_macos
|
||||||
|
import geolocator_apple
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import quill_native_bridge_macos
|
import quill_native_bridge_macos
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
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"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin"))
|
QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
|||||||
248
pubspec.lock
248
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:
|
||||||
@ -157,10 +165,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "89500471922dd3a89ab0d6e13ab4a2268c25474bff4ca7c628f55c76e0ced1de"
|
sha256: cacfdc5abe93e64d418caa9256eef663499ad791bb688d9fd12c85a311968fba
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.5"
|
version: "8.3.2"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
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:
|
||||||
@ -185,6 +201,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+3"
|
version: "0.9.3+3"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -293,6 +317,54 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
geolocator:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: geolocator
|
||||||
|
sha256: "5c23f3613f50586c0bbb2b8f970240ae66b3bd992088cf60dd5ee2e6f7dde3a8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.0.2"
|
||||||
|
geolocator_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_android
|
||||||
|
sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.6.2"
|
||||||
|
geolocator_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_apple
|
||||||
|
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.13"
|
||||||
|
geolocator_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_platform_interface
|
||||||
|
sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.6"
|
||||||
|
geolocator_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_web
|
||||||
|
sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
geolocator_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_windows
|
||||||
|
sha256: "4f4218f122a6978d0ad655fa3541eea74c67417440b09f0657238810d5af6bdc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3"
|
||||||
get:
|
get:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -366,7 +438,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.5"
|
version: "0.15.5"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
||||||
@ -381,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:
|
||||||
@ -461,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:
|
||||||
@ -525,6 +677,62 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.4.0"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.1.0"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.7"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3+5"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||||
|
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:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -541,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:
|
||||||
@ -706,6 +922,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.1"
|
||||||
|
sprintf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sprintf
|
||||||
|
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -898,6 +1122,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.0"
|
version: "0.3.0"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.1"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -938,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"
|
||||||
|
|||||||
@ -58,6 +58,11 @@ dependencies:
|
|||||||
appflowy_board: ^0.1.2
|
appflowy_board: ^0.1.2
|
||||||
syncfusion_flutter_calendar: ^28.2.6
|
syncfusion_flutter_calendar: ^28.2.6
|
||||||
syncfusion_flutter_maps: ^28.1.33
|
syncfusion_flutter_maps: ^28.1.33
|
||||||
|
http: ^1.2.2
|
||||||
|
geolocator: ^9.0.1
|
||||||
|
permission_handler: ^11.3.0
|
||||||
|
image: ^4.0.17
|
||||||
|
image_picker: ^1.0.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@ -7,11 +7,17 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
#include <geolocator_windows/geolocator_windows.h>
|
||||||
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
GeolocatorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||||
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
|
geolocator_windows
|
||||||
|
permission_handler_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user