Compare commits

..

No commits in common. "main" and "Vaibhav_Task-#179" have entirely different histories.

319 changed files with 207599 additions and 43922 deletions

View File

@ -3,84 +3,42 @@ plugins {
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
id("com.google.gms.google-services")
}
// Load keystore properties from key.properties file
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// Define the namespace for your Android application
namespace = "com.marco.aiot"
// Set the compile SDK version based on Flutter's configuration
namespace = "com.example.marco"
compileSdk = flutter.compileSdkVersion
// Set the NDK version based on Flutter's configuration
ndkVersion = flutter.ndkVersion
// Configure Java compatibility options
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
// Enable core library desugaring for Java 8+ APIs
coreLibraryDesugaringEnabled true
}
// Configure Kotlin options for JVM target
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
// Default configuration for your application
defaultConfig {
// Specify your unique Application ID. This identifies your app on Google Play.
applicationId = "com.marco.aiot"
// Set minimum and target SDK versions based on Flutter's configuration
minSdk = 23
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.marco"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
// Set version code and name based on Flutter's configuration (from pubspec.yaml)
versionCode = flutter.versionCode
versionName = flutter.versionName
}
// Define signing configurations for different build types
signingConfigs {
release {
// Reference the key alias from key.properties
keyAlias keystoreProperties['keyAlias']
// Reference the key password from key.properties
keyPassword keystoreProperties['keyPassword']
// Reference the keystore file path from key.properties
storeFile file(keystoreProperties['storeFile'])
// Reference the keystore password from key.properties
storePassword keystoreProperties['storePassword']
}
}
// Define different build types (e.g., debug, release)
buildTypes {
release {
// Apply the 'release' signing configuration defined above to the release build
signingConfig signingConfigs.release
// Enable code minification to reduce app size
minifyEnabled true
// Enable resource shrinking to remove unused resources
shrinkResources true
// Other release specific configurations can be added here, e.g., ProGuard rules
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
// Configure Flutter specific settings, pointing to the root of your Flutter project
flutter {
source = "../.."
}
// Add required dependencies for desugaring
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
}

View File

@ -1,29 +0,0 @@
{
"project_info": {
"project_number": "626581282477",
"project_id": "mtest-a0635",
"storage_bucket": "mtest-a0635.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024",
"android_client_info": {
"package_name": "com.marco.aiot"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCBkDQRpbSdR0bo6pO4Bm0ZIdXkdaE3z-A"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@ -6,6 +6,5 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
</manifest>

View File

@ -1,14 +1,9 @@
<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" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:label="Marco"
android:label="marco"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@ -38,9 +33,6 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="high_importance_channel"/>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

View File

@ -1,4 +1,4 @@
package com.marco.aiot
package com.example.marco
import io.flutter.embedding.android.FlutterActivity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=false
android.enableJetifier=true

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip

View File

@ -18,9 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.6.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("com.google.gms.google-services") version "4.4.2" apply false
}
include ":app"

95737
assets/data/australia.json Normal file

File diff suppressed because it is too large Load Diff

192
assets/data/chat.json Normal file
View File

@ -0,0 +1,192 @@
[
{
"id": 1,
"first_name": "James Carter",
"email": "james.carter@example.com",
"messages": [
{
"message": "How is your day going?",
"send_at": "2024-11-15T10:05:10Z",
"from_me": false
},
{
"message": "Reminder about the project meeting tomorrow",
"send_at": "2023-06-20T14:23:11Z",
"from_me": true
},
{
"message": "Can we meet today for a quick chat?",
"send_at": "2023-04-19T17:30:08Z",
"from_me": false
},
{
"message": "Yes, all is good. See you tomorrow at 2 PM for the meeting",
"send_at": "2023-03-22T11:09:45Z",
"from_me": false
}
]
},
{
"id": 2,
"first_name": "Sophia Lee",
"email": "sophia.lee@example.com",
"messages": [
{
"message": "Are we meeting today for the weekly catch-up?",
"send_at": "2023-09-10T08:45:36Z",
"from_me": false
},
{
"message": "Please review these updated documents",
"send_at": "2023-11-17T11:22:33Z",
"from_me": true
},
{
"message": "Good morning, How are you? When is our next meeting?",
"send_at": "2023-05-13T09:25:18Z",
"from_me": true
}
]
},
{
"id": 3,
"first_name": "Ethan Scott",
"email": "ethan.scott@example.com",
"messages": [
{
"message": "Are you available for a quick call? Need to discuss something",
"send_at": "2023-12-05T07:40:21Z",
"from_me": false
},
{
"message": "Let's meet today, shall we?",
"send_at": "2023-03-17T14:00:12Z",
"from_me": true
}
]
},
{
"id": 4,
"first_name": "Olivia Brown",
"email": "olivia.brown@example.com",
"messages": [
{
"message": "Let's meet today for a team discussion",
"send_at": "2023-05-30T11:55:10Z",
"from_me": false
},
{
"message": "Hope you're having a great day. Let's catch up soon",
"send_at": "2023-07-12T12:36:44Z",
"from_me": true
},
{
"message": "I need to go buy some groceries this afternoon, I'll be a bit late.",
"send_at": "2024-01-10T13:20:45Z",
"from_me": true
}
]
},
{
"id": 5,
"first_name": "Charlotte Miller",
"email": "charlotte.miller@example.com",
"messages": [
{
"message": "Are you available for a quick chat?",
"send_at": "2023-11-11T16:50:09Z",
"from_me": false
},
{
"message": "I just sent you the updated contract documents for review.",
"send_at": "2023-10-04T18:22:56Z",
"from_me": true
}
]
},
{
"id": 6,
"first_name": "Jackson Harris",
"email": "jackson.harris@example.com",
"messages": [
{
"message": "How's everything going? Any updates on the project?",
"send_at": "2023-08-25T11:10:30Z",
"from_me": false
},
{
"message": "Sending over the latest draft for your review",
"send_at": "2023-12-02T10:14:50Z",
"from_me": true
}
]
},
{
"id": 7,
"first_name": "Aiden Cooper",
"email": "aiden.cooper@example.com",
"messages": [
{
"message": "Do you have time today for a discussion?",
"send_at": "2023-10-05T09:18:22Z",
"from_me": false
},
{
"message": "The new update is ready for deployment, please check it.",
"send_at": "2024-01-25T11:29:13Z",
"from_me": true
}
]
},
{
"id": 8,
"first_name": "Lily King",
"email": "lily.king@example.com",
"messages": [
{
"message": "Would you be able to meet today for a catch-up?",
"send_at": "2023-06-18T13:12:09Z",
"from_me": false
},
{
"message": "I have finished reviewing the files, please take a look.",
"send_at": "2023-12-18T16:47:02Z",
"from_me": true
}
]
},
{
"id": 9,
"first_name": "Max Taylor",
"email": "max.taylor@example.com",
"messages": [
{
"message": "Please check the attached file and confirm if everything is okay.",
"send_at": "2023-09-29T14:10:33Z",
"from_me": false
},
{
"message": "Sending over the revised schedule for the next phase.",
"send_at": "2024-01-02T17:12:11Z",
"from_me": true
}
]
},
{
"id": 10,
"first_name": "Avery Clark",
"email": "avery.clark@example.com",
"messages": [
{
"message": "Are we ready for the meeting today?",
"send_at": "2023-08-22T12:43:50Z",
"from_me": false
},
{
"message": "I updated the timeline. Let me know if you have any questions.",
"send_at": "2023-12-15T10:55:29Z",
"from_me": true
}
]
}
]

View File

@ -0,0 +1,82 @@
[
{
"id": 1,
"asset": "Alaska Air Group, Inc.",
"date": "2024-06-17T12:59:41Z",
"ip_address": "113.9.18.110",
"status": "Unpaid",
"amount": 7061
},
{
"id": 2,
"asset": "T2 Biosystems, Inc.",
"date": "2024-09-09T00:20:08Z",
"ip_address": "45.51.68.143",
"status": "Unpaid",
"amount": 5677
},
{
"id": 3,
"asset": "North American Energy Partners, Inc.",
"date": "2024-07-17T10:46:42Z",
"ip_address": "221.131.122.193",
"status": "Unpaid",
"amount": 5420
},
{
"id": 4,
"asset": "Finjan Holdings, Inc.",
"date": "2024-01-20T20:10:26Z",
"ip_address": "50.242.43.22",
"status": "Success",
"amount": 6433
},
{
"id": 5,
"asset": "Omega Healthcare Investors, Inc.",
"date": "2024-11-14T23:08:09Z",
"ip_address": "109.125.5.131",
"status": "Success",
"amount": 6317
},
{
"id": 6,
"asset": "MediciNova, Inc.",
"date": "2024-01-12T18:20:33Z",
"ip_address": "54.103.156.190",
"status": "Unpaid",
"amount": 7952
},
{
"id": 7,
"asset": "PowerShares LadderRite 0-5 Year Corporate Bond Portfolio",
"date": "2024-04-26T00:44:42Z",
"ip_address": "169.190.183.205",
"status": "Success",
"amount": 6294
},
{
"id": 8,
"asset": "VelocityShares Daily 2x VIX Medium-Term ETN",
"date": "2024-02-01T12:47:59Z",
"ip_address": "144.189.211.137",
"status": "Success",
"amount": 4419
},
{
"id": 9,
"asset": "Liberty TripAdvisor Holdings, Inc.",
"date": "2023-12-29T14:49:59Z",
"ip_address": "166.41.221.149",
"status": "Unpaid",
"amount": 4195
},
{
"id": 10,
"asset": "Scorpio Tankers Inc.",
"date": "2023-12-02T11:36:44Z",
"ip_address": "27.151.0.226",
"status": "Success",
"amount": 8395
}
]

202
assets/data/customer.json Normal file
View File

@ -0,0 +1,202 @@
[
{
"id": 1,
"first_name": "Sabina",
"last_name": "Brothwood",
"project_name": "Wunsch, DuBuque and Green",
"phone_number": "541-568-8047",
"balance": "31907",
"order_count": 7,
"last_order": "2022-10-07T03:43:16Z"
},
{
"id": 2,
"first_name": "Felic",
"last_name": "Parlor",
"project_name": "Dare LLC",
"phone_number": "866-349-3385",
"balance": "02260",
"order_count": 26,
"last_order": "2023-07-12T07:52:19Z"
},
{
"id": 3,
"first_name": "Marnie",
"last_name": "Kofax",
"project_name": "Von LLC",
"phone_number": "821-779-3766",
"balance": "663",
"order_count": 21,
"last_order": "2022-10-14T21:19:33Z"
},
{
"id": 4,
"first_name": "Tine",
"last_name": "Meron",
"project_name": "Stracke Inc",
"phone_number": "901-149-2915",
"balance": "84",
"order_count": 8,
"last_order": "2023-04-06T12:36:09Z"
},
{
"id": 5,
"first_name": "Shanon",
"last_name": "Ivashchenko",
"project_name": "Satterfield, Schultz and Jones",
"phone_number": "452-728-1072",
"balance": "0878",
"order_count": 34,
"last_order": "2023-04-03T15:07:21Z"
},
{
"id": 6,
"first_name": "Guthrey",
"last_name": "Crossland",
"project_name": "Medhurst and Sons",
"phone_number": "212-991-7314",
"balance": "0291",
"order_count": 7,
"last_order": "2022-12-03T04:24:53Z"
},
{
"id": 7,
"first_name": "Florie",
"last_name": "Chestnutt",
"project_name": "Beer-Kunze",
"phone_number": "935-525-9749",
"balance": "07984",
"order_count": 69,
"last_order": "2023-01-14T10:42:28Z"
},
{
"id": 8,
"first_name": "Wittie",
"last_name": "Damsell",
"project_name": "Daniel, Legros and Roberts",
"phone_number": "632-787-4799",
"balance": "22844",
"order_count": 41,
"last_order": "2023-01-18T09:38:50Z"
},
{
"id": 9,
"first_name": "Aimee",
"last_name": "Dibdall",
"project_name": "Schuster LLC",
"phone_number": "404-339-9261",
"balance": "460",
"order_count": 41,
"last_order": "2023-04-15T03:08:51Z"
},
{
"id": 10,
"first_name": "Inna",
"last_name": "Juggins",
"project_name": "Johnson Group",
"phone_number": "769-573-9516",
"balance": "77",
"order_count": 18,
"last_order": "2022-09-13T05:14:51Z"
},
{
"id": 11,
"first_name": "Cathyleen",
"last_name": "Went",
"project_name": "DuBuque LLC",
"phone_number": "558-736-4450",
"balance": "24",
"order_count": 98,
"last_order": "2023-07-05T05:26:12Z"
},
{
"id": 12,
"first_name": "Kora",
"last_name": "Dowderswell",
"project_name": "Harber, Daugherty and West",
"phone_number": "721-147-2917",
"balance": "32",
"order_count": 5,
"last_order": "2022-10-22T07:47:42Z"
},
{
"id": 13,
"first_name": "Loni",
"last_name": "Armin",
"project_name": "Fadel-Kerluke",
"phone_number": "251-582-9867",
"balance": "2122",
"order_count": 4,
"last_order": "2023-01-26T19:56:37Z"
},
{
"id": 14,
"first_name": "Kalle",
"last_name": "Spybey",
"project_name": "Kshlerin, Torp and Koelpin",
"phone_number": "245-661-6328",
"balance": "61034",
"order_count": 70,
"last_order": "2022-12-29T15:38:20Z"
},
{
"id": 15,
"first_name": "Verena",
"last_name": "Skerme",
"project_name": "Dach, Abshire and Crooks",
"phone_number": "227-694-0272",
"balance": "68921",
"order_count": 3,
"last_order": "2022-11-29T23:02:11Z"
},
{
"id": 16,
"first_name": "Lisle",
"last_name": "McGowan",
"project_name": "White, Murphy and Sawayn",
"phone_number": "196-817-6277",
"balance": "7250",
"order_count": 34,
"last_order": "2023-06-14T11:10:56Z"
},
{
"id": 17,
"first_name": "Bryce",
"last_name": "Pires",
"project_name": "Crooks Group",
"phone_number": "424-217-0372",
"balance": "549",
"order_count": 50,
"last_order": "2023-01-08T17:58:09Z"
},
{
"id": 18,
"first_name": "Ibrahim",
"last_name": "Battram",
"project_name": "Schmidt, Feil and Schaden",
"phone_number": "836-473-5900",
"balance": "3",
"order_count": 86,
"last_order": "2023-08-05T01:46:22Z"
},
{
"id": 19,
"first_name": "Josepha",
"last_name": "Grishkov",
"project_name": "Welch-Wisozk",
"phone_number": "928-393-5306",
"balance": "528",
"order_count": 38,
"last_order": "2023-08-18T19:01:25Z"
},
{
"id": 20,
"first_name": "Ellis",
"last_name": "Barfoot",
"project_name": "Davis, Ondricka and Schaefer",
"phone_number": "169-236-9311",
"balance": "169",
"order_count": 11,
"last_order": "2023-02-21T16:29:59Z"
}
]

View File

@ -0,0 +1,72 @@
[
{
"id": 1,
"image": "assets/dummy/dummy_1.jpg",
"name": "Meir O'Leahy",
"user_name": "moleahy0",
"contact_number": "817-666-8080"
},
{
"id": 2,
"image": "assets/dummy/dummy_2.jpg",
"name": "Ernie Ayling",
"user_name": "eayling1",
"contact_number": "890-910-3243"
},
{
"id": 3,
"image": "assets/dummy/dummy_3.jpg",
"name": "Mead Ezzle",
"user_name": "mezzle2",
"contact_number": "293-162-4468"
},
{
"id": 4,
"image": "assets/dummy/dummy_4.jpg",
"name": "Esta Norewood",
"user_name": "enorewood3",
"contact_number": "532-164-0604"
},
{
"id": 5,
"image": "assets/dummy/dummy_5.jpg",
"name": "Bartram Cottell",
"user_name": "bcottell4",
"contact_number": "940-143-2842"
},
{
"id": 6,
"image": "assets/dummy/dummy_1.jpg",
"name": "Nicola Reolfo",
"user_name": "nreolfo5",
"contact_number": "356-558-8324"
},
{
"id": 7,
"image": "assets/dummy/dummy_2.jpg",
"name": "Normy Gilhoolie",
"user_name": "ngilhoolie6",
"contact_number": "256-770-5288"
},
{
"id": 8,
"image": "assets/dummy/dummy_3.jpg",
"name": "Octavia Margerrison",
"user_name": "omargerrison7",
"contact_number": "744-595-1968"
},
{
"id": 9,
"image": "assets/dummy/dummy_4.jpg",
"name": "Stella Barriball",
"user_name": "sbarriball8",
"contact_number": "906-522-1874"
},
{
"id": 10,
"image": "assets/dummy/dummy_5.jpg",
"name": "Panchito Chase",
"user_name": "pchase9",
"contact_number": "929-922-7735"
}
]

54614
assets/data/europe_map.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,102 @@
[
{
"id": 1,
"candidate": "Patrica",
"category": "Manufacture",
"designation": "Sr.UI Developer",
"mail": "pbeedie0@ustream.tv",
"location": "Pojan",
"date": "2024-08-09T06:03:25Z",
"type": "Freelancer"
},
{
"id": 2,
"candidate": "Angelique",
"category": "Marketing",
"designation": "Team Lead",
"mail": "asamwayes1@fotki.com",
"location": "Jiangluo",
"date": "2023-11-17T10:23:18Z",
"type": "Hybride"
},
{
"id": 3,
"candidate": "Garnet",
"category": "Marketing",
"designation": "Team Lead",
"mail": "gjarrelt2@dailymail.co.uk",
"location": "Wissembourg",
"date": "2024-03-18T15:31:21Z",
"type": "Freelancer"
},
{
"id": 4,
"candidate": "Guglielmo",
"category": "Manufacture",
"designation": "Sales Executive",
"mail": "gcarlone3@ted.com",
"location": "Aoqiao",
"date": "2024-04-08T22:04:13Z",
"type": "Part Time"
},
{
"id": 5,
"candidate": "Reggie",
"category": "Manufacture",
"designation": "Team Lead",
"mail": "rmacieiczyk4@booking.com",
"location": "Insrom",
"date": "2024-01-09T06:29:40Z",
"type": "Freelancer"
},
{
"id": 6,
"candidate": "Florri",
"category": "Manufacture",
"designation": "Sales Executive",
"mail": "fharesign5@yellowbook.com",
"location": "Itambacuri",
"date": "2024-09-05T11:09:36Z",
"type": "Part Time"
},
{
"id": 7,
"candidate": "Annabella",
"category": "Manufacture",
"designation": "Team Lead",
"mail": "aossipenko6@ucoz.com",
"location": "Watthana Nakhon",
"date": "2024-07-27T16:17:23Z",
"type": "Full Time"
},
{
"id": 8,
"candidate": "Arlene",
"category": "Manufacture",
"designation": "Team Lead",
"mail": "agook7@google.com.hk",
"location": "Hayama",
"date": "2024-09-14T13:32:31Z",
"type": "Freelancer"
},
{
"id": 9,
"candidate": "Shurlocke",
"category": "Manufacture",
"designation": "Sales Executive",
"mail": "sgallehawk8@squidoo.com",
"location": "Bel Air Rivière Sèche",
"date": "2024-06-18T00:23:24Z",
"type": "Freelancer"
},
{
"id": 10,
"candidate": "Ricoriki",
"category": "Service",
"designation": "Sales Executive",
"mail": "rgillio9@mapy.cz",
"location": "Tawangsari",
"date": "2024-06-14T19:59:41Z",
"type": "Freelancer"
}
]

View File

@ -0,0 +1,112 @@
[
{
"id": 1,
"first_name": "Bordy",
"email": "bjeffreys0@macromedia.com",
"phone_number": "217-779-9808",
"company_name": "Voonyx",
"status": "Won Lead",
"location": "Presidencia Roque Sáenz Peña",
"date": "2024-06-20T06:26:12Z",
"amount": 52397
},
{
"id": 2,
"first_name": "Collin",
"email": "cgething1@paginegialle.it",
"phone_number": "124-897-0512",
"company_name": "Rhyzio",
"status": "New Lead",
"location": "Nombre de Jesús",
"date": "2023-11-20T12:12:32Z",
"amount": 58203
},
{
"id": 3,
"first_name": "Bear",
"email": "bfowlds2@booking.com",
"phone_number": "391-249-1041",
"company_name": "Blogtag",
"status": "Lost Lead",
"location": "Shani",
"date": "2024-11-09T07:57:49Z",
"amount": 18717
},
{
"id": 4,
"first_name": "Robers",
"email": "raujouanet3@google.cn",
"phone_number": "128-604-5632",
"company_name": "Quire",
"status": "Lost Lead",
"location": "Benghazi",
"date": "2023-12-09T17:33:39Z",
"amount": 11267
},
{
"id": 5,
"first_name": "Shirlene",
"email": "sjoiris4@theglobeandmail.com",
"phone_number": "471-884-5686",
"company_name": "Voonder",
"status": "New Lead",
"location": "Krasnaye",
"date": "2024-01-15T03:06:07Z",
"amount": 66877
},
{
"id": 6,
"first_name": "Erik",
"email": "ebudden5@zdnet.com",
"phone_number": "957-550-9950",
"company_name": "Digitube",
"status": "Won Lead",
"location": "Taouloukoult",
"date": "2024-01-21T03:05:01Z",
"amount": 55766
},
{
"id": 7,
"first_name": "Sabina",
"email": "sdenman6@ning.com",
"phone_number": "612-207-4109",
"company_name": "Kwinu",
"status": "Lost Lead",
"location": "Minneapolis",
"date": "2024-05-19T15:59:28Z",
"amount": 24691
},
{
"id": 8,
"first_name": "Andi",
"email": "aschruyer7@imdb.com",
"phone_number": "410-936-5855",
"company_name": "Photojam",
"status": "Won Lead",
"location": "Masina",
"date": "2024-09-30T18:31:07Z",
"amount": 7228
},
{
"id": 9,
"first_name": "Kathy",
"email": "kstandall8@woothemes.com",
"phone_number": "840-267-7381",
"company_name": "Quinu",
"status": "Won Lead",
"location": "Shashi",
"date": "2024-04-21T18:00:25Z",
"amount": 85726
},
{
"id": 10,
"first_name": "Lenka",
"email": "llennon9@hexun.com",
"phone_number": "962-993-3146",
"company_name": "Skaboo",
"status": "Won Lead",
"location": "Shireet",
"date": "2024-06-19T12:27:05Z",
"amount": 10069
}
]

View File

@ -0,0 +1,197 @@
[
{
"id": 1,
"name": "Mints - Striped Red",
"description": "Laceration of ulnar artery at wrs/hnd lv of unsp arm",
"price": 54,
"stock": 72,
"category": "Scallops 60/80 Iqf",
"order_counts": 10,
"created_at": "2022-07-20T09:52:34Z",
"rating": 2.49,
"rating_count": 42,
"sku": "RCII"
},
{
"id": 2,
"name": "Pasta - Ravioli",
"description": "Oth disp fx of upper end l humer, subs for fx w delay heal",
"price": 35,
"stock": 64,
"category": "Chocolate - Mi - Amere Semi",
"order_counts": 45,
"created_at": "2023-03-25T22:32:10Z",
"rating": 4.66,
"rating_count": 82,
"sku": "RDS.A"
},
{
"id": 3,
"name": "Soup - Campbells Chili",
"description": "Nondisp fx of anterior wall of left acetab, init for opn fx",
"price": 27,
"stock": 21,
"category": "Tomatoes - Cherry, Yellow",
"order_counts": 53,
"created_at": "2022-05-18T15:56:06Z",
"rating": 3.05,
"rating_count": 66,
"sku": "STNG"
},
{
"id": 4,
"name": "Fennel - Seeds",
"description": "Displ spiral fx shaft of ulna, r arm, 7thD",
"price": 124,
"stock": 56,
"category": "Squid - U - 10 Thailand",
"order_counts": 13,
"created_at": "2022-04-21T11:32:39Z",
"rating": 0.59,
"rating_count": 55,
"sku": "FEUZ"
},
{
"id": 5,
"name": "Salt - Celery",
"description": "Interstitial myositis, lower leg",
"price": 25,
"stock": 78,
"category": "Gatorade - Lemon Lime",
"order_counts": 30,
"created_at": "2023-01-01T01:15:44Z",
"rating": 1.42,
"rating_count": 10,
"sku": "VER"
},
{
"id": 6,
"name": "Flour - Chickpea",
"description": "Oth fx upr end unsp rad, 7thJ",
"price": 131,
"stock": 50,
"category": "Sweet Pea Sprouts",
"order_counts": 11,
"created_at": "2023-04-09T04:41:43Z",
"rating": 4.05,
"rating_count": 24,
"sku": "HDS"
},
{
"id": 7,
"name": "Chips - Miss Vickies",
"description": "Contusion of right hip, initial encounter",
"price": 52,
"stock": 63,
"category": "Extract - Almond",
"order_counts": 62,
"created_at": "2022-06-24T05:25:56Z",
"rating": 3.35,
"rating_count": 6,
"sku": "MACQW"
},
{
"id": 8,
"name": "Ice Cream - Super Sandwich",
"description": "Acute post-traumatic headache",
"price": 87,
"stock": 44,
"category": "Bread - Wheat Baguette",
"order_counts": 63,
"created_at": "2022-07-06T05:37:09Z",
"rating": 0.96,
"rating_count": 38,
"sku": "AA"
},
{
"id": 9,
"name": "Alize Gold Passion",
"description": "War op involving explosion of marine weapons, civilian",
"price": 113,
"stock": 72,
"category": "Lid - Translucent, 3.5 And 6 Oz",
"order_counts": 23,
"created_at": "2022-07-09T18:19:37Z",
"rating": 3.25,
"rating_count": 64,
"sku": "EVOK"
},
{
"id": 10,
"name": "Mushrooms - Honey",
"description": "Sltr-haris Type IV physl fx low end l femr, 7thP",
"price": 98,
"stock": 64,
"category": "Lemon Balm - Fresh",
"order_counts": 5,
"created_at": "2022-08-05T22:14:06Z",
"rating": 3.51,
"rating_count": 0,
"sku": "TDG"
},
{
"id": 11,
"name": "Bread Base - Goodhearth",
"description": "Unspecified injury of axillary artery",
"price": 81,
"stock": 56,
"category": "Beef - Bones, Marrow",
"order_counts": 76,
"created_at": "2023-04-22T03:11:41Z",
"rating": 4.07,
"rating_count": 22,
"sku": "GPAC"
},
{
"id": 12,
"name": "Veal - Heart",
"description": "Laceration without foreign body of left buttock, subs encntr",
"price": 93,
"stock": 13,
"category": "Peach - Fresh",
"order_counts": 12,
"created_at": "2023-02-18T16:15:16Z",
"rating": 2.08,
"rating_count": 87,
"sku": "PAH"
},
{
"id": 13,
"name": "Tomatoes - Grape",
"description": "Nondisplaced bicondylar fracture of left tibia",
"price": 132,
"stock": 13,
"category": "Veal - Striploin",
"order_counts": 24,
"created_at": "2022-06-08T20:49:59Z",
"rating": 3.32,
"rating_count": 82,
"sku": "ENZL"
},
{
"id": 14,
"name": "Tomato Paste",
"description": "Abrasion of unspecified part of neck, initial encounter",
"price": 48,
"stock": 24,
"category": "Juice - Tomato, 48 Oz",
"order_counts": 4,
"created_at": "2022-12-03T04:03:52Z",
"rating": 4.11,
"rating_count": 66,
"sku": "LTEA"
},
{
"id": 15,
"name": "Cheese - Roquefort Pappillon",
"description": "Wedge comprsn fx first thor vertebra, init for opn fx",
"price": 15,
"stock": 68,
"category": "Veal - Insides, Grains",
"order_counts": 72,
"created_at": "2022-09-22T06:22:33Z",
"rating": 1.54,
"rating_count": 40,
"sku": "AIF"
}
]

View File

@ -0,0 +1,102 @@
[
{
"order_id": "TWT76911",
"customer_name": "Robinson",
"location": "Lyubokhna",
"order_date": "2023-09-17T00:35:06Z",
"quantity": 6,
"payments": "COD",
"price": 336,
"status": "New"
},
{
"order_id": "TWT23890",
"customer_name": "Claudina",
"location": "Maracha",
"order_date": "2023-09-11T15:01:04Z",
"quantity": 8,
"payments": "American Express",
"price": 428,
"status": "Shopping"
},
{
"order_id": "TWT84616",
"customer_name": "Dewain",
"location": "Fuji",
"order_date": "2023-12-26T02:42:54Z",
"quantity": 6,
"payments": "Credit Card",
"price": 410,
"status": "Shopping"
},
{
"order_id": "TWT66711",
"customer_name": "Margette",
"location": "Chicago",
"order_date": "2024-01-09T18:24:04Z",
"quantity": 10,
"payments": "Paypal",
"price": 268,
"status": "Pending"
},
{
"order_id": "TWT50711",
"customer_name": "Brittany",
"location": "Hakha",
"order_date": "2023-09-28T14:17:02Z",
"quantity": 2,
"payments": "Visa Card",
"price": 229,
"status": "New"
},
{
"order_id": "TWT37588",
"customer_name": "Venus",
"location": "Sioguí Arriba",
"order_date": "2023-10-16T18:22:32Z",
"quantity": 5,
"payments": "Visa Card",
"price": 211,
"status": "Delivered"
},
{
"order_id": "TWT36092",
"customer_name": "Norry",
"location": "Hongqi",
"order_date": "2024-01-28T16:50:34Z",
"quantity": 7,
"payments": "American Express",
"price": 111,
"status": "Shopping"
},
{
"order_id": "TWT99659",
"customer_name": "Rabbi",
"location": "Macari",
"order_date": "2023-03-27T17:42:51Z",
"quantity": 9,
"payments": "COD",
"price": 268,
"status": "Pending"
},
{
"order_id": "TWT21952",
"customer_name": "Hesther",
"location": "København",
"order_date": "2024-02-08T00:16:01Z",
"quantity": 9,
"payments": "Credit Card",
"price": 392,
"status": "Delivered"
},
{
"order_id": "TWT66885",
"customer_name": "Sioux",
"location": "Taohua",
"order_date": "2023-10-23T16:25:29Z",
"quantity": 1,
"payments": "Paypal",
"price": 337,
"status": "New"
}
]

View File

@ -0,0 +1,82 @@
[
{
"id": 1,
"title": "Marketing Manager",
"assign_to": "Hercules",
"date": "2024-03-10T00:14:27Z",
"priority": "High",
"status": "Pending"
},
{
"id": 2,
"title": "Community Outreach Specialist",
"assign_to": "Fayre",
"date": "2024-10-07T06:24:18Z",
"priority": "High",
"status": "Pending"
},
{
"id": 3,
"title": "Senior Quality Engineer",
"assign_to": "Nancy",
"date": "2024-01-11T18:22:29Z",
"priority": "Medium",
"status": "In Progress"
},
{
"id": 4,
"title": "VP Sales",
"assign_to": "Jemimah",
"date": "2024-06-17T17:57:40Z",
"priority": "Low",
"status": "Finished"
},
{
"id": 5,
"title": "Sales Associate",
"assign_to": "Raquel",
"date": "2024-05-06T11:11:43Z",
"priority": "Medium",
"status": "Finished"
},
{
"id": 6,
"title": "Dental Hygienist",
"assign_to": "Vasili",
"date": "2024-05-31T21:16:27Z",
"priority": "High",
"status": "Finished"
},
{
"id": 7,
"title": "Occupational Therapist",
"assign_to": "Lulu",
"date": "2024-03-28T21:07:00Z",
"priority": "Medium",
"status": "Pending"
},
{
"id": 8,
"title": "Analyst Programmer",
"assign_to": "Egor",
"date": "2023-12-11T08:16:01Z",
"priority": "High",
"status": "Finished"
},
{
"id": 9,
"title": "Research Assistant III",
"assign_to": "Max",
"date": "2024-02-15T21:59:34Z",
"priority": "High",
"status": "Pending"
},
{
"id": 10,
"title": "Developer II",
"assign_to": "Kaela",
"date": "2024-06-24T07:29:47Z",
"priority": "High",
"status": "Cancelled"
}
]

View File

@ -0,0 +1,47 @@
[
{
"id": 1,
"product_name": "Theobald",
"quantity": 2,
"customer": "Theobald Southcott",
"status": "Shipped",
"price": 361,
"order_date": "2023-12-11T23:48:54Z"
},
{
"id": 2,
"product_name": "Carla",
"quantity": 3,
"customer": "Carla Grgic",
"status": "Pending",
"price": 329,
"order_date": "2024-05-25T09:37:51Z"
},
{
"id": 3,
"product_name": "Liana",
"quantity": 3,
"customer": "Liana Swannell",
"status": "Delivery",
"price": 120,
"order_date": "2024-04-06T19:02:14Z"
},
{
"id": 4,
"product_name": "Radcliffe",
"quantity": 4,
"customer": "Radcliffe Venard",
"status": "Shipped",
"price": 750,
"order_date": "2024-10-27T10:44:12Z"
},
{
"id": 5,
"product_name": "Delmer",
"quantity": 3,
"customer": "Delmer Vamplew",
"status": "Delivery",
"price": 469,
"order_date": "2024-10-16T15:55:22Z"
}
]

View File

@ -0,0 +1,91 @@
[
{
"id": 1,
"title": "Finish report",
"description": "Complete the quarterly report for the team meeting.",
"due_date": "2024-07-14T00:37:09Z",
"priority": "High",
"status": "Pending"
},
{
"id": 2,
"title": "Team meeting",
"description": "Attend the weekly project meeting and provide updates.",
"due_date": "2024-04-18T01:25:27Z",
"priority": "Medium",
"status": "Completed"
},
{
"id": 3,
"title": "Buy groceries",
"description": "Purchase ingredients for dinner and weekly supplies.",
"due_date": "2024-02-17T16:32:03Z",
"priority": "Low",
"status": "Pending"
},
{
"id": 4,
"title": "Update website",
"description": "Update the homepage with new content and images.",
"due_date": "2024-07-16T15:49:59Z",
"priority": "Medium",
"status": "Pending"
},
{
"id": 5,
"title": "Send emails",
"description": "Send out follow-up emails to clients from last week's meeting.",
"due_date": "2024-09-24T11:08:14Z",
"priority": "High",
"status": "Completed"
},
{
"id": 6,
"title": "Organize workspace",
"description": "Declutter desk and organize office supplies.",
"due_date": "2024-10-06T11:49:14Z",
"priority": "Low",
"status": "Pending"
},
{
"id": 7,
"title": "Prepare presentation",
"description": "Create slides for next week's client pitch.",
"due_date": "2024-05-20T04:38:51Z",
"priority": "High",
"status": "In Progress"
},
{
"id": 8,
"title": "Write blog post",
"description": "Write a blog post about industry trends for the company website.",
"due_date": "2024-01-13T07:46:34Z",
"priority": "Medium",
"status": "Pending"
},
{
"id": 9,
"title": "Schedule doctor's appointment",
"description": "Call and book a check-up appointment with the doctor.",
"due_date": "2024-09-22T10:54:21Z",
"priority": "Low",
"status": "Pending"
},
{
"id": 10,
"title": "Review budget",
"description": "Review and adjust monthly budget for expenses.",
"due_date": "2024-08-20T03:57:48Z",
"priority": "Medium",
"status": "Pending"
}
]

View File

@ -0,0 +1,62 @@
[
{
"id": 1,
"first_name": "Roobbie",
"last_name": "Ivashintsov",
"email": "rivashintsov0@symantec.com"
},
{
"id": 2,
"first_name": "Cissy",
"last_name": "Salmons",
"email": "csalmons1@unicef.org"
},
{
"id": 3,
"first_name": "Jillene",
"last_name": "Besnardeau",
"email": "jbesnardeau2@china.com.cn"
},
{
"id": 4,
"first_name": "Catriona",
"last_name": "Wrennall",
"email": "cwrennall3@godaddy.com"
},
{
"id": 5,
"first_name": "Risa",
"last_name": "Rumens",
"email": "rrumens4@un.org"
},
{
"id": 6,
"first_name": "Gianina",
"last_name": "Pavlenkov",
"email": "gpavlenkov5@ted.com"
},
{
"id": 7,
"first_name": "Tripp",
"last_name": "Blowick",
"email": "tblowick6@reuters.com"
},
{
"id": 8,
"first_name": "Ephrem",
"last_name": "Pfertner",
"email": "epfertner7@godaddy.com"
},
{
"id": 9,
"first_name": "Jacinda",
"last_name": "Tomkies",
"email": "jtomkies8@si.edu"
},
{
"id": 10,
"first_name": "Traver",
"last_name": "Poile",
"email": "tpoile9@phoca.cz"
}
]

View File

@ -0,0 +1,56 @@
[
{
"id": 1,
"session_duration": "2023-08-18T14:47:41Z",
"channel": "Organic Search",
"session": 547,
"bounce_rate": 27.2,
"target_reached": 843,
"page_per_session": 4.4
},
{
"id": 2,
"session_duration": "2023-04-20T20:13:24Z",
"channel": "Direct",
"session": 855,
"bounce_rate": 25.8,
"target_reached": 998,
"page_per_session": 6.6
},
{
"id": 3,
"session_duration": "2023-09-05T02:15:03Z",
"channel": "Referral",
"session": 337,
"bounce_rate": 12.4,
"target_reached": 509,
"page_per_session": 8.0
},
{
"id": 4,
"session_duration": "2023-06-23T13:50:55Z",
"channel": "Social",
"session": 279,
"bounce_rate": 40.4,
"target_reached": 860,
"page_per_session": 7.3
},
{
"id": 5,
"session_duration": "2023-09-14T09:01:34Z",
"channel": "Email",
"session": 118,
"bounce_rate": 46.2,
"target_reached": 168,
"page_per_session": 3.0
},
{
"id": 6,
"session_duration": "2023-07-14T01:46:51Z",
"channel": "Paid Search",
"session": 205,
"bounce_rate": 32.8,
"target_reached": 583,
"page_per_session": 2.5
}
]

42679
assets/data/world_map.json Normal file

File diff suppressed because it is too large Load Diff

BIN
assets/dummy/dummy_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
assets/dummy/dummy_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
assets/dummy/dummy_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
assets/dummy/dummy_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
assets/dummy/dummy_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,13 +0,0 @@
{
"type": "service_account",
"project_id": "mtest-a0635",
"private_key_id": "39a69f7d2a64234784e0d0ce6c113052296d6dc1",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCimbxktO7PeQ4h\n81Ye2ZBcZjltDhqqD0o9XyLmNdHszzM056bwpJkvgoyyTJAIvR2fcBF3YQFyuC+1\nddLHtchP48FjflZ+zZzLp7oaA/Zh28OZLbCsu+Nm8vO3WJVoIaJYgi+jEz21G128\ncOIbgkKIpLMz1wQhPPOwDTuSdQ+WajWJb04/aNrmTRH1hMreyhHiIFmalcavUgc1\nY5FvgrGs7EaKjYBevoFN3dwmEXjfyHjfBSxnt1yytl9tbtINqdrYLYAMm1l3+KqO\nCGxicQE5kjI1osI2wRjsk105RHnpxPg2GZnI4vTIOkEY5czhRSOs94g2d628H6fq\nVzf9UqwtAgMBAAECggEARluLf3AjHbdd/CbVDwhJRRIeqye9NfTjxOaTrVWAfp2x\npKTQQbSXbE1rIAOtF3rthH3zsNpSzBcS3cwb5rqr8JW2qpySRNAnlp//ER7Bz9pO\nKsvwdO3gGj3qY117WNGk8/NxNXkv7FvpFY8q54hXzdSmjjnt2YwMThOLwXXRxt2B\nFxN3FpBWqw12epqS162nW2nIRJ34Jloil4J5x61Sc79MCFyCxyhMlrBkY+Ni/xb1\nigBXBjczxNiJqqDie0mc16WB1HMEcBP9Yjtb46Hhfs3NDDWNqDkoM8QmEMSg8EHy\nyjcSlf0Wj8I9Kf+0PZo+2FB2DbuhfA8IVR9U/c00KQKBgQDd/OULx6QpmUev1Gl/\nrwwN67ZUMJ72cRuwvLFsMTIzZ+oItO0AR1uMkRZ1crOMc490XNUvSCGP6piZQAn1\nro8qNAh+0Q/UvKHM1khOj/4DxEGZRnNOhe6QLZM9QNygENuEYfdYDD9wcQI9Xs+B\nMIOBsuuqUVHlsbvYkeYNS8M8swKBgQC7g3i1dYRC/bkNMthVS4GTlFRuLscyIjTi\nhruhdaSE+fBZ5RO3XDzz6oDHYcdo/z5ySqI7EIsckNRbwFsMCOjSP3xJapadPYwU\nIhZBU7lgNlPnHJ/BIUwA5JZqRqGTNWrFINUHZFp2RK/x2bYdfoqY8bq08eWs9gmR\nc7U7i+6jnwKBgGaO3isxExD89fewBQWuk70it1vyEp785rQimT3JBM5nJeLb49sL\nHKq2pU+hrH4pLY+vC/cKNidNVS8IPRG6kf4HiB0+7Td15rLCFSnmsI6A72Wm/MK8\ncdk+lRXpj4SMBT8GG8Yb8ns6WrSLxwaCqV8UkHhhlZqvIIAP998Qr6StAoGAQTwr\n8nU/3k6G4qCdwo7SNZWVCgAcLMTZwTU+cZ2L7vdFNwELKu9cBT/ALZ1G0rB5+Skd\n546J1xZLyt/QzQ8McJjFlIUQgQO4iAiT1YZbJ62+4tiCe54p4uWjrrWD4MLkslAJ\nzNiM4DhlPa6QPRKZBTyTx/+f99xg18l5c43rJ+ECgYBkXMfjdn8SOaG6ggJrf1xx\nas49vwAscx4AJaOdVu3D8lCwoNCuAJhBHcFqsJ0wEHWpsqKAdXxqX/Nt2x8t7zL0\nPoRCvfsq5P7GdRrNhrHxLwjDqh+OS+Ow6t0esPQ5RPBgtjvthAlb7bV2nIfkpmdl\nFbjML8vkXk9iPJsbAfO2jw==\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fbsvc@mtest-a0635.iam.gserviceaccount.com",
"client_id": "111097905744982732087",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40mtest-a0635.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@ -1,55 +0,0 @@
#!/bin/bash
# ===============================
# Flutter APK Build Script (AAB Disabled)
# ===============================
# Exit immediately if a command exits with a non-zero status
set -e
# Colors for pretty output
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# App info
APP_NAME="Marco"
BUILD_DIR="build/app/outputs"
echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}"
# Step 1: Clean previous builds
echo -e "${YELLOW}🧹 Cleaning previous builds...${NC}"
flutter clean
# Step 2: Get dependencies
echo -e "${YELLOW}📦 Fetching dependencies...${NC}"
flutter pub get
# ==============================
# Step 3: Build AAB (Commented)
# ==============================
# echo -e "${CYAN}🏗 Building AAB file...${NC}"
# flutter build appbundle --release
# Step 4: Build APK
echo -e "${CYAN}🏗 Building APK file...${NC}"
flutter build apk --release
# Step 5: Show output paths
# AAB_PATH="$BUILD_DIR/bundle/release/app-release.aab"
APK_PATH="$BUILD_DIR/apk/release/app-release.apk"
echo -e "${GREEN}✅ Build completed successfully!${NC}"
# echo -e "${YELLOW}📍 AAB file: ${CYAN}$AAB_PATH${NC}"
echo -e "${YELLOW}📍 APK file: ${CYAN}$APK_PATH${NC}"
# Optional: open the folder (Mac/Linux)
if command -v xdg-open &> /dev/null
then
xdg-open "$BUILD_DIR"
elif command -v open &> /dev/null
then
open "$BUILD_DIR"
fi

View File

@ -1,3 +0,0 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@ -368,7 +368,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@ -384,7 +384,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -401,7 +401,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -416,7 +416,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -547,7 +547,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;

View File

@ -1,426 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_image_compressor.dart';
import 'package:marco/model/attendance/attendance_model.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/attendance/attendance_log_model.dart';
import 'package:marco/model/regularization_log_model.dart';
import 'package:marco/model/attendance/attendance_log_view_model.dart';
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
import 'package:marco/controller/project_controller.dart';
class AttendanceController extends GetxController {
// Data models
List<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
List<EmployeeModel> employees = [];
List<AttendanceLogModel> attendanceLogs = [];
List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = [];
// ------------------ Organizations ------------------
List<Organization> organizations = [];
Organization? selectedOrganization;
final isLoadingOrganizations = false.obs;
// States
String selectedTab = 'todaysAttendance';
DateTime? startDateAttendance;
DateTime? endDateAttendance;
final isLoading = true.obs;
final isLoadingProjects = true.obs;
final isLoadingEmployees = true.obs;
final isLoadingAttendanceLogs = true.obs;
final isLoadingRegularizationLogs = true.obs;
final isLoadingLogView = true.obs;
final uploadingStates = <String, RxBool>{}.obs;
var showPendingOnly = false.obs;
@override
void onInit() {
super.onInit();
_initializeDefaults();
// 🔹 Fetch organizations for the selected project
final projectId = Get.find<ProjectController>().selectedProject?.id;
if (projectId != null) {
fetchOrganizations(projectId);
}
}
void _initializeDefaults() {
_setDefaultDateRange();
}
void _setDefaultDateRange() {
final today = DateTime.now();
startDateAttendance = today.subtract(const Duration(days: 7));
endDateAttendance = today.subtract(const Duration(days: 1));
logSafe(
"Default date range set: $startDateAttendance to $endDateAttendance");
}
// ------------------ Project & Employee ------------------
/// Called when a notification says attendance has been updated
Future<void> refreshDataFromNotification({String? projectId}) async {
projectId ??= Get.find<ProjectController>().selectedProject?.id;
if (projectId == null) {
logSafe("No project selected for attendance refresh from notification",
level: LogLevel.warning);
return;
}
await fetchProjectData(projectId);
logSafe(
"Attendance data refreshed from notification for project $projectId");
}
// 🔍 Search query
final searchQuery = ''.obs;
// Computed filtered employees
List<EmployeeModel> get filteredEmployees {
if (searchQuery.value.isEmpty) return employees;
return employees
.where((e) =>
e.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
// Computed filtered logs
List<AttendanceLogModel> get filteredLogs {
if (searchQuery.value.isEmpty) return attendanceLogs;
return attendanceLogs
.where((log) =>
(log.name).toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
// Computed filtered regularization logs
List<RegularizationLogModel> get filteredRegularizationLogs {
if (searchQuery.value.isEmpty) return regularizationLogs;
return regularizationLogs
.where((log) =>
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
Future<void> fetchTodaysAttendance(String? projectId) async {
if (projectId == null) return;
isLoadingEmployees.value = true;
final response = await ApiService.getTodaysAttendance(
projectId,
organizationId: selectedOrganization?.id,
);
if (response != null) {
employees = response.map((e) => EmployeeModel.fromJson(e)).toList();
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
logSafe("Employees fetched: ${employees.length} for project $projectId");
} else {
logSafe("Failed to fetch employees for project $projectId",
level: LogLevel.error);
}
isLoadingEmployees.value = false;
update();
}
Future<void> fetchOrganizations(String projectId) async {
isLoadingOrganizations.value = true;
final response = await ApiService.getAssignedOrganizations(projectId);
if (response != null) {
organizations = response.data;
logSafe("Organizations fetched: ${organizations.length}");
} else {
logSafe("Failed to fetch organizations for project $projectId",
level: LogLevel.error);
}
isLoadingOrganizations.value = false;
update();
}
// ------------------ Attendance Capture ------------------
Future<bool> captureAndUploadAttendance(
String id,
String employeeId,
String projectId, {
String comment = "Marked via mobile app",
required int action,
bool imageCapture = true,
String? markTime, // still optional in controller
String? date, // new optional param
}) async {
try {
uploadingStates[employeeId]?.value = true;
XFile? image;
if (imageCapture) {
image = await ImagePicker()
.pickImage(source: ImageSource.camera, imageQuality: 80);
if (image == null) {
logSafe("Image capture cancelled.", level: LogLevel.warning);
return false;
}
final compressedBytes =
await compressImageToUnder100KB(File(image.path));
if (compressedBytes == null) {
logSafe("Image compression failed.", level: LogLevel.error);
return false;
}
final compressedFile = await saveCompressedImageToFile(compressedBytes);
image = XFile(compressedFile.path);
}
if (!await _handleLocationPermission()) return false;
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
final imageName = imageCapture
? ApiService.generateImageName(employeeId, employees.length + 1)
: "";
// ---------------- DATE / TIME LOGIC ----------------
final now = DateTime.now();
// Default effectiveDate = now
DateTime effectiveDate = now;
if (action == 1) {
// Checkout
// Try to find today's open log for this employee
final log = attendanceLogs.firstWhereOrNull(
(log) => log.employeeId == employeeId && log.checkOut == null,
);
if (log?.checkIn != null) {
effectiveDate = log!.checkIn!; // use check-in date
}
}
final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now);
final formattedDate =
date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
// ---------------- API CALL ----------------
final result = await ApiService.uploadAttendanceImage(
id,
employeeId,
image,
position.latitude,
position.longitude,
imageName: imageName,
projectId: projectId,
comment: comment,
action: action,
imageCapture: imageCapture,
markTime: formattedMarkTime,
date: formattedDate,
);
logSafe(
"Attendance uploaded for $employeeId, action: $action, date: $formattedDate");
return result;
} catch (e, stacktrace) {
logSafe("Error uploading attendance",
level: LogLevel.error, error: e, stackTrace: stacktrace);
return false;
} finally {
uploadingStates[employeeId]?.value = false;
}
}
Future<bool> _handleLocationPermission() async {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
logSafe('Location permissions are denied', level: LogLevel.warning);
return false;
}
}
if (permission == LocationPermission.deniedForever) {
logSafe('Location permissions are permanently denied',
level: LogLevel.error);
return false;
}
return true;
}
// ------------------ Attendance Logs ------------------
Future<void> fetchAttendanceLogs(String? projectId,
{DateTime? dateFrom, DateTime? dateTo}) async {
if (projectId == null) return;
isLoadingAttendanceLogs.value = true;
final response = await ApiService.getAttendanceLogs(
projectId,
dateFrom: dateFrom,
dateTo: dateTo,
organizationId: selectedOrganization?.id,
);
if (response != null) {
attendanceLogs =
response.map((e) => AttendanceLogModel.fromJson(e)).toList();
logSafe("Attendance logs fetched: ${attendanceLogs.length}");
} else {
logSafe("Failed to fetch attendance logs for project $projectId",
level: LogLevel.error);
}
isLoadingAttendanceLogs.value = false;
update();
}
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final groupedLogs = <String, List<AttendanceLogModel>>{};
for (var logItem in attendanceLogs) {
final checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown';
groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem);
}
final sortedEntries = groupedLogs.entries.toList()
..sort((a, b) {
if (a.key == 'Unknown') return 1;
if (b.key == 'Unknown') return -1;
final dateA = DateFormat('dd MMM yyyy').parse(a.key);
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA);
});
return Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
}
// ------------------ Regularization Logs ------------------
Future<void> fetchRegularizationLogs(String? projectId) async {
if (projectId == null) return;
isLoadingRegularizationLogs.value = true;
final response = await ApiService.getRegularizationLogs(
projectId,
organizationId: selectedOrganization?.id,
);
if (response != null) {
regularizationLogs =
response.map((e) => RegularizationLogModel.fromJson(e)).toList();
logSafe("Regularization logs fetched: ${regularizationLogs.length}");
} else {
logSafe("Failed to fetch regularization logs for project $projectId",
level: LogLevel.error);
}
isLoadingRegularizationLogs.value = false;
update();
}
// ------------------ Attendance Log View ------------------
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
isLoadingLogView.value = true;
final response = await ApiService.getAttendanceLogView(id);
if (response != null) {
attendenceLogsView =
response.map((e) => AttendanceLogViewModel.fromJson(e)).toList();
attendenceLogsView.sort((a, b) => (b.activityTime ?? DateTime(2000))
.compareTo(a.activityTime ?? DateTime(2000)));
logSafe("Attendance log view fetched for ID: $id");
} else {
logSafe("Failed to fetch attendance log view for ID $id",
level: LogLevel.error);
}
isLoadingLogView.value = false;
update();
}
// ------------------ Combined Load ------------------
Future<void> loadAttendanceData(String projectId) async {
isLoading.value = true;
await fetchProjectData(projectId);
isLoading.value = false;
}
Future<void> fetchProjectData(String? projectId) async {
if (projectId == null) return;
await fetchOrganizations(projectId);
// Call APIs depending on the selected tab only
switch (selectedTab) {
case 'todaysAttendance':
await fetchTodaysAttendance(projectId);
break;
case 'attendanceLogs':
await fetchAttendanceLogs(
projectId,
dateFrom: startDateAttendance,
dateTo: endDateAttendance,
);
break;
case 'regularizationRequests':
await fetchRegularizationLogs(projectId);
break;
}
logSafe(
"Project data fetched for project ID: $projectId, tab: $selectedTab");
update();
}
// ------------------ UI Interaction ------------------
Future<void> selectDateRangeForAttendance(
BuildContext context, AttendanceController controller) async {
final today = DateTime.now();
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2022),
lastDate: today.subtract(const Duration(days: 1)),
initialDateRange: DateTimeRange(
start: startDateAttendance ?? today.subtract(const Duration(days: 7)),
end: endDateAttendance ?? today.subtract(const Duration(days: 1)),
),
);
if (picked != null) {
startDateAttendance = picked.start;
endDateAttendance = picked.end;
logSafe(
"Date range selected: $startDateAttendance to $endDateAttendance");
await controller.fetchAttendanceLogs(
Get.find<ProjectController>().selectedProject?.id,
dateFrom: picked.start,
dateTo: picked.end,
);
}
}
}

View File

@ -4,17 +4,13 @@ import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
class ForgotPasswordController extends MyController {
final MyFormValidator basicValidator = MyFormValidator();
final RxBool isLoading = false.obs;
MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false;
@override
void onInit() {
super.onInit();
basicValidator.addField(
'email',
required: true,
@ -22,49 +18,24 @@ class ForgotPasswordController extends MyController {
validators: [MyEmailValidator()],
controller: TextEditingController(text: "demo@example.com"),
);
super.onInit();
}
Future<void> onForgotPassword() async {
if (!basicValidator.validateForm()) return;
isLoading.value = true;
final data = basicValidator.getData();
final email = data['email']?.toString() ?? '';
try {
logSafe("Forgot password requested for: $email", );
final result = await AuthService.forgotPassword(email);
if (result == null) {
showAppSnackbar(
title: "Success",
message: "Password reset link has been sent.",
type: SnackbarType.success,
);
await LocalStorage.logout();
} else {
final errorMessage = result['error'] ?? "Failed to send reset link. Please try again.";
showAppSnackbar(
title: "Failed",
message: errorMessage,
type: SnackbarType.error,
);
logSafe("Failed to send reset password email for $email: $errorMessage", level: LogLevel.warning, );
Future<void> onLogin() async {
if (basicValidator.validateForm()) {
update();
var errors = await AuthService.loginUser(basicValidator.getData());
if (errors != null) {
basicValidator.validateForm();
basicValidator.clearErrors();
}
} catch (e, stacktrace) {
logSafe("Error during forgot password", level: LogLevel.error, error: e, stackTrace: stacktrace);
showAppSnackbar(
title: "Error",
message: "Something went wrong. Please try again later.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
Get.toNamed('/auth/reset_password');
update();
}
}
void gotoLogIn() {
Get.offAllNamed('/auth/login-option');
Get.toNamed('/auth/login');
}
}

View File

@ -4,115 +4,61 @@ import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart';
class LoginController extends MyController {
final MyFormValidator basicValidator = MyFormValidator();
MyFormValidator basicValidator = MyFormValidator();
final RxBool isLoading = false.obs;
final RxBool showPassword = false.obs;
final RxBool isChecked = false.obs;
final RxBool showSplash = false.obs;
bool showPassword = false, isChecked = false;
RxBool isLoading = false.obs; // Add reactive loading state
final String _dummyEmail = "admin@marcoaiot.com";
final String _dummyPassword = "User@123";
@override
void onInit() {
basicValidator.addField('username', required: true, label: "User_Name", validators: [MyEmailValidator()], controller: TextEditingController(text: _dummyEmail));
basicValidator.addField('password', required: true, label: "Password", validators: [MyLengthValidator(min: 6, max: 10)], controller: TextEditingController(text: _dummyPassword));
super.onInit();
_initializeForm();
_loadSavedCredentials();
}
void _initializeForm() {
basicValidator.addField(
'username',
required: true,
label: "User_Name",
validators: [MyEmailValidator()],
controller: TextEditingController(),
);
basicValidator.addField(
'password',
required: true,
label: "Password",
validators: [MyLengthValidator(min: 6)],
controller: TextEditingController(),
);
void onChangeCheckBox(bool? value) {
isChecked = value ?? isChecked;
update();
}
void onChangeCheckBox(bool? value) => isChecked.value = value ?? false;
void onChangeShowPassword() => showPassword.toggle();
void onChangeShowPassword() {
showPassword = !showPassword;
update();
}
Future<void> onLogin() async {
if (!basicValidator.validateForm()) return;
showSplash.value = true;
try {
final loginData = basicValidator.getData();
logSafe("Attempting login for user: ${loginData['username']}");
final errors = await AuthService.loginUser(loginData);
if (basicValidator.validateForm()) {
// Set loading to true
isLoading.value = true;
update();
var errors = await AuthService.loginUser(basicValidator.getData());
if (errors != null) {
showAppSnackbar(
title: "Login Failed",
message: "Username or password is incorrect",
type: SnackbarType.error,
);
basicValidator.addErrors(errors);
basicValidator.validateForm();
basicValidator.clearErrors();
} else {
await _handleRememberMe();
enableRemoteLogging();
logSafe("Login successful for user: ${loginData['username']}");
Get.offNamed('/select-tenant');
String nextUrl = Uri.parse(ModalRoute.of(Get.context!)?.settings.name ?? "").queryParameters['next'] ?? "/home";
Get.toNamed(nextUrl);
}
} catch (e, stacktrace) {
showAppSnackbar(
title: "Login Error",
message: "An unexpected error occurred",
type: SnackbarType.error,
);
logSafe("Exception during login",
level: LogLevel.error, error: e, stackTrace: stacktrace);
} finally {
showSplash.value = false;
// Set loading to false after the API call is complete
isLoading.value = false;
update();
}
}
Future<void> _handleRememberMe() async {
if (isChecked.value) {
await LocalStorage.setToken(
'username', basicValidator.getController('username')!.text);
await LocalStorage.setToken(
'password', basicValidator.getController('password')!.text);
await LocalStorage.setBool('remember_me', true);
} else {
await LocalStorage.removeToken('username');
await LocalStorage.removeToken('password');
await LocalStorage.setBool('remember_me', false);
basicValidator.clearErrors();
}
void goToForgotPassword() {
Get.toNamed('/auth/forgot_password');
}
Future<void> _loadSavedCredentials() async {
final savedUsername = LocalStorage.getToken('username');
final savedPassword = LocalStorage.getToken('password');
final remember = LocalStorage.getBool('remember_me') ?? false;
isChecked.value = remember;
if (remember) {
basicValidator.getController('username')?.text = savedUsername ?? '';
basicValidator.getController('password')?.text = savedPassword ?? '';
}
void gotoRegister() {
Get.offAndToNamed('/auth/register_account');
}
void goToForgotPassword() => Get.toNamed('/auth/forgot_password');
void gotoRegister() => Get.offAndToNamed('/auth/register_account');
}

View File

@ -1,321 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/project_controller.dart';
class MPINController extends GetxController {
final MyFormValidator basicValidator = MyFormValidator();
final isNewUser = false.obs;
final isChangeMpin = false.obs;
final RxBool isLoading = false.obs;
final formKey = GlobalKey<FormState>();
// Updated to 4-digit MPIN
final digitControllers = List.generate(4, (_) => TextEditingController());
final focusNodes = List.generate(4, (_) => FocusNode());
final retypeControllers = List.generate(4, (_) => TextEditingController());
final retypeFocusNodes = List.generate(4, (_) => FocusNode());
final RxInt failedAttempts = 0.obs;
@override
void onInit() {
super.onInit();
final bool hasMpin = LocalStorage.getIsMpin();
isNewUser.value = !hasMpin;
logSafe("onInit called. isNewUser: ${isNewUser.value}");
}
/// Enable Change MPIN mode
void setChangeMpinMode() {
isChangeMpin.value = true;
isNewUser.value = false;
clearFields();
clearRetypeFields();
logSafe("setChangeMpinMode activated");
}
/// Handle digit entry and focus movement
void onDigitChanged(String value, int index, {bool isRetype = false}) {
logSafe(
"onDigitChanged -> index: $index, value: $value, isRetype: $isRetype");
final nodes = isRetype ? retypeFocusNodes : focusNodes;
if (value.isNotEmpty && index < 3) {
nodes[index + 1].requestFocus();
} else if (value.isEmpty && index > 0) {
nodes[index - 1].requestFocus();
}
}
/// Submit MPIN for verification or generation
Future<void> onSubmitMPIN() async {
logSafe("onSubmitMPIN triggered");
if (!formKey.currentState!.validate()) {
logSafe("Form validation failed", level: LogLevel.warning);
return;
}
final enteredMPIN = digitControllers.map((c) => c.text).join();
logSafe("Entered MPIN: $enteredMPIN");
if (enteredMPIN.length < 4) {
_showError("Please enter all 4 digits.");
return;
}
if (isNewUser.value || isChangeMpin.value) {
final retypeMPIN = retypeControllers.map((c) => c.text).join();
logSafe("Retyped MPIN: $retypeMPIN");
if (retypeMPIN.length < 4) {
_showError("Please enter all 4 digits in Retype MPIN.");
return;
}
if (enteredMPIN != retypeMPIN) {
_showError("MPIN and Retype MPIN do not match.");
clearFields();
clearRetypeFields();
return;
}
final bool success = await generateMPIN(mpin: enteredMPIN);
if (success) {
logSafe("MPIN generation/change successful.");
showAppSnackbar(
title: "Success",
message: isChangeMpin.value
? "MPIN changed successfully."
: "MPIN generated successfully. Please login again.",
type: SnackbarType.success,
);
await LocalStorage.logout();
} else {
logSafe("MPIN generation/change failed.", level: LogLevel.warning);
clearFields();
clearRetypeFields();
}
} else {
logSafe("Existing user. Proceeding to verify MPIN.");
await verifyMPIN();
}
}
/// Forgot MPIN
Future<void> onForgotMPIN() async {
logSafe("onForgotMPIN called");
isNewUser.value = true;
isChangeMpin.value = false;
clearFields();
clearRetypeFields();
}
/// Switch to login/enter MPIN screen
void switchToEnterMPIN() {
logSafe("switchToEnterMPIN called");
isNewUser.value = false;
isChangeMpin.value = false;
clearFields();
clearRetypeFields();
}
/// Show error snackbar
void _showError(String message) {
logSafe("ERROR: $message", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: message,
type: SnackbarType.error,
);
}
/// Navigate to dashboard
/// Navigate to tenant selection after MPIN verification
void _navigateToTenantSelection({String? message}) {
if (message != null) {
logSafe("Navigating to Tenant Selection with message: $message");
showAppSnackbar(
title: "Success",
message: message,
type: SnackbarType.success,
);
}
Get.offAllNamed('/select-tenant');
}
/// Clear the primary MPIN fields
void clearFields() {
logSafe("clearFields called");
for (final c in digitControllers) {
c.clear();
}
focusNodes.first.requestFocus();
}
/// Clear the retype MPIN fields
void clearRetypeFields() {
logSafe("clearRetypeFields called");
for (final c in retypeControllers) {
c.clear();
}
retypeFocusNodes.first.requestFocus();
}
/// Cleanup
@override
void onClose() {
logSafe("onClose called");
for (final controller in digitControllers) {
controller.dispose();
}
for (final node in focusNodes) {
node.dispose();
}
for (final controller in retypeControllers) {
controller.dispose();
}
for (final node in retypeFocusNodes) {
node.dispose();
}
super.onClose();
}
/// Generate MPIN for new user/change MPIN
Future<bool> generateMPIN({required String mpin}) async {
try {
isLoading.value = true;
logSafe("generateMPIN started");
final employeeInfo = LocalStorage.getEmployeeInfo();
final String? employeeId = employeeInfo?.id;
if (employeeId == null || employeeId.isEmpty) {
isLoading.value = false;
_showError("Missing employee ID.");
return false;
}
logSafe("Calling AuthService.generateMpin for employeeId: $employeeId");
final response = await AuthService.generateMpin(
employeeId: employeeId,
mpin: mpin,
);
isLoading.value = false;
if (response == null) {
return true;
} else {
logSafe("MPIN generation returned error: $response",
level: LogLevel.warning);
showAppSnackbar(
title: "MPIN Operation Failed",
message: "Please check your inputs.",
type: SnackbarType.error,
);
basicValidator.addErrors(response);
basicValidator.validateForm();
basicValidator.clearErrors();
return false;
}
} catch (e) {
isLoading.value = false;
logSafe("Exception in generateMPIN", level: LogLevel.error, error: e);
_showError("Failed to process MPIN.");
return false;
}
}
/// Verify MPIN for existing user
Future<void> verifyMPIN() async {
logSafe("verifyMPIN triggered");
final enteredMPIN = digitControllers.map((c) => c.text).join();
if (enteredMPIN.length < 4) {
_showError("Please enter all 4 digits.");
return;
}
final mpinToken = await LocalStorage.getMpinToken();
if (mpinToken == null || mpinToken.isEmpty) {
_showError("Missing MPIN token. Please log in again.");
return;
}
try {
isLoading.value = true;
final fcmToken = await FirebaseNotificationService().getFcmToken();
final response = await AuthService.verifyMpin(
mpin: enteredMPIN,
mpinToken: mpinToken,
fcmToken: fcmToken ?? '',
);
isLoading.value = false;
if (response == null) {
logSafe("MPIN verified successfully");
await LocalStorage.setBool('mpin_verified', true);
// 🔹 Ensure controllers are injected and loaded
final token = await LocalStorage.getJwtToken();
if (token != null && token.isNotEmpty) {
if (!Get.isRegistered<PermissionController>()) {
Get.put(PermissionController());
await Get.find<PermissionController>().loadData(token);
}
if (!Get.isRegistered<ProjectController>()) {
Get.put(ProjectController(), permanent: true);
await Get.find<ProjectController>().fetchProjects();
}
}
showAppSnackbar(
title: "Success",
message: "MPIN Verified Successfully",
type: SnackbarType.success,
);
_navigateToTenantSelection();
} else {
final errorMessage = response["error"] ?? "Invalid MPIN";
logSafe("MPIN verification failed: $errorMessage",
level: LogLevel.warning);
showAppSnackbar(
title: "Error",
message: errorMessage,
type: SnackbarType.error,
);
clearFields();
onInvalidMPIN();
}
} catch (e) {
isLoading.value = false;
logSafe("Exception in verifyMPIN", level: LogLevel.error, error: e);
_showError("Something went wrong. Please try again.");
}
}
/// Increment failed attempts and warn
void onInvalidMPIN() {
failedAttempts.value++;
if (failedAttempts.value >= 3) {
showAppSnackbar(
title: "Error",
message: "Too many failed attempts. Consider logging in again.",
type: SnackbarType.error,
);
}
}
}

View File

@ -1,217 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart';
class OTPController extends GetxController {
final formKey = GlobalKey<FormState>();
final RxString email = ''.obs;
final RxBool isOTPSent = false.obs;
final RxBool isSending = false.obs;
final RxBool isResending = false.obs;
final RxInt timer = 0.obs;
Timer? _countdownTimer;
final TextEditingController emailController = TextEditingController();
final List<TextEditingController> otpControllers =
List.generate(4, (_) => TextEditingController());
final List<FocusNode> focusNodes = List.generate(4, (_) => FocusNode());
@override
void onInit() {
super.onInit();
timer.value = 0;
_loadSavedEmail();
logSafe("[OTPController] Initialized");
}
@override
void onClose() {
_countdownTimer?.cancel();
emailController.dispose();
for (final controller in otpControllers) {
controller.dispose();
}
for (final node in focusNodes) {
node.dispose();
}
logSafe("[OTPController] Disposed");
super.onClose();
}
Future<bool> _sendOTP(String email) async {
logSafe("[OTPController] Sending OTP");
final result = await AuthService.generateOtp(email);
if (result == null) {
logSafe("[OTPController] OTP sent successfully");
return true;
} else {
logSafe(
"[OTPController] OTP send failed",
level: LogLevel.warning,
error: result['error'],
);
showAppSnackbar(
title: "Error",
message: result['error'] ?? "Failed to send OTP",
type: SnackbarType.error,
);
return false;
}
}
Future<void> sendOTP() async {
final userEmail = emailController.text.trim();
logSafe("[OTPController] sendOTP called");
if (!_validateEmail(userEmail)) {
logSafe("[OTPController] Invalid email format", level: LogLevel.warning);
showAppSnackbar(
title: "Error",
message: "Please enter a valid email address",
type: SnackbarType.error,
);
return;
}
if (isSending.value) return;
isSending.value = true;
final success = await _sendOTP(userEmail);
if (success) {
email.value = userEmail;
isOTPSent.value = true;
await _saveEmailIfRemembered(userEmail);
_startTimer();
_clearOTPFields();
}
isSending.value = false;
}
Future<void> onResendOTP() async {
if (isResending.value) return;
logSafe("[OTPController] Resending OTP");
isResending.value = true;
_clearOTPFields();
final success = await _sendOTP(email.value);
if (success) {
_startTimer();
}
isResending.value = false;
}
void onOTPChanged(String value, int index) {
logSafe("[OTPController] OTP field changed: index=$index",
level: LogLevel.debug);
if (value.isNotEmpty) {
if (index < otpControllers.length - 1) {
focusNodes[index + 1].requestFocus();
} else {
focusNodes[index].unfocus();
}
} else {
if (index > 0) {
focusNodes[index - 1].requestFocus();
}
}
}
Future<void> verifyOTP() async {
final enteredOTP = otpControllers.map((c) => c.text).join();
final result = await AuthService.verifyOtp(
email: email.value,
otp: enteredOTP,
);
if (result == null) {
// Handle remember-me like in LoginController
final remember = LocalStorage.getBool('remember_me') ?? false;
if (remember) await LocalStorage.setToken('otp_email', email.value);
// Enable remote logging
enableRemoteLogging();
Get.offAllNamed('/select-tenant');
} else {
showAppSnackbar(
title: "Error",
message: result['error']!,
type: SnackbarType.error,
);
}
}
void _clearOTPFields() {
logSafe("[OTPController] Clearing OTP input fields", level: LogLevel.debug);
for (final controller in otpControllers) {
controller.clear();
}
focusNodes[0].requestFocus();
}
void _startTimer() {
logSafe("[OTPController] Starting resend timer");
timer.value = 60;
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (this.timer.value > 0) {
this.timer.value--;
} else {
timer.cancel();
}
});
}
void resetForChangeEmail() {
logSafe("[OTPController] Resetting OTP form for change email");
isOTPSent.value = false;
email.value = '';
emailController.clear();
_clearOTPFields();
timer.value = 0;
isSending.value = false;
isResending.value = false;
for (final node in focusNodes) {
node.unfocus();
}
// Optionally remove saved email
LocalStorage.removeToken('otp_email');
}
bool _validateEmail(String email) {
final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,}$');
return regex.hasMatch(email);
}
/// Save email to local storage if "remember me" is set
Future<void> _saveEmailIfRemembered(String email) async {
final remember = LocalStorage.getBool('remember_me') ?? false;
if (remember) {
await LocalStorage.setToken('otp_email', email);
}
}
/// Load email from local storage if "remember me" is true
Future<void> _loadSavedEmail() async {
final remember = LocalStorage.getBool('remember_me') ?? false;
if (remember) {
final savedEmail = LocalStorage.getToken('otp_email') ?? '';
emailController.text = savedEmail;
email.value = savedEmail;
logSafe(
"[OTPController] Loaded saved email from local storage: $savedEmail");
}
}
}

View File

@ -3,17 +3,16 @@ import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/auth_service.dart';
class RegisterAccountController extends MyController {
MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false;
@override
void onInit() {
logSafe("[RegisterAccountController] onInit called");
basicValidator.addField(
'email',
required: true,
@ -39,40 +38,29 @@ class RegisterAccountController extends MyController {
validators: [MyLengthValidator(min: 6, max: 10)],
controller: TextEditingController(),
);
super.onInit();
}
Future<void> onLogin() async {
if (basicValidator.validateForm()) {
update();
final data = basicValidator.getData();
logSafe("[RegisterAccountController] Submitting registration data");
final errors = await AuthService.loginUser(data);
var errors = await AuthService.loginUser(basicValidator.getData());
if (errors != null) {
logSafe("[RegisterAccountController] Login errors: $errors", level: LogLevel.warning);
basicValidator.addErrors(errors);
basicValidator.validateForm();
basicValidator.clearErrors();
}
logSafe("[RegisterAccountController] Redirecting to /starter");
Get.toNamed('/starter');
update();
} else {
logSafe("[RegisterAccountController] Validation failed", level: LogLevel.warning);
}
}
void onChangeShowPassword() {
showPassword = !showPassword;
logSafe("[RegisterAccountController] showPassword toggled: $showPassword");
update();
}
void gotoLogin() {
logSafe("[RegisterAccountController] Navigating to /auth/login-option");
Get.toNamed('/auth/login-option');
Get.toNamed('/auth/login');
}
}

View File

@ -4,68 +4,56 @@ import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/app_logger.dart';
class ResetPasswordController extends MyController {
MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false;
bool confirmPassword = false;
@override
void onInit() {
super.onInit();
logSafe("[ResetPasswordController] onInit called");
basicValidator.addField(
'password',
required: true,
validators: [MyLengthValidator(min: 6, max: 10)],
validators: [
MyLengthValidator(min: 6, max: 10),
],
controller: TextEditingController(),
);
basicValidator.addField(
'confirm_password',
required: true,
label: "Confirm password",
validators: [MyLengthValidator(min: 6, max: 10)],
validators: [
MyLengthValidator(min: 6, max: 10),
],
controller: TextEditingController(),
);
}
Future<void> onResetPassword() async {
logSafe("[ResetPasswordController] onResetPassword triggered");
if (basicValidator.validateForm()) {
final data = basicValidator.getData();
logSafe("[ResetPasswordController] Reset password form data");
update();
final errors = await AuthService.loginUser(data); // Consider renaming this to resetPassword() for clarity
var errors = await AuthService.loginUser(basicValidator.getData());
if (errors != null) {
logSafe("[ResetPasswordController] Received errors: $errors", level: LogLevel.warning);
basicValidator.addErrors(errors);
basicValidator.validateForm();
basicValidator.clearErrors();
}
logSafe("[ResetPasswordController] Navigating to /dashboard");
Get.toNamed('/dashboard');
Get.toNamed('/home');
update();
} else {
logSafe("[ResetPasswordController] Form validation failed", level: LogLevel.warning);
}
}
void onChangeShowPassword() {
showPassword = !showPassword;
logSafe("[ResetPasswordController] showPassword toggled: $showPassword");
update();
}
void onConfirmPassword() {
confirmPassword = !confirmPassword;
logSafe("[ResetPasswordController] confirmPassword toggled: $confirmPassword");
update();
}
}
}

View File

@ -0,0 +1,52 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/visitor_by_channels_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class AnalyticsController extends MyController {
String selectActivity = "Year";
List<VisitorByChannelsModel> visitorByChannel = [];
final TooltipBehavior columnChartToolTip = TooltipBehavior(enable: true, format: 'point.x : point.y', tooltipPosition: TooltipPosition.pointer);
final TooltipBehavior audienceOverview = TooltipBehavior(enable: true, format: 'point.x : point.y', tooltipPosition: TooltipPosition.pointer);
@override
void onInit() {
VisitorByChannelsModel.dummyList.then((value) {
visitorByChannel = value;
update();
});
super.onInit();
}
void onSelectedActivity(String time) {
selectActivity = time;
update();
}
void removeData(index) {
visitorByChannel.removeAt(index);
update();
}
final List<ChartSampleData> columnChart = <ChartSampleData>[
ChartSampleData(x: 2010, y: 32, yValue: 50),
ChartSampleData(x: 2011, y: 44, yValue: 40),
ChartSampleData(x: 2012, y: 40, yValue: 60),
ChartSampleData(x: 2013, y: 50, yValue: 38),
ChartSampleData(x: 2014, y: 10, yValue: 28),
ChartSampleData(x: 2015, y: 20, yValue: 16),
ChartSampleData(x: 2016, y: 30, yValue: 50),
];
final List<ChartSampleData> audienceOverviewChart = [
ChartSampleData(x: 2018, y: 50, yValue: 38),
ChartSampleData(x: 2019, y: 10, yValue: 28),
ChartSampleData(x: 2020, y: 32, yValue: 50),
ChartSampleData(x: 2020, y: 44, yValue: 40),
ChartSampleData(x: 2020, y: 40, yValue: 60),
ChartSampleData(x: 2020, y: 50, yValue: 38),
ChartSampleData(x: 2021, y: 10, yValue: 28),
ChartSampleData(x: 2022, y: 20, yValue: 16),
ChartSampleData(x: 2023, y: 30, yValue: 50)
];
}

View File

@ -0,0 +1,258 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance_model.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employee_model.dart';
import 'package:marco/model/attendance_log_model.dart';
import 'package:marco/model/regularization_log_model.dart';
import 'package:marco/model/attendance_log_view_model.dart';
import 'package:logger/logger.dart';
final Logger log = Logger();
class AttendanceController extends GetxController {
List<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeModel> employees = [];
DateTime? startDateAttendance;
DateTime? endDateAttendance;
List<AttendanceLogModel> attendanceLogs = [];
List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = [];
RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
_initializeDefaults();
}
void _initializeDefaults() {
_setDefaultDateRange();
fetchProjects();
}
void _setDefaultDateRange() {
final today = DateTime.now();
startDateAttendance = today.subtract(const Duration(days: 7));
endDateAttendance = today;
log.i("Default date range set: $startDateAttendance to $endDateAttendance");
}
Future<void> fetchProjects() async {
isLoading.value = true;
final response = await ApiService.getProjects();
isLoading.value = false;
if (response != null && response.isNotEmpty) {
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length} projects loaded.");
await fetchProjectData(selectedProjectId);
update(['attendance_dashboard_controller']);
} else {
log.w("No project data found or API call failed.");
}
}
Future<void> fetchProjectData(String? projectId) async {
if (projectId == null) return;
isLoading.value = true;
await Future.wait([
fetchEmployeesByProject(projectId),
fetchAttendanceLogs(projectId,
dateFrom: startDateAttendance, dateTo: endDateAttendance),
fetchRegularizationLogs(projectId),
]);
isLoading.value = false;
log.i("Project data fetched for project ID: $projectId");
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null) return;
isLoading.value = true;
final response = await ApiService.getEmployeesByProject(projectId);
isLoading.value = false;
if (response != null) {
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
log.i(
"Employees fetched: ${employees.length} employees for project $projectId");
update();
} else {
log.e("Failed to fetch employees for project $projectId");
}
}
Future<bool> captureAndUploadAttendance(
String id,
String employeeId,
String projectId, {
String comment = "Marked via mobile app",
required int action,
bool imageCapture = true,
}) async {
try {
XFile? image;
if (imageCapture) {
image = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (image == null) {
log.w("Image capture cancelled.");
return false;
}
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
final imageName = imageCapture
? ApiService.generateImageName(employeeId, employees.length + 1)
: "";
final result = await ApiService.uploadAttendanceImage(
id,
employeeId,
image,
position.latitude,
position.longitude,
imageName: imageName,
projectId: projectId,
comment: comment,
action: action,
imageCapture: imageCapture,
);
log.i("Attendance uploaded for $employeeId, action: $action");
return result;
} catch (e, stacktrace) {
log.e("Error uploading attendance", error: e, stackTrace: stacktrace);
return false;
}
}
Future<void> selectDateRangeForAttendance(
BuildContext context,
AttendanceController controller,
) async {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2022),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(
start: startDateAttendance ??
DateTime.now().subtract(const Duration(days: 7)),
end: endDateAttendance ?? DateTime.now(),
),
);
if (picked != null) {
startDateAttendance = picked.start;
endDateAttendance = picked.end;
log.i("Date range selected: $startDateAttendance to $endDateAttendance");
await controller.fetchAttendanceLogs(
controller.selectedProjectId,
dateFrom: picked.start,
dateTo: picked.end,
);
}
}
Future<void> fetchAttendanceLogs(
String? projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return;
isLoading.value = true;
final response = await ApiService.getAttendanceLogs(
projectId,
dateFrom: dateFrom,
dateTo: dateTo,
);
isLoading.value = false;
if (response != null) {
attendanceLogs =
response.map((json) => AttendanceLogModel.fromJson(json)).toList();
log.i("Attendance logs fetched: ${attendanceLogs.length}");
update();
} else {
log.e("Failed to fetch attendance logs for project $projectId");
}
}
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final groupedLogs = <String, List<AttendanceLogModel>>{};
for (var logItem in attendanceLogs) {
final checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown';
groupedLogs.putIfAbsent(checkInDate, () => []);
groupedLogs[checkInDate]!.add(logItem);
}
log.i("Logs grouped by check-in date.");
return groupedLogs;
}
Future<void> fetchRegularizationLogs(
String? projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return;
isLoading.value = true;
final response = await ApiService.getRegularizationLogs(projectId);
isLoading.value = false;
if (response != null) {
regularizationLogs = response
.map((json) => RegularizationLogModel.fromJson(json))
.toList();
log.i("Regularization logs fetched: ${regularizationLogs.length}");
update();
} else {
log.e("Failed to fetch regularization logs for project $projectId");
}
}
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
isLoading.value = true;
final response = await ApiService.getAttendanceLogView(id);
isLoading.value = false;
if (response != null) {
attendenceLogsView = response
.map((json) => AttendanceLogViewModel.fromJson(json))
.toList();
log.i("Attendance log view fetched for ID: $id");
update();
} else {
log.e("Failed to fetch attendance log view for ID $id");
}
}
}

View File

@ -0,0 +1,35 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/lead_report_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class CrmController extends MyController {
List<ChartSampleData>? chartData;
List<LeadReportModel> leadReport = [];
TooltipBehavior? tooltipBehavior;
@override
void onInit() {
tooltipBehavior = TooltipBehavior(enable: true);
chartData = <ChartSampleData>[
ChartSampleData(x: 'Jan', y: 10, secondSeriesYValue: 5),
ChartSampleData(x: 'Feb', y: 12, secondSeriesYValue: 8),
ChartSampleData(x: 'Mar', y: 14, secondSeriesYValue: 9),
ChartSampleData(x: 'Apr', y: 11, secondSeriesYValue: 7),
ChartSampleData(x: 'May', y: 15, secondSeriesYValue: 10),
ChartSampleData(x: 'Jun', y: 9, secondSeriesYValue: 6),
ChartSampleData(x: 'Jul', y: 13, secondSeriesYValue: 7),
ChartSampleData(x: 'Aug', y: 12, secondSeriesYValue: 8),
ChartSampleData(x: 'Sep', y: 14, secondSeriesYValue: 10),
ChartSampleData(x: 'Oct', y: 15, secondSeriesYValue: 12),
ChartSampleData(x: 'Nov', y: 13, secondSeriesYValue: 9),
ChartSampleData(x: 'Dec', y: 11, secondSeriesYValue: 6),
];
LeadReportModel.dummyList.then((value) {
leadReport = value.sublist(0, 5);
update();
});
super.onInit();
}
}

View File

@ -0,0 +1,43 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/coin_growth_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class CryptoController extends MyController {
List<ChartSampleData>? chartData;
DateTimeIntervalType intervalType = DateTimeIntervalType.months;
List<CoinGrowthModel> coinGrowth = [];
bool enableSolidCandle = false;
TrackballBehavior? trackballBehavior;
@override
void onInit() {
CoinGrowthModel.dummyList.then((value) {
coinGrowth = value.sublist(0, 5);
update();
});
chartData = <ChartSampleData>[
ChartSampleData(x: 'Jan', y: 50, secondSeriesYValue: 40, thirdSeriesYValue: 45),
ChartSampleData(x: 'Feb', y: 47, secondSeriesYValue: 39, thirdSeriesYValue: 48),
ChartSampleData(x: 'Mar', y: 55, secondSeriesYValue: 42, thirdSeriesYValue: 50),
ChartSampleData(x: 'Apr', y: 60, secondSeriesYValue: 45, thirdSeriesYValue: 53),
ChartSampleData(x: 'May', y: 70, secondSeriesYValue: 50, thirdSeriesYValue: 58),
ChartSampleData(x: 'Jun', y: 75, secondSeriesYValue: 55, thirdSeriesYValue: 62),
ChartSampleData(x: 'Jul', y: 80, secondSeriesYValue: 58, thirdSeriesYValue: 65),
ChartSampleData(x: 'Aug', y: 78, secondSeriesYValue: 60, thirdSeriesYValue: 66),
ChartSampleData(x: 'Sep', y: 72, secondSeriesYValue: 55, thirdSeriesYValue: 64),
ChartSampleData(x: 'Oct', y: 65, secondSeriesYValue: 50, thirdSeriesYValue: 57),
ChartSampleData(x: 'Nov', y: 58, secondSeriesYValue: 45, thirdSeriesYValue: 53),
ChartSampleData(x: 'Dec', y: 50, secondSeriesYValue: 40, thirdSeriesYValue: 48)
];
super.onInit();
}
void onSelectIntervalType(DateTimeIntervalType interval) {
intervalType = interval;
update();
}
}

View File

@ -1,263 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/model/dashboard/project_progress_model.dart';
class DashboardController extends GetxController {
// =========================
// Attendance overview
// =========================
final RxList<Map<String, dynamic>> roleWiseData =
<Map<String, dynamic>>[].obs;
final RxString attendanceSelectedRange = '15D'.obs;
final RxBool attendanceIsChartView = true.obs;
final RxBool isAttendanceLoading = false.obs;
// =========================
// Project progress overview
// =========================
final RxList<ChartTaskData> projectChartData = <ChartTaskData>[].obs;
final RxString projectSelectedRange = '15D'.obs;
final RxBool projectIsChartView = true.obs;
final RxBool isProjectLoading = false.obs;
// =========================
// Projects overview
// =========================
final RxInt totalProjects = 0.obs;
final RxInt ongoingProjects = 0.obs;
final RxBool isProjectsLoading = false.obs;
// =========================
// Tasks overview
// =========================
final RxInt totalTasks = 0.obs;
final RxInt completedTasks = 0.obs;
final RxBool isTasksLoading = false.obs;
// =========================
// Teams overview
// =========================
final RxInt totalEmployees = 0.obs;
final RxInt inToday = 0.obs;
final RxBool isTeamsLoading = false.obs;
// Common ranges
final List<String> ranges = ['7D', '15D', '30D'];
// Inside your DashboardController
final ProjectController projectController =
Get.put(ProjectController(), permanent: true);
@override
void onInit() {
super.onInit();
logSafe(
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
level: LogLevel.info,
);
fetchAllDashboardData();
// React to project change
ever<String>(projectController.selectedProjectId, (id) {
fetchAllDashboardData();
});
// React to range changes
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
ever(projectSelectedRange, (_) => fetchProjectProgress());
}
// =========================
// Helper Methods
// =========================
int _getDaysFromRange(String range) {
switch (range) {
case '7D':
return 7;
case '15D':
return 15;
case '30D':
return 30;
case '3M':
return 90;
case '6M':
return 180;
default:
return 7;
}
}
int getAttendanceDays() => _getDaysFromRange(attendanceSelectedRange.value);
int getProjectDays() => _getDaysFromRange(projectSelectedRange.value);
void updateAttendanceRange(String range) {
attendanceSelectedRange.value = range;
logSafe('Attendance range updated to $range', level: LogLevel.debug);
}
void updateProjectRange(String range) {
projectSelectedRange.value = range;
logSafe('Project range updated to $range', level: LogLevel.debug);
}
void toggleAttendanceChartView(bool isChart) {
attendanceIsChartView.value = isChart;
logSafe('Attendance chart view toggled to: $isChart',
level: LogLevel.debug);
}
void toggleProjectChartView(bool isChart) {
projectIsChartView.value = isChart;
logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug);
}
// =========================
// Manual Refresh Methods
// =========================
Future<void> refreshDashboard() async {
logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug);
await fetchAllDashboardData();
}
Future<void> refreshAttendance() async => fetchRoleWiseAttendance();
Future<void> refreshTasks() async {
final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) await fetchDashboardTasks(projectId: projectId);
}
Future<void> refreshProjects() async => fetchProjectProgress();
// =========================
// Fetch All Dashboard Data
// =========================
Future<void> fetchAllDashboardData() async {
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) {
logSafe('No project selected. Skipping dashboard API calls.',
level: LogLevel.warning);
return;
}
await Future.wait([
fetchRoleWiseAttendance(),
fetchProjectProgress(),
fetchDashboardTasks(projectId: projectId),
fetchDashboardTeams(projectId: projectId),
]);
}
// =========================
// API Calls
// =========================
Future<void> fetchRoleWiseAttendance() async {
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
try {
isAttendanceLoading.value = true;
final List<dynamic>? response =
await ApiService.getDashboardAttendanceOverview(
projectId, getAttendanceDays());
if (response != null) {
roleWiseData.value =
response.map((e) => Map<String, dynamic>.from(e)).toList();
logSafe('Attendance overview fetched successfully.',
level: LogLevel.info);
} else {
roleWiseData.clear();
logSafe('Failed to fetch attendance overview: response is null.',
level: LogLevel.error);
}
} catch (e, st) {
roleWiseData.clear();
logSafe('Error fetching attendance overview',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isAttendanceLoading.value = false;
}
}
Future<void> fetchProjectProgress() async {
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
try {
isProjectLoading.value = true;
final response = await ApiService.getProjectProgress(
projectId: projectId, days: getProjectDays());
if (response != null && response.success) {
projectChartData.value =
response.data.map((d) => ChartTaskData.fromProjectData(d)).toList();
logSafe('Project progress data mapped for chart', level: LogLevel.info);
} else {
projectChartData.clear();
logSafe('Failed to fetch project progress', level: LogLevel.error);
}
} catch (e, st) {
projectChartData.clear();
logSafe('Error fetching project progress',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isProjectLoading.value = false;
}
}
Future<void> fetchDashboardTasks({required String projectId}) async {
if (projectId.isEmpty) return;
try {
isTasksLoading.value = true;
final response = await ApiService.getDashboardTasks(projectId: projectId);
if (response != null && response.success) {
totalTasks.value = response.data?.totalTasks ?? 0;
completedTasks.value = response.data?.completedTasks ?? 0;
logSafe('Dashboard tasks fetched', level: LogLevel.info);
} else {
totalTasks.value = 0;
completedTasks.value = 0;
logSafe('Failed to fetch tasks', level: LogLevel.error);
}
} catch (e, st) {
totalTasks.value = 0;
completedTasks.value = 0;
logSafe('Error fetching tasks',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isTasksLoading.value = false;
}
}
Future<void> fetchDashboardTeams({required String projectId}) async {
if (projectId.isEmpty) return;
try {
isTeamsLoading.value = true;
final response = await ApiService.getDashboardTeams(projectId: projectId);
if (response != null && response.success) {
totalEmployees.value = response.data?.totalEmployees ?? 0;
inToday.value = response.data?.inToday ?? 0;
logSafe('Dashboard teams fetched', level: LogLevel.info);
} else {
totalEmployees.value = 0;
inToday.value = 0;
logSafe('Failed to fetch teams', level: LogLevel.error);
}
} catch (e, st) {
totalEmployees.value = 0;
inToday.value = 0;
logSafe('Error fetching teams',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isTeamsLoading.value = false;
}
}
}

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/product_order_modal.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class EcommerceController extends MyController {
List<ChartSampleData>? salesAnalyticsData;
List<ProductOrderModal> order = [];
String selectedTimeByLocation = "Year";
@override
void onInit() {
ProductOrderModal.dummyList.then((value) {
order = value.sublist(0, 5);
update();
});
salesAnalyticsData = <ChartSampleData>[
ChartSampleData(x: 'Jan', y: 43, secondSeriesYValue: 37, thirdSeriesYValue: 41),
ChartSampleData(x: 'Feb', y: 45, secondSeriesYValue: 37, thirdSeriesYValue: 45),
ChartSampleData(x: 'Mar', y: 50, secondSeriesYValue: 39, thirdSeriesYValue: 48),
ChartSampleData(x: 'Apr', y: 55, secondSeriesYValue: 43, thirdSeriesYValue: 52),
ChartSampleData(x: 'May', y: 63, secondSeriesYValue: 48, thirdSeriesYValue: 57),
ChartSampleData(x: 'Jun', y: 68, secondSeriesYValue: 54, thirdSeriesYValue: 61),
ChartSampleData(x: 'Jul', y: 72, secondSeriesYValue: 57, thirdSeriesYValue: 66),
ChartSampleData(x: 'Aug', y: 70, secondSeriesYValue: 57, thirdSeriesYValue: 66),
ChartSampleData(x: 'Sep', y: 66, secondSeriesYValue: 54, thirdSeriesYValue: 63),
ChartSampleData(x: 'Oct', y: 57, secondSeriesYValue: 48, thirdSeriesYValue: 55),
ChartSampleData(x: 'Nov', y: 50, secondSeriesYValue: 43, thirdSeriesYValue: 50),
ChartSampleData(x: 'Dec', y: 45, secondSeriesYValue: 37, thirdSeriesYValue: 45)
];
super.onInit();
}
final List<ChartSampleData> chartData = [
ChartSampleData(x: 'Jan', y: 10, yValue: 1000),
ChartSampleData(x: 'Fab', y: 20, yValue: 2000),
ChartSampleData(x: 'Mar', y: 15, yValue: 1500),
ChartSampleData(x: 'Jun', y: 5, yValue: 500),
ChartSampleData(x: 'Jul', y: 30, yValue: 3000),
ChartSampleData(x: 'Aug', y: 20, yValue: 2000),
ChartSampleData(x: 'Sep', y: 40, yValue: 4000),
ChartSampleData(x: 'Oct', y: 60, yValue: 6000),
ChartSampleData(x: 'Nov', y: 55, yValue: 5500),
ChartSampleData(x: 'Dec', y: 38, yValue: 3000),
];
final TooltipBehavior chart = TooltipBehavior(
enable: true,
format: 'point.x : point.yValue1 : point.yValue2',
);
final List<ChartSampleData> circleChart = [
ChartSampleData(x: 'David', y: 25, pointColor: const Color.fromRGBO(9, 0, 136, 1)),
ChartSampleData(x: 'Steve', y: 38, pointColor: const Color.fromRGBO(147, 0, 119, 1)),
ChartSampleData(x: 'Jack', y: 34, pointColor: const Color.fromRGBO(228, 0, 124, 1)),
ChartSampleData(x: 'Others', y: 52, pointColor: const Color.fromRGBO(255, 189, 57, 1))
];
void onSelectedTimeByLocation(String time) {
selectedTimeByLocation = time;
update();
}
@override
void dispose() {
salesAnalyticsData!.clear();
super.dispose();
}
}

View File

@ -0,0 +1,42 @@
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 JobController 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();
}
}

View File

@ -0,0 +1,46 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/project_summary_model.dart';
import 'package:marco/model/task_list_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class ProjectController extends MyController {
TooltipBehavior? tooltipBehavior;
List<TaskListModel> task = [];
List<ProjectSummaryModel> projectSummary = [];
List<ChartSampleData>? chartData;
@override
void onInit() {
TaskListModel.dummyList.then((value) {
task = value;
update();
});
ProjectSummaryModel.dummyList.then((value) {
projectSummary = value.sublist(0, 5);
update();
});
chartData = <ChartSampleData>[
ChartSampleData(x: 'Jan', y: 10, secondSeriesYValue: 8, thirdSeriesYValue: 12),
ChartSampleData(x: 'Feb', y: 5, secondSeriesYValue: 6, thirdSeriesYValue: 7),
ChartSampleData(x: 'Mar', y: 11, secondSeriesYValue: 9, thirdSeriesYValue: 6),
ChartSampleData(x: 'Apr', y: 14, secondSeriesYValue: 10, thirdSeriesYValue: 13),
ChartSampleData(x: 'May', y: 9, secondSeriesYValue: 7, thirdSeriesYValue: 5),
ChartSampleData(x: 'Jun', y: 8, secondSeriesYValue: 12, thirdSeriesYValue: 11),
ChartSampleData(x: 'Jul', y: 12, secondSeriesYValue: 11, thirdSeriesYValue: 9),
ChartSampleData(x: 'Aug', y: 7, secondSeriesYValue: 13, thirdSeriesYValue: 10),
ChartSampleData(x: 'Sep', y: 6, secondSeriesYValue: 5, thirdSeriesYValue: 8),
ChartSampleData(x: 'Oct', y: 4, secondSeriesYValue: 14, thirdSeriesYValue: 15),
ChartSampleData(x: 'Nov', y: 13, secondSeriesYValue: 4, thirdSeriesYValue: 11),
ChartSampleData(x: 'Dec', y: 15, secondSeriesYValue: 3, thirdSeriesYValue: 4)
];
tooltipBehavior = TooltipBehavior(enable: true, format: 'point.x : point.ym');
super.onInit();
}
void onSelectTask(TaskListModel task) {
task.isSelectTask = !task.isSelectTask;
update();
}
}

View File

@ -0,0 +1,56 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/recent_order_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class SalesController extends MyController {
List<ChartData>? statisticsData;
List<RecentOrderModel> recentOrder = [];
@override
void onInit() {
RecentOrderModel.dummyList.then((value) {
recentOrder = value;
update();
});
statisticsData = <ChartData>[
ChartData(2005, 15, 25),
ChartData(2006, 40, 55),
ChartData(2007, 50, 70),
ChartData(2008, 55, 80),
ChartData(2009, 65, 85),
ChartData(2010, 70, 95),
ChartData(2011, 90, 110)
];
super.onInit();
}
final List<ChartSampleData> visitorChartData = [
ChartSampleData(x: 'Jan', y: 12, yValue: 1200),
ChartSampleData(x: 'Feb', y: 18, yValue: 1800),
ChartSampleData(x: 'Mar', y: 22, yValue: 2200),
ChartSampleData(x: 'Apr', y: 10, yValue: 1000),
ChartSampleData(x: 'May', y: 25, yValue: 2500),
ChartSampleData(x: 'Jun', y: 35, yValue: 3500),
ChartSampleData(x: 'Jul', y: 28, yValue: 2800),
ChartSampleData(x: 'Aug', y: 45, yValue: 4500),
ChartSampleData(x: 'Sep', y: 50, yValue: 5000),
ChartSampleData(x: 'Oct', y: 60, yValue: 6000),
ChartSampleData(x: 'Nov', y: 42, yValue: 4200),
ChartSampleData(x: 'Dec', y: 55, yValue: 5500),
];
final TooltipBehavior visitorChart = TooltipBehavior(
enable: true,
format: 'point.x : point.yValue1 : point.yValue2',
);
}
class ChartData {
ChartData(this.x, this.y, this.y2);
final double x;
final double y;
final double y2;
}

View File

@ -1,71 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/directory/directory_controller.dart';
import 'package:marco/controller/directory/notes_controller.dart';
class AddCommentController extends GetxController {
final String contactId;
AddCommentController({required this.contactId});
final RxString note = ''.obs;
final RxBool isSubmitting = false.obs;
Future<void> submitComment() async {
if (note.value.trim().isEmpty) {
showAppSnackbar(
title: "Missing Comment",
message: "Please enter a comment before submitting.",
type: SnackbarType.warning,
);
return;
}
isSubmitting.value = true;
try {
logSafe("Submitting comment for contactId: $contactId");
final success = await ApiService.addContactComment(
note.value.trim(),
contactId,
);
if (success) {
logSafe("Comment added successfully.");
// Refresh UI
final directoryController = Get.find<DirectoryController>();
await directoryController.fetchCommentsForContact(contactId);
final notesController = Get.find<NotesController>();
await notesController.fetchNotes(
pageSize: 1000, pageNumber: 1); // Fixed here
Get.back(result: true);
showAppSnackbar(
title: "Comment Added",
message: "Your comment has been successfully added.",
type: SnackbarType.success,
);
}
} catch (e) {
logSafe("Error while submitting comment: $e", level: LogLevel.error);
showAppSnackbar(
title: "Unexpected Error",
message: "Something went wrong while adding your comment.",
type: SnackbarType.error,
);
} finally {
isSubmitting.value = false;
}
}
void updateNote(String value) {
note.value = value;
logSafe("Note updated: ${value.trim()}");
}
}

View File

@ -1,316 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class AddContactController extends GetxController {
final RxList<String> categories = <String>[].obs;
final RxList<String> buckets = <String>[].obs;
final RxList<String> globalProjects = <String>[].obs;
final RxList<String> tags = <String>[].obs;
final RxString selectedCategory = ''.obs;
final RxList<String> selectedBuckets = <String>[].obs;
final RxString selectedProject = ''.obs;
final RxList<String> enteredTags = <String>[].obs;
final RxList<String> filteredSuggestions = <String>[].obs;
final RxList<String> organizationNames = <String>[].obs;
final RxList<String> filteredOrgSuggestions = <String>[].obs;
final RxMap<String, String> categoriesMap = <String, String>{}.obs;
final RxMap<String, String> bucketsMap = <String, String>{}.obs;
final RxMap<String, String> projectsMap = <String, String>{}.obs;
final RxMap<String, String> tagsMap = <String, String>{}.obs;
final RxBool isInitialized = false.obs;
final RxList<String> selectedProjects = <String>[].obs;
final RxBool isSubmitting = false.obs;
@override
void onInit() {
super.onInit();
logSafe("AddContactController initialized", level: LogLevel.debug);
fetchInitialData();
}
Future<void> fetchInitialData() async {
logSafe("Fetching initial dropdown data", level: LogLevel.debug);
await Future.wait([
fetchBuckets(),
fetchGlobalProjects(),
fetchTags(),
fetchCategories(),
fetchOrganizationNames(),
]);
// Mark initialization as done
isInitialized.value = true;
}
void resetForm() {
selectedCategory.value = '';
selectedProject.value = '';
selectedBuckets.clear();
enteredTags.clear();
filteredSuggestions.clear();
filteredOrgSuggestions.clear();
selectedProjects.clear();
}
Future<void> fetchBuckets() async {
try {
final response = await ApiService.getContactBucketList();
if (response != null && response['data'] is List) {
final names = <String>[];
for (var item in response['data']) {
if (item['name'] != null && item['id'] != null) {
bucketsMap[item['name']] = item['id'].toString();
names.add(item['name']);
}
}
buckets.assignAll(names);
logSafe("Fetched \${names.length} buckets");
}
} catch (e) {
logSafe("Failed to fetch buckets: \$e", level: LogLevel.error);
}
}
Future<void> fetchOrganizationNames() async {
try {
final orgs = await ApiService.getOrganizationList();
organizationNames.assignAll(orgs);
logSafe("Fetched \${orgs.length} organization names");
} catch (e) {
logSafe("Failed to load organization names: \$e", level: LogLevel.error);
}
}
Future<void> submitContact({
String? id,
required String name,
required String organization,
required List<Map<String, String>> emails,
required List<Map<String, String>> phones,
required String address,
required String description,
String? designation,
}) async {
if (isSubmitting.value) return;
isSubmitting.value = true;
final categoryId = categoriesMap[selectedCategory.value];
final bucketIds = selectedBuckets
.map((name) => bucketsMap[name])
.whereType<String>()
.toList();
if (bucketIds.isEmpty) {
showAppSnackbar(
title: "Missing Buckets",
message: "Please select at least one bucket.",
type: SnackbarType.warning,
);
isSubmitting.value = false;
return;
}
final projectIds = selectedProjects
.map((name) => projectsMap[name])
.whereType<String>()
.toList();
if (name.trim().isEmpty) {
showAppSnackbar(
title: "Missing Name",
message: "Please enter the contact name.",
type: SnackbarType.warning,
);
isSubmitting.value = false;
return;
}
if (organization.trim().isEmpty) {
showAppSnackbar(
title: "Missing Organization",
message: "Please enter the organization name.",
type: SnackbarType.warning,
);
isSubmitting.value = false;
return;
}
if (selectedBuckets.isEmpty) {
showAppSnackbar(
title: "Missing Bucket",
message: "Please select at least one bucket.",
type: SnackbarType.warning,
);
isSubmitting.value = false;
return;
}
try {
final tagObjects = enteredTags.map((tagName) {
final tagId = tagsMap[tagName];
return tagId != null
? {"id": tagId, "name": tagName}
: {"name": tagName};
}).toList();
final body = {
if (id != null) "id": id,
"name": name.trim(),
"organization": organization.trim(),
if (selectedCategory.value.isNotEmpty && categoryId != null)
"contactCategoryId": categoryId,
if (projectIds.isNotEmpty) "projectIds": projectIds,
"bucketIds": bucketIds,
if (enteredTags.isNotEmpty) "tags": tagObjects,
if (emails.isNotEmpty) "contactEmails": emails,
if (phones.isNotEmpty) "contactPhones": phones,
if (address.trim().isNotEmpty) "address": address.trim(),
if (description.trim().isNotEmpty) "description": description.trim(),
if (designation != null && designation.trim().isNotEmpty)
"designation": designation.trim(),
};
logSafe("${id != null ? 'Updating' : 'Creating'} contact");
final response = id != null
? await ApiService.updateContact(id, body)
: await ApiService.createContact(body);
if (response == true) {
Get.back(result: true);
showAppSnackbar(
title: "Success",
message: id != null
? "Contact updated successfully"
: "Contact created successfully",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to ${id != null ? 'update' : 'create'} contact",
type: SnackbarType.error,
);
}
} catch (e) {
logSafe("Submit contact error: $e", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Something went wrong",
type: SnackbarType.error,
);
} finally {
isSubmitting.value = false;
}
}
void filterOrganizationSuggestions(String query) {
if (query.trim().isEmpty) {
filteredOrgSuggestions.clear();
return;
}
final lower = query.toLowerCase();
filteredOrgSuggestions.assignAll(
organizationNames
.where((name) => name.toLowerCase().contains(lower))
.toList(),
);
logSafe("Filtered organization suggestions for: \$query",
level: LogLevel.debug);
}
Future<void> fetchGlobalProjects() async {
try {
final response = await ApiService.getGlobalProjects();
if (response != null) {
final names = <String>[];
for (var item in response) {
final name = item['name']?.toString().trim();
final id = item['id']?.toString().trim();
if (name != null && id != null && name.isNotEmpty) {
projectsMap[name] = id;
names.add(name);
}
}
globalProjects.assignAll(names);
logSafe("Fetched \${names.length} global projects");
}
} catch (e) {
logSafe("Failed to fetch global projects: \$e", level: LogLevel.error);
}
}
Future<void> fetchTags() async {
try {
final response = await ApiService.getContactTagList();
if (response != null && response['data'] is List) {
tags.assignAll(List<String>.from(
response['data'].map((e) => e['name'] ?? '').where((e) => e != ''),
));
logSafe("Fetched \${tags.length} tags");
}
} catch (e) {
logSafe("Failed to fetch tags: \$e", level: LogLevel.error);
}
}
void filterSuggestions(String query) {
if (query.trim().isEmpty) {
filteredSuggestions.clear();
return;
}
final lower = query.toLowerCase();
filteredSuggestions.assignAll(
tags
.where((tag) =>
tag.toLowerCase().contains(lower) && !enteredTags.contains(tag))
.toList(),
);
logSafe("Filtered tag suggestions for: \$query", level: LogLevel.debug);
}
void clearSuggestions() {
filteredSuggestions.clear();
logSafe("Cleared tag suggestions", level: LogLevel.debug);
}
Future<void> fetchCategories() async {
try {
final response = await ApiService.getContactCategoryList();
if (response != null && response['data'] is List) {
final names = <String>[];
for (var item in response['data']) {
final name = item['name']?.toString().trim();
final id = item['id']?.toString().trim();
if (name != null && id != null && name.isNotEmpty) {
categoriesMap[name] = id;
names.add(name);
}
}
categories.assignAll(names);
logSafe("Fetched \${names.length} contact categories");
}
} catch (e) {
logSafe("Failed to fetch categories: \$e", level: LogLevel.error);
}
}
void addEnteredTag(String tag) {
if (tag.trim().isNotEmpty && !enteredTags.contains(tag.trim())) {
enteredTags.add(tag.trim());
logSafe("Added tag: \$tag", level: LogLevel.debug);
}
}
void removeEnteredTag(String tag) {
enteredTags.remove(tag);
logSafe("Removed tag: \$tag", level: LogLevel.debug);
}
}

View File

@ -1,70 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class BucketController extends GetxController {
RxBool isCreating = false.obs;
final RxString name = ''.obs;
final RxString description = ''.obs;
Future<void> createBucket() async {
if (name.value.trim().isEmpty) {
showAppSnackbar(
title: "Missing Name",
message: "Bucket name is required.",
type: SnackbarType.warning,
);
return;
}
isCreating.value = true;
try {
logSafe("Creating bucket: ${name.value}");
final success = await ApiService.createBucket(
name: name.value.trim(),
description: description.value.trim(),
);
if (success) {
logSafe("Bucket created successfully");
Get.back(result: true); // Close bottom sheet/dialog
showAppSnackbar(
title: "Success",
message: "Bucket has been created successfully.",
type: SnackbarType.success,
);
} else {
logSafe("Bucket creation failed", level: LogLevel.error);
showAppSnackbar(
title: "Creation Failed",
message: "Unable to create bucket. Please try again later.",
type: SnackbarType.error,
);
}
} catch (e) {
logSafe("Error during bucket creation: $e", level: LogLevel.error);
showAppSnackbar(
title: "Unexpected Error",
message: "Something went wrong. Please try again.",
type: SnackbarType.error,
);
} finally {
isCreating.value = false;
}
}
void updateName(String value) {
name.value = value;
logSafe("Bucket name updated: ${value.trim()}");
}
void updateDescription(String value) {
description.value = value;
logSafe("Bucket description updated: ${value.trim()}");
}
}

View File

@ -1,390 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/directory/contact_model.dart';
import 'package:marco/model/directory/contact_bucket_list_model.dart';
import 'package:marco/model/directory/directory_comment_model.dart';
class DirectoryController extends GetxController {
// -------------------- CONTACTS --------------------
RxList<ContactModel> allContacts = <ContactModel>[].obs;
RxList<ContactModel> filteredContacts = <ContactModel>[].obs;
RxList<ContactCategory> contactCategories = <ContactCategory>[].obs;
RxList<String> selectedCategories = <String>[].obs;
RxList<String> selectedBuckets = <String>[].obs;
RxBool isActive = true.obs;
RxBool isLoading = false.obs;
RxList<ContactBucket> contactBuckets = <ContactBucket>[].obs;
RxString searchQuery = ''.obs;
// -------------------- COMMENTS --------------------
final Map<String, RxList<DirectoryComment>> activeCommentsMap = {};
final Map<String, RxList<DirectoryComment>> inactiveCommentsMap = {};
final editingCommentId = Rxn<String>();
@override
void onInit() {
super.onInit();
fetchContacts();
fetchBuckets();
}
// -------------------- COMMENTS HANDLING --------------------
RxList<DirectoryComment> getCommentsForContact(String contactId,
{bool active = true}) {
return active
? activeCommentsMap[contactId] ?? <DirectoryComment>[].obs
: inactiveCommentsMap[contactId] ?? <DirectoryComment>[].obs;
}
Future<void> fetchCommentsForContact(String contactId,
{bool active = true}) async {
try {
final data =
await ApiService.getDirectoryComments(contactId, active: active);
var comments =
data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? [];
// Deduplicate by ID before storing
final Map<String, DirectoryComment> uniqueMap = {
for (var c in comments) c.id: c,
};
comments = uniqueMap.values.toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
if (active) {
activeCommentsMap[contactId] = <DirectoryComment>[].obs
..assignAll(comments);
} else {
inactiveCommentsMap[contactId] = <DirectoryComment>[].obs
..assignAll(comments);
}
} catch (e, stack) {
logSafe("Error fetching ${active ? 'active' : 'inactive'} comments: $e",
level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
if (active) {
activeCommentsMap[contactId] = <DirectoryComment>[].obs;
} else {
inactiveCommentsMap[contactId] = <DirectoryComment>[].obs;
}
}
}
List<DirectoryComment> combinedComments(String contactId) {
final activeList = getCommentsForContact(contactId, active: true);
final inactiveList = getCommentsForContact(contactId, active: false);
// Deduplicate by ID (active wins)
final Map<String, DirectoryComment> byId = {};
for (final c in inactiveList) {
byId[c.id] = c;
}
for (final c in activeList) {
byId[c.id] = c;
}
final combined = byId.values.toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
return combined;
}
Future<void> updateComment(DirectoryComment comment) async {
try {
final existing = getCommentsForContact(comment.contactId)
.firstWhereOrNull((c) => c.id == comment.id);
if (existing != null && existing.note.trim() == comment.note.trim()) {
showAppSnackbar(
title: "No Changes",
message: "No changes were made to the comment.",
type: SnackbarType.info,
);
return;
}
final success = await ApiService.updateContactComment(
comment.id, comment.note, comment.contactId);
if (success) {
await fetchCommentsForContact(comment.contactId, active: true);
await fetchCommentsForContact(comment.contactId, active: false);
showAppSnackbar(
title: "Success",
message: "Comment updated successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to update comment.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Update comment failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Failed to update comment.",
type: SnackbarType.error,
);
}
}
Future<void> deleteComment(String commentId, String contactId) async {
try {
final success = await ApiService.restoreContactComment(commentId, false);
if (success) {
if (editingCommentId.value == commentId) editingCommentId.value = null;
await fetchCommentsForContact(contactId, active: true);
await fetchCommentsForContact(contactId, active: false);
showAppSnackbar(
title: "Deleted",
message: "Comment deleted successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to delete comment.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Delete comment failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while deleting comment.",
type: SnackbarType.error,
);
}
}
Future<void> restoreComment(String commentId, String contactId) async {
try {
final success = await ApiService.restoreContactComment(commentId, true);
if (success) {
await fetchCommentsForContact(contactId, active: true);
await fetchCommentsForContact(contactId, active: false);
showAppSnackbar(
title: "Restored",
message: "Comment restored successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to restore comment.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Restore comment failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while restoring comment.",
type: SnackbarType.error,
);
}
}
// -------------------- CONTACTS HANDLING --------------------
Future<void> fetchBuckets() async {
try {
final response = await ApiService.getContactBucketList();
if (response != null && response['data'] is List) {
final buckets = (response['data'] as List)
.map((e) => ContactBucket.fromJson(e))
.toList();
contactBuckets.assignAll(buckets);
} else {
contactBuckets.clear();
}
} catch (e) {
logSafe("Bucket fetch error: $e", level: LogLevel.error);
}
}
// -------------------- CONTACT DELETION / RESTORE --------------------
Future<void> deleteContact(String contactId) async {
try {
final success = await ApiService.deleteDirectoryContact(contactId);
if (success) {
// Refresh contacts after deletion
await fetchContacts(active: true);
await fetchContacts(active: false);
showAppSnackbar(
title: "Deleted",
message: "Contact deleted successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to delete contact.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Delete contact failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while deleting contact.",
type: SnackbarType.error,
);
}
}
Future<void> restoreContact(String contactId) async {
try {
final success = await ApiService.restoreDirectoryContact(contactId);
if (success) {
// Refresh contacts after restore
await fetchContacts(active: true);
await fetchContacts(active: false);
showAppSnackbar(
title: "Restored",
message: "Contact restored successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to restore contact.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Restore contact failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while restoring contact.",
type: SnackbarType.error,
);
}
}
Future<void> fetchContacts({bool active = true}) async {
try {
isLoading.value = true;
final response = await ApiService.getDirectoryData(isActive: active);
if (response != null) {
final contacts = response.map((e) => ContactModel.fromJson(e)).toList();
allContacts.assignAll(contacts);
extractCategoriesFromContacts();
applyFilters();
} else {
allContacts.clear();
filteredContacts.clear();
}
} catch (e) {
logSafe("Directory fetch error: $e", level: LogLevel.error);
} finally {
isLoading.value = false;
}
}
void extractCategoriesFromContacts() {
final uniqueCategories = <String, ContactCategory>{};
for (final contact in allContacts) {
final category = contact.contactCategory;
if (category != null) {
uniqueCategories.putIfAbsent(category.id, () => category);
}
}
contactCategories.value = uniqueCategories.values.toList();
}
void applyFilters() {
final query = searchQuery.value.toLowerCase();
filteredContacts.value = allContacts.where((contact) {
final categoryMatch = selectedCategories.isEmpty ||
(contact.contactCategory != null &&
selectedCategories.contains(contact.contactCategory!.id));
final bucketMatch = selectedBuckets.isEmpty ||
contact.bucketIds.any((id) => selectedBuckets.contains(id));
final nameMatch = contact.name.toLowerCase().contains(query);
final orgMatch = contact.organization.toLowerCase().contains(query);
final emailMatch = contact.contactEmails
.any((e) => e.emailAddress.toLowerCase().contains(query));
final phoneMatch = contact.contactPhones
.any((p) => p.phoneNumber.toLowerCase().contains(query));
final tagMatch =
contact.tags.any((tag) => tag.name.toLowerCase().contains(query));
final categoryNameMatch =
contact.contactCategory?.name.toLowerCase().contains(query) ?? false;
final bucketNameMatch = contact.bucketIds.any((id) {
final bucketName = contactBuckets
.firstWhereOrNull((b) => b.id == id)
?.name
.toLowerCase() ??
'';
return bucketName.contains(query);
});
final searchMatch = query.isEmpty ||
nameMatch ||
orgMatch ||
emailMatch ||
phoneMatch ||
tagMatch ||
categoryNameMatch ||
bucketNameMatch;
return categoryMatch && bucketMatch && searchMatch;
}).toList();
filteredContacts
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
}
void toggleCategory(String categoryId) {
if (selectedCategories.contains(categoryId)) {
selectedCategories.remove(categoryId);
} else {
selectedCategories.add(categoryId);
}
}
void toggleBucket(String bucketId) {
if (selectedBuckets.contains(bucketId)) {
selectedBuckets.remove(bucketId);
} else {
selectedBuckets.add(bucketId);
}
}
void updateSearchQuery(String value) {
searchQuery.value = value;
applyFilters();
}
String getBucketNames(ContactModel contact, List<ContactBucket> allBuckets) {
return contact.bucketIds
.map((id) => allBuckets.firstWhereOrNull((b) => b.id == id)?.name ?? '')
.where((name) => name.isNotEmpty)
.join(', ');
}
bool hasActiveFilters() {
return selectedCategories.isNotEmpty ||
selectedBuckets.isNotEmpty ||
searchQuery.value.trim().isNotEmpty;
}
}

View File

@ -1,152 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/directory/directory_controller.dart';
class ManageBucketController extends GetxController {
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
RxBool isLoading = false.obs;
final DirectoryController directoryController = Get.find();
@override
void onInit() {
super.onInit();
fetchAllEmployees();
}
Future<bool> updateBucket({
required String id,
required String name,
required String description,
required List<String> employeeIds,
required List<String> originalEmployeeIds,
}) async {
isLoading(true);
update();
try {
final updated = await ApiService.updateBucket(
id: id,
name: name,
description: description,
);
if (!updated) {
showAppSnackbar(
title: "Update Failed",
message: "Unable to update bucket details.",
type: SnackbarType.error,
);
isLoading(false);
update();
return false;
}
final allInvolvedIds = {...originalEmployeeIds, ...employeeIds}.toList();
final assignPayload = allInvolvedIds.map((empId) {
return {
"employeeId": empId,
"isActive": employeeIds.contains(empId),
};
}).toList();
final assigned = await ApiService.assignEmployeesToBucket(
bucketId: id,
employees: assignPayload,
);
if (!assigned) {
showAppSnackbar(
title: "Assignment Failed",
message: "Employees couldn't be updated.",
type: SnackbarType.warning,
);
} else {
showAppSnackbar(
title: "Success",
message: "Bucket updated successfully.",
type: SnackbarType.success,
);
}
return true;
} catch (e, stack) {
logSafe("Error in updateBucket: $e", level: LogLevel.error);
logSafe("Stack: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Unexpected Error",
message: "Please try again later.",
type: SnackbarType.error,
);
return false;
} finally {
isLoading(false);
update();
}
}
Future<void> fetchAllEmployees() async {
isLoading.value = true;
try {
final response = await ApiService.getAllEmployees();
if (response != null && response.isNotEmpty) {
allEmployees.assignAll(
response.map((json) => EmployeeModel.fromJson(json)));
logSafe(
"All Employees fetched for Manage Bucket: ${allEmployees.length}",
level: LogLevel.info,
);
} else {
allEmployees.clear();
logSafe("No employees found for Manage Bucket.",
level: LogLevel.warning);
}
} catch (e) {
allEmployees.clear();
logSafe("Error fetching employees in Manage Bucket",
level: LogLevel.error, error: e);
}
isLoading.value = false;
update();
}
Future<void> deleteBucket(String bucketId) async {
isLoading.value = true;
update();
try {
final deleted = await ApiService.deleteBucket(bucketId);
if (deleted) {
directoryController.contactBuckets.removeWhere((b) => b.id == bucketId);
showAppSnackbar(
title: "Deleted",
message: "Bucket deleted successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Delete Failed",
message: "Unable to delete bucket.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Error deleting bucket: $e", level: LogLevel.error);
logSafe("Stack: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Unexpected Error",
message: "Failed to delete bucket.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
update();
}
}
}

View File

@ -1,169 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/directory/note_list_response_model.dart';
class NotesController extends GetxController {
RxList<NoteModel> notesList = <NoteModel>[].obs;
RxBool isLoading = false.obs;
RxnString editingNoteId = RxnString();
RxString searchQuery = ''.obs;
List<NoteModel> get filteredNotesList {
if (searchQuery.isEmpty) return notesList;
final query = searchQuery.value.toLowerCase();
return notesList.where((note) {
return note.note.toLowerCase().contains(query) ||
note.contactName.toLowerCase().contains(query) ||
note.organizationName.toLowerCase().contains(query) ||
note.createdBy.firstName.toLowerCase().contains(query);
}).toList();
}
@override
void onInit() {
super.onInit();
fetchNotes();
}
Future<void> fetchNotes({int pageSize = 1000, int pageNumber = 1}) async {
isLoading.value = true;
logSafe(
"📤 Fetching directory notes with pageSize=$pageSize & pageNumber=$pageNumber");
try {
final response = await ApiService.getDirectoryNotes(
pageSize: pageSize, pageNumber: pageNumber);
logSafe("💡 Directory Notes Response: $response");
if (response == null) {
logSafe("⚠️ Response is null while fetching directory notes");
notesList.clear();
} else {
logSafe("💡 Directory Notes Response: $response");
notesList.value = NotePaginationData.fromJson(response).data;
}
} catch (e, st) {
logSafe("💥 Error occurred while fetching directory notes",
error: e, stackTrace: st);
notesList.clear();
} finally {
isLoading.value = false;
}
}
Future<void> updateNote(NoteModel updatedNote) async {
try {
logSafe(
"Attempting to update note. id: ${updatedNote.id}, contactId: ${updatedNote.contactId}");
final oldNote = notesList.firstWhereOrNull((n) => n.id == updatedNote.id);
if (oldNote != null && oldNote.note.trim() == updatedNote.note.trim()) {
logSafe("No changes detected in note. id: ${updatedNote.id}");
showAppSnackbar(
title: "No Changes",
message: "No changes were made to the note.",
type: SnackbarType.info,
);
return;
}
final success = await ApiService.updateContactComment(
updatedNote.id,
updatedNote.note,
updatedNote.contactId,
);
if (success) {
logSafe("Note updated successfully. id: ${updatedNote.id}");
final index = notesList.indexWhere((n) => n.id == updatedNote.id);
if (index != -1) {
notesList[index] = updatedNote;
notesList.refresh();
}
showAppSnackbar(
title: "Success",
message: "Note updated successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to update note.",
type: SnackbarType.error,
);
}
} catch (e, stackTrace) {
logSafe("Update note failed: ${e.toString()}");
logSafe("StackTrace: ${stackTrace.toString()}");
showAppSnackbar(
title: "Error",
message: "Failed to update note.",
type: SnackbarType.error,
);
}
}
Future<void> restoreOrDeleteNote(NoteModel note,
{bool restore = true}) async {
final action = restore ? "restore" : "delete";
try {
logSafe("Attempting to $action note id: ${note.id}");
final success = await ApiService.restoreContactComment(
note.id,
restore, // true = restore, false = delete
);
if (success) {
final index = notesList.indexWhere((n) => n.id == note.id);
if (index != -1) {
notesList[index] = note.copyWith(isActive: restore);
notesList.refresh();
}
showAppSnackbar(
title: restore ? "Restored" : "Deleted",
message: restore
? "Note has been restored successfully."
: "Note has been deleted successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message:
restore ? "Failed to restore note." : "Failed to delete note.",
type: SnackbarType.error,
);
}
} catch (e, st) {
logSafe("$action note failed: $e", error: e, stackTrace: st);
showAppSnackbar(
title: "Error",
message: "Something went wrong while trying to $action the note.",
type: SnackbarType.error,
);
}
}
void addNote(NoteModel note) {
notesList.insert(0, note);
logSafe("Note added to list");
}
void deleteNote(int index) {
if (index >= 0 && index < notesList.length) {
notesList.removeAt(index);
logSafe("Note removed from list at index $index");
}
}
void clearAllNotes() {
notesList.clear();
logSafe("All notes cleared from list");
}
}

View File

@ -1,82 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/document/document_details_model.dart';
import 'package:marco/model/document/document_version_model.dart';
class DocumentDetailsController extends GetxController {
/// Observables
var isLoading = false.obs;
var documentDetails = Rxn<DocumentDetailsResponse>();
var versions = <DocumentVersionItem>[].obs;
var isVersionsLoading = false.obs;
// Loading states for buttons
var isVerifyLoading = false.obs;
var isRejectLoading = false.obs;
/// Fetch document details by id
Future<void> fetchDocumentDetails(String documentId) async {
try {
isLoading.value = true;
final response = await ApiService.getDocumentDetailsApi(documentId);
documentDetails.value = response;
} finally {
isLoading.value = false;
}
}
/// Fetch document versions by parentAttachmentId
Future<void> fetchDocumentVersions(String parentAttachmentId) async {
try {
isVersionsLoading.value = true;
final response = await ApiService.getDocumentVersionsApi(
parentAttachmentId: parentAttachmentId,
);
if (response != null) {
versions.assignAll(response.data.data);
} else {
versions.clear();
}
} finally {
isVersionsLoading.value = false;
}
}
/// Verify document
Future<bool> verifyDocument(String documentId) async {
try {
isVerifyLoading.value = true;
final result =
await ApiService.verifyDocumentApi(id: documentId, isVerify: true);
if (result) await fetchDocumentDetails(documentId);
return result;
} finally {
isVerifyLoading.value = false;
}
}
/// Reject document
Future<bool> rejectDocument(String documentId) async {
try {
isRejectLoading.value = true;
final result =
await ApiService.verifyDocumentApi(id: documentId, isVerify: false);
if (result) await fetchDocumentDetails(documentId);
return result;
} finally {
isRejectLoading.value = false;
}
}
/// Fetch Pre-Signed URL for a given version
Future<String?> fetchPresignedUrl(String versionId) async {
return await ApiService.getPresignedUrlApi(versionId);
}
/// Clear data when leaving the screen
void clearDetails() {
documentDetails.value = null;
versions.clear();
}
}

View File

@ -1,239 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/document/master_document_type_model.dart';
import 'package:marco/model/document/master_document_tags.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class DocumentUploadController extends GetxController {
// Observables
var isLoading = false.obs;
var isUploading = false.obs;
var categories = <DocumentType>[].obs;
var tags = <TagItem>[].obs;
DocumentType? selectedCategory;
/// --- FILE HANDLING ---
String? selectedFileName;
String? selectedFileBase64;
String? selectedFileContentType;
int? selectedFileSize;
/// --- TAG HANDLING ---
final tagCtrl = TextEditingController();
final enteredTags = <String>[].obs;
final filteredSuggestions = <String>[].obs;
var documentTypes = <DocumentType>[].obs;
DocumentType? selectedType;
@override
void onInit() {
super.onInit();
fetchCategories();
fetchTags();
}
/// Fetch available document categories
Future<void> fetchCategories() async {
try {
isLoading.value = true;
final response = await ApiService.getMasterDocumentTypesApi();
if (response != null && response.data.isNotEmpty) {
categories.assignAll(response.data);
logSafe("Fetched categories: ${categories.length}");
} else {
logSafe("No categories fetched", level: LogLevel.warning);
}
} finally {
isLoading.value = false;
}
}
Future<void> fetchDocumentTypes(String categoryId) async {
try {
isLoading.value = true;
final response =
await ApiService.getDocumentTypesByCategoryApi(categoryId);
if (response != null && response.data.isNotEmpty) {
documentTypes.assignAll(response.data);
selectedType = null; // reset previous type
} else {
documentTypes.clear();
selectedType = null;
}
} finally {
isLoading.value = false;
}
}
Future<String?> fetchPresignedUrl(String versionId) async {
return await ApiService.getPresignedUrlApi(versionId);
}
/// Fetch available document tags
Future<void> fetchTags() async {
try {
isLoading.value = true;
final response = await ApiService.getMasterDocumentTagsApi();
if (response != null) {
tags.assignAll(response.data);
logSafe("Fetched tags: ${tags.length}");
} else {
logSafe("No tags fetched", level: LogLevel.warning);
}
} finally {
isLoading.value = false;
}
}
/// --- TAG LOGIC ---
void filterSuggestions(String query) {
if (query.isEmpty) {
filteredSuggestions.clear();
return;
}
filteredSuggestions.assignAll(
tags.map((t) => t.name).where(
(tag) => tag.toLowerCase().contains(query.toLowerCase()),
),
);
}
void addEnteredTag(String tag) {
if (tag.trim().isEmpty) return;
if (!enteredTags.contains(tag.trim())) {
enteredTags.add(tag.trim());
}
}
void removeEnteredTag(String tag) {
enteredTags.remove(tag);
}
void clearSuggestions() {
filteredSuggestions.clear();
}
/// Upload document
Future<bool> uploadDocument({
required String documentId,
required String name,
required String entityId,
required String documentTypeId,
required String fileName,
required String base64Data,
required String contentType,
required int fileSize,
String? description,
}) async {
try {
isUploading.value = true;
final payloadTags =
enteredTags.map((t) => {"name": t, "isActive": true}).toList();
final payload = {
"documentId": documentId,
"name": name,
"description": description,
"entityId": entityId,
"documentTypeId": documentTypeId,
"fileName": fileName,
"base64Data":
base64Data.isNotEmpty ? "<base64-string-truncated>" : null,
"contentType": contentType,
"fileSize": fileSize,
"tags": payloadTags,
};
// Log the payload (hide long base64 string for readability)
logSafe("Upload payload: $payload");
final success = await ApiService.uploadDocumentApi(
documentId: documentId,
name: name,
description: description,
entityId: entityId,
documentTypeId: documentTypeId,
fileName: fileName,
base64Data: base64Data,
contentType: contentType,
fileSize: fileSize,
tags: payloadTags,
);
if (success) {
showAppSnackbar(
title: "Success",
message: "Document uploaded successfully",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Could not upload document",
type: SnackbarType.error,
);
}
return success;
} catch (e, stack) {
logSafe("Upload error: $e", level: LogLevel.error);
logSafe("Stacktrace: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "An unexpected error occurred",
type: SnackbarType.error,
);
return false;
} finally {
isUploading.value = false;
}
}
Future<bool> editDocument(Map<String, dynamic> payload) async {
try {
isUploading.value = true;
final attachment = payload["attachment"];
final success = await ApiService.editDocumentApi(
id: payload["id"],
name: payload["name"],
documentId: payload["documentId"],
description: payload["description"],
tags: (payload["tags"] as List).cast<Map<String, dynamic>>(),
attachment: attachment,
);
if (success) {
showAppSnackbar(
title: "Success",
message: "Document updated successfully",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to update document",
type: SnackbarType.error,
);
}
return success;
} catch (e, stack) {
logSafe("Edit error: $e", level: LogLevel.error);
logSafe("Stacktrace: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "An unexpected error occurred",
type: SnackbarType.error,
);
return false;
} finally {
isUploading.value = false;
}
}
}

View File

@ -1,181 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/document/document_filter_model.dart';
import 'package:marco/model/document/documents_list_model.dart';
class DocumentController extends GetxController {
// ------------------ Observables ---------------------
var isLoading = false.obs;
var documents = <DocumentItem>[].obs;
var filters = Rxn<DocumentFiltersData>();
// Selected filters (multi-select support)
var selectedUploadedBy = <String>[].obs;
var selectedCategory = <String>[].obs;
var selectedType = <String>[].obs;
var selectedTag = <String>[].obs;
// Pagination state
var pageNumber = 1.obs;
final int pageSize = 20;
var hasMore = true.obs;
// Error message
var errorMessage = "".obs;
// NEW: show inactive toggle
var showInactive = false.obs;
// NEW: search
var searchQuery = ''.obs;
var searchController = TextEditingController();
// New filter fields
var isUploadedAt = true.obs;
var isVerified = RxnBool();
var startDate = Rxn<String>();
var endDate = Rxn<String>();
// ------------------ API Calls -----------------------
/// Fetch Document Filters for an Entity
Future<void> fetchFilters(String entityTypeId) async {
try {
isLoading.value = true;
final response = await ApiService.getDocumentFilters(entityTypeId);
if (response != null && response.success) {
filters.value = response.data;
} else {
errorMessage.value = response?.message ?? "Failed to fetch filters";
}
} catch (e) {
errorMessage.value = "Error fetching filters: $e";
} finally {
isLoading.value = false;
}
}
/// Toggle document active/inactive state
Future<bool> toggleDocumentActive(
String id, {
required bool isActive,
required String entityTypeId,
required String entityId,
}) async {
try {
isLoading.value = true;
final success =
await ApiService.deleteDocumentApi(id: id, isActive: isActive);
if (success) {
// 🔥 Always fetch fresh list after toggle
await fetchDocuments(
entityTypeId: entityTypeId,
entityId: entityId,
reset: true,
);
return true;
} else {
errorMessage.value = "Failed to update document state";
return false;
}
} catch (e) {
errorMessage.value = "Error updating document: $e";
return false;
} finally {
isLoading.value = false;
}
}
/// Permanently delete a document (or deactivate depending on API)
Future<bool> deleteDocument(String id, {bool isActive = false}) async {
try {
isLoading.value = true;
final success =
await ApiService.deleteDocumentApi(id: id, isActive: isActive);
if (success) {
// remove from local list immediately for better UX
documents.removeWhere((doc) => doc.id == id);
return true;
} else {
errorMessage.value = "Failed to delete document";
return false;
}
} catch (e) {
errorMessage.value = "Error deleting document: $e";
return false;
} finally {
isLoading.value = false;
}
}
/// Fetch Documents for an entity
Future<void> fetchDocuments({
required String entityTypeId,
required String entityId,
String? filter,
String? searchString,
bool reset = false,
}) async {
try {
if (reset) {
pageNumber.value = 1;
documents.clear();
hasMore.value = true;
}
if (!hasMore.value) return;
isLoading.value = true;
final response = await ApiService.getDocumentListApi(
entityTypeId: entityTypeId,
entityId: entityId,
filter: filter ?? "",
searchString: searchString ?? searchQuery.value,
pageNumber: pageNumber.value,
pageSize: pageSize,
isActive: !showInactive.value,
);
if (response != null && response.success) {
if (response.data.data.isNotEmpty) {
documents.addAll(response.data.data);
pageNumber.value++;
} else {
hasMore.value = false;
}
} else {
errorMessage.value = response?.message ?? "Failed to fetch documents";
}
} catch (e) {
errorMessage.value = "Error fetching documents: $e";
} finally {
isLoading.value = false;
}
}
// ------------------ Helpers -----------------------
/// Clear selected filters
void clearFilters() {
selectedUploadedBy.clear();
selectedCategory.clear();
selectedType.clear();
selectedTag.clear();
isUploadedAt.value = true;
isVerified.value = null;
startDate.value = null;
endDate.value = null;
}
/// Check if any filters are active (for red dot indicator)
bool hasActiveFilters() {
return selectedUploadedBy.isNotEmpty ||
selectedCategory.isNotEmpty ||
selectedType.isNotEmpty ||
selectedTag.isNotEmpty;
}
}

View File

@ -1,62 +0,0 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
class DynamicMenuController extends GetxController {
// UI reactive states
final RxBool isLoading = false.obs;
final RxBool hasError = false.obs;
final RxString errorMessage = ''.obs;
final RxList<MenuItem> menuItems = <MenuItem>[].obs;
@override
void onInit() {
super.onInit();
// Fetch menus directly from API at startup
fetchMenu();
}
/// Fetch dynamic menu from API (no local cache)
Future<void> fetchMenu() async {
isLoading.value = true;
hasError.value = false;
errorMessage.value = '';
try {
final responseData = await ApiService.getMenuApi();
if (responseData != null) {
final menuResponse = MenuResponse.fromJson(responseData);
menuItems.assignAll(menuResponse.data);
logSafe("✅ Menu loaded from API with ${menuItems.length} items");
} else {
_handleApiFailure("Menu API returned null response");
}
} catch (e) {
_handleApiFailure("Menu fetch exception: $e");
} finally {
isLoading.value = false;
}
}
void _handleApiFailure(String logMessage) {
logSafe(logMessage, level: LogLevel.error);
// No cache available, show error state
hasError.value = true;
errorMessage.value = "❌ Unable to load menus. Please try again later.";
menuItems.clear();
}
bool isMenuAllowed(String menuName) {
final menu = menuItems.firstWhereOrNull((m) => m.name == menuName);
return menu?.available ?? false;
}
@override
void onClose() {
super.onClose();
}
}

View File

@ -1,310 +0,0 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart';
enum Gender {
male,
female,
other;
const Gender();
}
class AddEmployeeController extends MyController {
Map<String, dynamic>? editingEmployeeData;
// State
final MyFormValidator basicValidator = MyFormValidator();
final List<PlatformFile> files = [];
final List<String> categories = [];
Gender? selectedGender;
List<Map<String, dynamic>> roles = [];
String? selectedRoleId;
String selectedCountryCode = '+91';
bool showOnline = true;
DateTime? joiningDate;
String? selectedOrganizationId;
RxString selectedOrganizationName = RxString('');
@override
void onInit() {
super.onInit();
logSafe('Initializing AddEmployeeController...');
_initializeFields();
fetchRoles();
if (editingEmployeeData != null) {
prefillFields();
}
}
void _initializeFields() {
basicValidator.addField(
'first_name',
label: 'First Name',
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'phone_number',
label: 'Phone Number',
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'last_name',
label: 'Last Name',
required: true,
controller: TextEditingController(),
);
// Email is optional in controller; UI enforces when application access is checked
basicValidator.addField(
'email',
label: 'Email',
required: false,
controller: TextEditingController(),
);
logSafe('Fields initialized for first_name, phone_number, last_name, email.');
}
// Prefill fields in edit mode
void prefillFields() {
logSafe('Prefilling data for editing...');
basicValidator.getController('first_name')?.text =
editingEmployeeData?['first_name'] ?? '';
basicValidator.getController('last_name')?.text =
editingEmployeeData?['last_name'] ?? '';
basicValidator.getController('phone_number')?.text =
editingEmployeeData?['phone_number'] ?? '';
selectedGender = editingEmployeeData?['gender'] != null
? Gender.values.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender'])
: null;
basicValidator.getController('email')?.text =
editingEmployeeData?['email'] ?? '';
selectedRoleId = editingEmployeeData?['job_role_id'];
if (editingEmployeeData?['joining_date'] != null) {
joiningDate = DateTime.tryParse(editingEmployeeData!['joining_date']);
}
update();
}
void setJoiningDate(DateTime date) {
joiningDate = date;
logSafe('Joining date selected: $date');
update();
}
void onGenderSelected(Gender? gender) {
selectedGender = gender;
logSafe('Gender selected: ${gender?.name}');
update();
}
Future<void> fetchRoles() async {
logSafe('Fetching roles...');
try {
final result = await ApiService.getRoles();
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logSafe('Roles fetched successfully.');
update();
} else {
logSafe('Failed to fetch roles: null result', level: LogLevel.error);
}
} catch (e, st) {
logSafe('Error fetching roles', level: LogLevel.error, error: e, stackTrace: st);
}
}
void onRoleSelected(String? roleId) {
selectedRoleId = roleId;
logSafe('Role selected: $roleId');
update();
}
// Create or update employee
Future<Map<String, dynamic>?> createOrUpdateEmployee({
String? email,
bool hasApplicationAccess = false,
}) async {
logSafe(editingEmployeeData != null
? 'Starting employee update...'
: 'Starting employee creation...');
if (selectedGender == null || selectedRoleId == null) {
showAppSnackbar(
title: 'Missing Fields',
message: 'Please select both Gender and Role.',
type: SnackbarType.warning,
);
return null;
}
final firstName = basicValidator.getController('first_name')?.text.trim();
final lastName = basicValidator.getController('last_name')?.text.trim();
final phoneNumber = basicValidator.getController('phone_number')?.text.trim();
try {
// sanitize orgId before sending
final String? orgId = (selectedOrganizationId != null &&
selectedOrganizationId!.trim().isNotEmpty)
? selectedOrganizationId
: null;
final response = await ApiService.createEmployee(
id: editingEmployeeData?['id'],
firstName: firstName!,
lastName: lastName!,
phoneNumber: phoneNumber!,
gender: selectedGender!.name,
jobRoleId: selectedRoleId!,
joiningDate: joiningDate?.toIso8601String() ?? '',
organizationId: orgId,
email: email,
hasApplicationAccess: hasApplicationAccess,
);
logSafe('Response: $response');
if (response != null && response['success'] == true) {
showAppSnackbar(
title: 'Success',
message: editingEmployeeData != null
? 'Employee updated successfully!'
: 'Employee created successfully!',
type: SnackbarType.success,
);
return response;
} else {
logSafe('Failed operation', level: LogLevel.error);
}
} catch (e, st) {
logSafe('Error creating/updating employee',
level: LogLevel.error, error: e, stackTrace: st);
}
showAppSnackbar(
title: 'Error',
message: 'Failed to save employee.',
type: SnackbarType.error,
);
return null;
}
Future<bool> _checkAndRequestContactsPermission() async {
final status = await Permission.contacts.request();
if (status.isGranted) return true;
if (status.isPermanentlyDenied) {
await openAppSettings();
}
showAppSnackbar(
title: 'Permission Required',
message: 'Please allow Contacts permission from settings to pick a contact.',
type: SnackbarType.warning,
);
return false;
}
Future<void> pickContact(BuildContext context) async {
final permissionGranted = await _checkAndRequestContactsPermission();
if (!permissionGranted) return;
try {
final picked = await FlutterContacts.openExternalPick();
if (picked == null) return;
final contact =
await FlutterContacts.getContact(picked.id, withProperties: true);
if (contact == null) {
showAppSnackbar(
title: 'Error',
message: 'Failed to load contact details.',
type: SnackbarType.error,
);
return;
}
if (contact.phones.isEmpty) {
showAppSnackbar(
title: 'No Phone Number',
message: 'Selected contact has no phone number.',
type: SnackbarType.warning,
);
return;
}
final indiaPhones = contact.phones.where((p) {
final normalized = p.number.replaceAll(RegExp(r'[^0-9+]'), '');
return normalized.startsWith('+91') ||
RegExp(r'^\d{10}$').hasMatch(normalized);
}).toList();
if (indiaPhones.isEmpty) {
showAppSnackbar(
title: 'No Indian Number',
message: 'Selected contact has no Indian (+91) phone number.',
type: SnackbarType.warning,
);
return;
}
String? selectedPhone;
if (indiaPhones.length == 1) {
selectedPhone = indiaPhones.first.number;
} else {
selectedPhone = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Choose an Indian number'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: indiaPhones
.map(
(p) => ListTile(
title: Text(p.number),
onTap: () => Navigator.of(ctx).pop(p.number),
),
)
.toList(),
),
),
);
if (selectedPhone == null) return;
}
final normalizedPhone = selectedPhone.replaceAll(RegExp(r'[^0-9]'), '');
final phoneWithoutCountryCode = normalizedPhone.length > 10
? normalizedPhone.substring(normalizedPhone.length - 10)
: normalizedPhone;
basicValidator.getController('phone_number')?.text =
phoneWithoutCountryCode;
update();
} catch (e, st) {
logSafe('Error fetching contacts',
level: LogLevel.error, error: e, stackTrace: st);
showAppSnackbar(
title: 'Error',
message: 'Failed to fetch contacts.',
type: SnackbarType.error,
);
}
}
}

View File

@ -1,145 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/global_project_model.dart';
import 'package:marco/model/employees/assigned_projects_model.dart';
import 'package:marco/controller/project_controller.dart';
class AssignProjectController extends GetxController {
final String employeeId;
final String jobRoleId;
AssignProjectController({
required this.employeeId,
required this.jobRoleId,
});
final ProjectController projectController = Get.put(ProjectController());
RxBool isLoading = false.obs;
RxBool isAssigning = false.obs;
RxList<String> assignedProjectIds = <String>[].obs;
RxList<String> selectedProjects = <String>[].obs;
RxList<GlobalProjectModel> allProjects = <GlobalProjectModel>[].obs;
RxList<GlobalProjectModel> filteredProjects = <GlobalProjectModel>[].obs;
@override
void onInit() {
super.onInit();
WidgetsBinding.instance.addPostFrameCallback((_) {
fetchAllProjectsAndAssignments();
});
}
/// Fetch all projects and assigned projects
Future<void> fetchAllProjectsAndAssignments() async {
isLoading.value = true;
try {
await projectController.fetchProjects();
allProjects.assignAll(projectController.projects);
filteredProjects.assignAll(allProjects); // initially show all
final responseList = await ApiService.getAssignedProjects(employeeId);
if (responseList != null) {
final assignedProjects =
responseList.map((e) => AssignedProject.fromJson(e)).toList();
assignedProjectIds.assignAll(
assignedProjects.map((p) => p.id).toList(),
);
selectedProjects.assignAll(assignedProjectIds);
}
logSafe("All Projects: ${allProjects.map((e) => e.id)}");
logSafe("Assigned Project IDs: $assignedProjectIds");
} catch (e, stack) {
logSafe("Error fetching projects or assignments: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
} finally {
isLoading.value = false;
}
}
/// Assign selected projects
Future<bool> assignProjectsToEmployee() async {
if (selectedProjects.isEmpty) {
logSafe("No projects selected for assignment.", level: LogLevel.warning);
return false;
}
final List<Map<String, dynamic>> projectPayload =
selectedProjects.map((id) {
return {"projectId": id, "jobRoleId": jobRoleId, "status": true};
}).toList();
isAssigning.value = true;
try {
final success = await ApiService.assignProjects(
employeeId: employeeId,
projects: projectPayload,
);
if (success) {
logSafe("Projects assigned successfully.");
assignedProjectIds.assignAll(selectedProjects);
return true;
} else {
logSafe("Failed to assign projects.", level: LogLevel.error);
return false;
}
} catch (e, stack) {
logSafe("Error assigning projects: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
return false;
} finally {
isAssigning.value = false;
}
}
/// Toggle project selection
void toggleProjectSelection(String projectId, bool isSelected) {
if (isSelected) {
if (!selectedProjects.contains(projectId)) {
selectedProjects.add(projectId);
}
} else {
selectedProjects.remove(projectId);
}
}
/// Check if project is selected
bool isProjectSelected(String projectId) {
return selectedProjects.contains(projectId);
}
/// Select all / deselect all
void toggleSelectAll() {
if (areAllSelected()) {
selectedProjects.clear();
} else {
selectedProjects.assignAll(allProjects.map((p) => p.id.toString()));
}
}
/// Are all selected?
bool areAllSelected() {
return selectedProjects.length == allProjects.length &&
allProjects.isNotEmpty;
}
/// Filter projects by search text
void filterProjects(String query) {
if (query.isEmpty) {
filteredProjects.assignAll(allProjects);
} else {
filteredProjects.assignAll(
allProjects
.where((p) => p.name.toLowerCase().contains(query.toLowerCase()))
.toList(),
);
}
}
}

View File

@ -1,195 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance/attendance_model.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/employees/employee_details_model.dart';
import 'package:marco/controller/project_controller.dart';
class EmployeesScreenController extends GetxController {
List<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeDetailsModel> employeeDetails = [];
RxBool isAllEmployeeSelected = false.obs;
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
RxBool isLoading = false.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
Rxn<EmployeeDetailsModel>();
RxBool isLoadingEmployeeDetails = false.obs;
@override
void onInit() {
super.onInit();
isLoading.value = true;
fetchAllProjects().then((_) {
final projectId = Get.find<ProjectController>().selectedProject?.id;
if (projectId != null) {
selectedProjectId = projectId;
fetchEmployeesByProject(projectId);
} else if (isAllEmployeeSelected.value) {
fetchAllEmployees();
} else {
clearEmployees();
}
});
}
Future<void> fetchAllProjects() async {
isLoading.value = true;
await _handleApiCall(
ApiService.getProjects,
onSuccess: (data) {
projects = data.map((json) => ProjectModel.fromJson(json)).toList();
logSafe(
"Projects fetched: ${projects.length} projects loaded.",
level: LogLevel.info,
);
},
onEmpty: () {
logSafe("No project data found or API call failed.",
level: LogLevel.warning);
},
);
isLoading.value = false;
update();
}
void clearEmployees() {
employees.clear();
logSafe("Employees cleared", level: LogLevel.info);
update(['employee_screen_controller']);
}
Future<void> fetchAllEmployees({String? organizationId}) async {
isLoading.value = true;
update(['employee_screen_controller']);
await _handleApiCall(
() => ApiService.getAllEmployees(
organizationId: organizationId), // pass orgId to API
onSuccess: (data) {
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
logSafe(
"All Employees fetched: ${employees.length} employees loaded.",
level: LogLevel.info,
);
},
onEmpty: () {
employees.clear();
logSafe(
"No Employee data found or API call failed",
level: LogLevel.warning,
);
},
);
isLoading.value = false;
update(['employee_screen_controller']);
}
Future<void> fetchEmployeesByProject(String projectId,
{String? organizationId}) async {
if (projectId.isEmpty) return;
isLoading.value = true;
await _handleApiCall(
() => ApiService.getAllEmployeesByProject(projectId,
organizationId: organizationId),
onSuccess: (data) {
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
},
onEmpty: () => employees.clear(),
);
isLoading.value = false;
update(['employee_screen_controller']);
}
Future<void> fetchEmployeeDetails(String? employeeId) async {
if (employeeId == null || employeeId.isEmpty) return;
isLoadingEmployeeDetails.value = true;
await _handleSingleApiCall(
() => ApiService.getEmployeeDetails(employeeId),
onSuccess: (data) {
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
logSafe(
"Employee details loaded for $employeeId",
level: LogLevel.info,
);
},
onEmpty: () {
selectedEmployeeDetails.value = null;
logSafe(
"No employee details found for $employeeId",
level: LogLevel.warning,
);
},
onError: (e) {
selectedEmployeeDetails.value = null;
logSafe(
"Error fetching employee details for $employeeId",
level: LogLevel.error,
error: e,
);
},
);
isLoadingEmployeeDetails.value = false;
}
Future<void> _handleApiCall(
Future<List<dynamic>?> Function() apiCall, {
required Function(List<dynamic>) onSuccess,
required Function() onEmpty,
Function(dynamic error)? onError,
}) async {
try {
final response = await apiCall();
if (response != null && response.isNotEmpty) {
onSuccess(response);
} else {
onEmpty();
}
} catch (e) {
if (onError != null) {
onError(e);
} else {
logSafe("API call error", level: LogLevel.error, error: e);
}
}
}
Future<void> _handleSingleApiCall(
Future<Map<String, dynamic>?> Function() apiCall, {
required Function(Map<String, dynamic>) onSuccess,
required Function() onEmpty,
Function(dynamic error)? onError,
}) async {
try {
final response = await apiCall();
if (response != null && response.isNotEmpty) {
onSuccess(response);
} else {
onEmpty();
}
} catch (e) {
if (onError != null) {
onError(e);
} else {
logSafe("API call error", level: LogLevel.error, error: e);
}
}
}
}

View File

@ -1,497 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:mime/mime.dart';
import 'package:image_picker/image_picker.dart';
import 'package:marco/controller/expense/expense_screen_controller.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/expense/expense_type_model.dart';
import 'package:marco/model/expense/payment_types_model.dart';
class AddExpenseController extends GetxController {
// --- Text Controllers ---
final controllers = <TextEditingController>[
TextEditingController(), // amount
TextEditingController(), // description
TextEditingController(), // supplier
TextEditingController(), // transactionId
TextEditingController(), // gst
TextEditingController(), // location
TextEditingController(), // transactionDate
TextEditingController(), // noOfPersons
TextEditingController(), // employeeSearch
];
TextEditingController get amountController => controllers[0];
TextEditingController get descriptionController => controllers[1];
TextEditingController get supplierController => controllers[2];
TextEditingController get transactionIdController => controllers[3];
TextEditingController get gstController => controllers[4];
TextEditingController get locationController => controllers[5];
TextEditingController get transactionDateController => controllers[6];
TextEditingController get noOfPersonsController => controllers[7];
TextEditingController get employeeSearchController => controllers[8];
// --- Reactive State ---
final isLoading = false.obs;
final isSubmitting = false.obs;
final isFetchingLocation = false.obs;
final isEditMode = false.obs;
final isSearchingEmployees = false.obs;
// --- Dropdown Selections & Data ---
final selectedPaymentMode = Rxn<PaymentModeModel>();
final selectedExpenseType = Rxn<ExpenseTypeModel>();
final selectedPaidBy = Rxn<EmployeeModel>();
final selectedProject = ''.obs;
final selectedTransactionDate = Rxn<DateTime>();
final attachments = <File>[].obs;
final existingAttachments = <Map<String, dynamic>>[].obs;
final globalProjects = <String>[].obs;
final projectsMap = <String, String>{}.obs;
final expenseTypes = <ExpenseTypeModel>[].obs;
final paymentModes = <PaymentModeModel>[].obs;
final allEmployees = <EmployeeModel>[].obs;
final employeeSearchResults = <EmployeeModel>[].obs;
String? editingExpenseId;
final expenseController = Get.find<ExpenseController>();
final ImagePicker _picker = ImagePicker();
@override
void onInit() {
super.onInit();
loadMasterData();
employeeSearchController.addListener(
() => searchEmployees(employeeSearchController.text),
);
}
@override
void onClose() {
for (var c in controllers) {
c.dispose();
}
super.onClose();
}
// --- Employee Search ---
Future<void> searchEmployees(String query) async {
if (query.trim().isEmpty) return employeeSearchResults.clear();
isSearchingEmployees.value = true;
try {
final data = await ApiService.searchEmployeesBasic(
searchString: query.trim(),
);
if (data is List) {
employeeSearchResults.assignAll(
data
.map((e) => EmployeeModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
} else {
employeeSearchResults.clear();
}
} catch (e) {
logSafe("Error searching employees: $e", level: LogLevel.error);
employeeSearchResults.clear();
} finally {
isSearchingEmployees.value = false;
}
}
// --- Form Population (Edit) ---
Future<void> populateFieldsForEdit(Map<String, dynamic> data) async {
isEditMode.value = true;
editingExpenseId = '${data['id']}';
selectedProject.value = data['projectName'] ?? '';
amountController.text = '${data['amount'] ?? ''}';
supplierController.text = data['supplerName'] ?? '';
descriptionController.text = data['description'] ?? '';
transactionIdController.text = data['transactionId'] ?? '';
locationController.text = data['location'] ?? '';
noOfPersonsController.text = '${data['noOfPersons'] ?? 0}';
_setTransactionDate(data['transactionDate']);
_setDropdowns(data);
await _setPaidBy(data);
_setAttachments(data['attachments']);
_logPrefilledData();
}
void _setTransactionDate(dynamic dateStr) {
if (dateStr == null) {
selectedTransactionDate.value = null;
transactionDateController.clear();
return;
}
try {
final parsed = DateTime.parse(dateStr);
selectedTransactionDate.value = parsed;
transactionDateController.text = DateFormat('dd-MM-yyyy').format(parsed);
} catch (_) {
selectedTransactionDate.value = null;
transactionDateController.clear();
}
}
void _setDropdowns(Map<String, dynamic> data) {
selectedExpenseType.value =
expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']);
selectedPaymentMode.value =
paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']);
}
Future<void> _setPaidBy(Map<String, dynamic> data) async {
final paidById = '${data['paidById']}';
selectedPaidBy.value =
allEmployees.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
if (selectedPaidBy.value == null && data['paidByFirstName'] != null) {
await searchEmployees(
'${data['paidByFirstName']} ${data['paidByLastName']}',
);
selectedPaidBy.value = employeeSearchResults
.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
}
}
void _setAttachments(dynamic attachmentsData) {
existingAttachments.clear();
if (attachmentsData is List) {
existingAttachments.addAll(
List<Map<String, dynamic>>.from(attachmentsData).map(
(e) => {...e, 'isActive': true},
),
);
}
}
void _logPrefilledData() {
final info = [
'ID: $editingExpenseId',
'Project: ${selectedProject.value}',
'Amount: ${amountController.text}',
'Supplier: ${supplierController.text}',
'Description: ${descriptionController.text}',
'Transaction ID: ${transactionIdController.text}',
'Location: ${locationController.text}',
'Transaction Date: ${transactionDateController.text}',
'No. of Persons: ${noOfPersonsController.text}',
'Expense Type: ${selectedExpenseType.value?.name}',
'Payment Mode: ${selectedPaymentMode.value?.name}',
'Paid By: ${selectedPaidBy.value?.name}',
'Attachments: ${attachments.length}',
'Existing Attachments: ${existingAttachments.length}',
];
for (var line in info) {
logSafe(line, level: LogLevel.info);
}
}
// --- Pickers ---
Future<void> pickTransactionDate(BuildContext context) async {
final pickedDate = await showDatePicker(
context: context,
initialDate: selectedTransactionDate.value ?? DateTime.now(),
firstDate: DateTime(DateTime.now().year - 5),
lastDate: DateTime.now(),
);
if (pickedDate != null) {
final now = DateTime.now();
final finalDateTime = DateTime(
pickedDate.year,
pickedDate.month,
pickedDate.day,
now.hour,
now.minute,
now.second,
);
selectedTransactionDate.value = finalDateTime;
transactionDateController.text =
DateFormat('dd MMM yyyy').format(finalDateTime);
}
}
Future<void> pickAttachments() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
allowMultiple: true,
);
if (result != null) {
attachments.addAll(
result.paths.whereType<String>().map(File.new),
);
}
} catch (e) {
_errorSnackbar("Attachment error: $e");
}
}
void removeAttachment(File file) => attachments.remove(file);
Future<void> pickFromCamera() async {
try {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) attachments.add(File(pickedFile.path));
} catch (e) {
_errorSnackbar("Camera error: $e");
}
}
// --- Location ---
Future<void> fetchCurrentLocation() async {
isFetchingLocation.value = true;
try {
if (!await _ensureLocationPermission()) return;
final position = await Geolocator.getCurrentPosition();
final placemarks =
await placemarkFromCoordinates(position.latitude, position.longitude);
locationController.text = placemarks.isNotEmpty
? [
placemarks.first.name,
placemarks.first.street,
placemarks.first.locality,
placemarks.first.administrativeArea,
placemarks.first.country,
].where((e) => e?.isNotEmpty == true).join(", ")
: "${position.latitude}, ${position.longitude}";
} catch (e) {
_errorSnackbar("Location error: $e");
} finally {
isFetchingLocation.value = false;
}
}
Future<bool> _ensureLocationPermission() async {
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
_errorSnackbar("Location permission denied.");
return false;
}
}
if (!await Geolocator.isLocationServiceEnabled()) {
_errorSnackbar("Location service disabled.");
return false;
}
return true;
}
// --- Data Fetching ---
Future<void> loadMasterData() async =>
Future.wait([fetchMasterData(), fetchGlobalProjects()]);
Future<void> fetchMasterData() async {
try {
final types = await ApiService.getMasterExpenseTypes();
if (types is List) {
expenseTypes.value = types
.map((e) => ExpenseTypeModel.fromJson(e as Map<String, dynamic>))
.toList();
}
final modes = await ApiService.getMasterPaymentModes();
if (modes is List) {
paymentModes.value = modes
.map((e) => PaymentModeModel.fromJson(e as Map<String, dynamic>))
.toList();
}
} catch (_) {
_errorSnackbar("Failed to fetch master data");
}
}
Future<void> fetchGlobalProjects() async {
try {
final response = await ApiService.getGlobalProjects();
if (response != null) {
final names = <String>[];
for (var item in response) {
final name = item['name']?.toString().trim();
final id = item['id']?.toString().trim();
if (name != null && id != null) {
projectsMap[name] = id;
names.add(name);
}
}
globalProjects.assignAll(names);
}
} catch (e) {
logSafe("Error fetching projects: $e", level: LogLevel.error);
}
}
// --- Submission ---
Future<void> submitOrUpdateExpense() async {
if (isSubmitting.value) return;
isSubmitting.value = true;
try {
final validationMsg = validateForm();
if (validationMsg.isNotEmpty) {
_errorSnackbar(validationMsg, "Missing Fields");
return;
}
final payload = await _buildExpensePayload();
final success = await _submitToApi(payload);
if (success) {
await expenseController.fetchExpenses();
Get.back();
showAppSnackbar(
title: "Success",
message:
"Expense ${isEditMode.value ? 'updated' : 'created'} successfully!",
type: SnackbarType.success,
);
} else {
_errorSnackbar("Operation failed. Try again.");
}
} catch (e) {
_errorSnackbar("Unexpected error: $e");
} finally {
isSubmitting.value = false;
}
}
Future<bool> _submitToApi(Map<String, dynamic> payload) async {
if (isEditMode.value && editingExpenseId != null) {
return ApiService.editExpenseApi(
expenseId: editingExpenseId!,
payload: payload,
);
}
return ApiService.createExpenseApi(
projectId: payload['projectId'],
expensesTypeId: payload['expensesTypeId'],
paymentModeId: payload['paymentModeId'],
paidById: payload['paidById'],
transactionDate: DateTime.parse(payload['transactionDate']),
transactionId: payload['transactionId'],
description: payload['description'],
location: payload['location'],
supplerName: payload['supplerName'],
amount: payload['amount'],
noOfPersons: payload['noOfPersons'],
billAttachments: payload['billAttachments'],
);
}
Future<Map<String, dynamic>> _buildExpensePayload() async {
final now = DateTime.now();
final existingPayload = isEditMode.value
? existingAttachments
.map((e) => {
"documentId": e['documentId'],
"fileName": e['fileName'],
"contentType": e['contentType'],
"fileSize": 0,
"description": "",
"url": e['url'],
"isActive": e['isActive'] ?? true,
"base64Data": "",
})
.toList()
: <Map<String, dynamic>>[];
final newPayload = await Future.wait(
attachments.map((file) async {
final bytes = await file.readAsBytes();
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(bytes),
"contentType":
lookupMimeType(file.path) ?? 'application/octet-stream',
"fileSize": await file.length(),
"description": "",
};
}),
);
final type = selectedExpenseType.value!;
return {
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
"projectId": projectsMap[selectedProject.value]!,
"expensesTypeId": type.id,
"paymentModeId": selectedPaymentMode.value!.id,
"paidById": selectedPaidBy.value!.id,
"transactionDate":
(selectedTransactionDate.value ?? now).toUtc().toIso8601String(),
"transactionId": transactionIdController.text,
"description": descriptionController.text,
"location": locationController.text,
"supplerName": supplierController.text,
"amount": double.parse(amountController.text.trim()),
"noOfPersons": type.noOfPersonsRequired == true
? int.tryParse(noOfPersonsController.text.trim()) ?? 0
: 0,
"billAttachments": [
...existingPayload,
...newPayload,
].isEmpty
? null
: [...existingPayload, ...newPayload],
};
}
String validateForm() {
final missing = <String>[];
if (selectedProject.value.isEmpty) missing.add("Project");
if (selectedExpenseType.value == null) missing.add("Expense Type");
if (selectedPaymentMode.value == null) missing.add("Payment Mode");
if (selectedPaidBy.value == null) missing.add("Paid By");
if (amountController.text.trim().isEmpty) missing.add("Amount");
if (descriptionController.text.trim().isEmpty) missing.add("Description");
if (selectedTransactionDate.value == null) {
missing.add("Transaction Date");
} else if (selectedTransactionDate.value!.isAfter(DateTime.now())) {
missing.add("Valid Transaction Date");
}
if (double.tryParse(amountController.text.trim()) == null) {
missing.add("Valid Amount");
}
final hasActiveExisting =
existingAttachments.any((e) => e['isActive'] != false);
if (attachments.isEmpty && !hasActiveExisting) {
missing.add("Attachment");
}
return missing.isEmpty ? '' : "Please provide: ${missing.join(', ')}.";
}
// --- Snackbar Helper ---
void _errorSnackbar(String msg, [String title = "Error"]) {
showAppSnackbar(title: title, message: msg, type: SnackbarType.error);
}
}

View File

@ -1,187 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/expense/expense_detail_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:flutter/material.dart';
class ExpenseDetailController extends GetxController {
final Rx<ExpenseDetailModel?> expense = Rx<ExpenseDetailModel?>(null);
final RxBool isLoading = false.obs;
final RxString errorMessage = ''.obs;
final Rx<EmployeeModel?> selectedReimbursedBy = Rx<EmployeeModel?>(null);
final RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
final RxList<EmployeeModel> employeeSearchResults = <EmployeeModel>[].obs;
late String _expenseId;
bool _isInitialized = false;
final employeeSearchController = TextEditingController();
final isSearchingEmployees = false.obs;
/// Call this once from the screen (NOT inside build) to initialize
void init(String expenseId) {
if (_isInitialized) return;
_isInitialized = true;
_expenseId = expenseId;
// Use Future.wait to fetch details and employees concurrently
Future.wait([
fetchExpenseDetails(),
fetchAllEmployees(),
]);
}
/// Generic method to handle API calls with loading and error states
Future<T?> _apiCallWrapper<T>(
Future<T?> Function() apiCall, String operationName) async {
isLoading.value = true;
errorMessage.value = ''; // Clear previous errors
try {
logSafe("Initiating $operationName...");
final result = await apiCall();
logSafe("$operationName completed successfully.");
return result;
} catch (e, stack) {
errorMessage.value =
'An unexpected error occurred during $operationName.';
logSafe("Exception in $operationName: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
return null;
} finally {
isLoading.value = false;
}
}
/// Fetch expense details by stored ID
Future<void> fetchExpenseDetails() async {
final result = await _apiCallWrapper(
() => ApiService.getExpenseDetailsApi(expenseId: _expenseId),
"fetch expense details");
if (result != null) {
try {
expense.value = ExpenseDetailModel.fromJson(result);
logSafe("Expense details loaded successfully: ${expense.value?.id}");
} catch (e) {
errorMessage.value = 'Failed to parse expense details: $e';
logSafe("Parse error in fetchExpenseDetails: $e",
level: LogLevel.error);
}
} else {
errorMessage.value = 'Failed to fetch expense details from server.';
logSafe("fetchExpenseDetails failed: null response",
level: LogLevel.error);
}
}
// This method seems like a utility and might be better placed in a helper or utility class
// if it's used across multiple controllers. Keeping it here for now as per original code.
List<String> parsePermissionIds(dynamic permissionData) {
if (permissionData == null) return [];
if (permissionData is List) {
return permissionData
.map((e) => e.toString().trim())
.where((e) => e.isNotEmpty)
.toList();
}
if (permissionData is String) {
final clean = permissionData.replaceAll(RegExp(r'[\[\]]'), '');
return clean
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
}
return [];
}
Future<void> searchEmployees(String query) async {
if (query.trim().isEmpty) return employeeSearchResults.clear();
isSearchingEmployees.value = true;
try {
final data =
await ApiService.searchEmployeesBasic(searchString: query.trim());
employeeSearchResults.assignAll(
(data ?? []).map((e) => EmployeeModel.fromJson(e)),
);
} catch (e) {
logSafe("Error searching employees: $e", level: LogLevel.error);
employeeSearchResults.clear();
} finally {
isSearchingEmployees.value = false;
}
}
/// Fetch all employees
Future<void> fetchAllEmployees() async {
final response = await _apiCallWrapper(
() => ApiService.getAllEmployees(), "fetch all employees");
if (response != null && response.isNotEmpty) {
try {
allEmployees.assignAll(response.map((e) => EmployeeModel.fromJson(e)));
logSafe("All Employees fetched: ${allEmployees.length}",
level: LogLevel.info);
} catch (e) {
errorMessage.value = 'Failed to parse employee data: $e';
logSafe("Parse error in fetchAllEmployees: $e", level: LogLevel.error);
}
} else {
allEmployees.clear();
logSafe("No employees found.", level: LogLevel.warning);
}
// `update()` is typically not needed for RxList directly unless you have specific GetBuilder/Obx usage that requires it
// If you are using Obx widgets, `allEmployees.assignAll` will automatically trigger a rebuild.
}
/// Update expense with reimbursement info and status
Future<bool> updateExpenseStatusWithReimbursement({
required String comment,
required String reimburseTransactionId,
required String reimburseDate,
required String reimburseById,
required String statusId,
}) async {
final success = await _apiCallWrapper(
() => ApiService.updateExpenseStatusApi(
expenseId: _expenseId,
statusId: statusId,
comment: comment,
reimburseTransactionId: reimburseTransactionId,
reimburseDate: reimburseDate,
reimbursedById: reimburseById,
),
"submit reimbursement",
);
if (success == true) {
// Explicitly check for true as _apiCallWrapper returns T?
await fetchExpenseDetails(); // Refresh details after successful update
return true;
} else {
errorMessage.value = "Failed to submit reimbursement.";
return false;
}
}
/// Update status for this specific expense
Future<bool> updateExpenseStatus(String statusId, {String? comment}) async {
final success = await _apiCallWrapper(
() => ApiService.updateExpenseStatusApi(
expenseId: _expenseId,
statusId: statusId,
comment: comment,
),
"update expense status",
);
if (success == true) {
await fetchExpenseDetails();
return true;
} else {
errorMessage.value = "Failed to update expense status.";
return false;
}
}
}

View File

@ -1,357 +0,0 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/expense/expense_list_model.dart';
import 'package:marco/model/expense/payment_types_model.dart';
import 'package:marco/model/expense/expense_type_model.dart';
import 'package:marco/model/expense/expense_status_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter/material.dart';
class ExpenseController extends GetxController {
final RxList<ExpenseModel> expenses = <ExpenseModel>[].obs;
final RxBool isLoading = false.obs;
final RxString errorMessage = ''.obs;
// Master data
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
final RxList<ExpenseStatusModel> expenseStatuses = <ExpenseStatusModel>[].obs;
final RxList<String> globalProjects = <String>[].obs;
final RxMap<String, String> projectsMap = <String, String>{}.obs;
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
// Persistent Filter States
final RxString selectedProject = ''.obs;
final RxString selectedStatus = ''.obs;
final Rx<DateTime?> startDate = Rx<DateTime?>(null);
final Rx<DateTime?> endDate = Rx<DateTime?>(null);
final RxList<EmployeeModel> selectedPaidByEmployees = <EmployeeModel>[].obs;
final RxList<EmployeeModel> selectedCreatedByEmployees =
<EmployeeModel>[].obs;
final RxString selectedDateType = 'Transaction Date'.obs;
final employeeSearchController = TextEditingController();
final isSearchingEmployees = false.obs;
final employeeSearchResults = <EmployeeModel>[].obs;
final List<String> dateTypes = [
'Transaction Date',
'Created At',
];
int _pageSize = 20;
int _pageNumber = 1;
@override
void onInit() {
super.onInit();
loadInitialMasterData();
fetchAllEmployees();
employeeSearchController.addListener(() {
searchEmployees(employeeSearchController.text);
});
}
bool get isFilterApplied {
return selectedProject.value.isNotEmpty ||
selectedStatus.value.isNotEmpty ||
startDate.value != null ||
endDate.value != null ||
selectedPaidByEmployees.isNotEmpty ||
selectedCreatedByEmployees.isNotEmpty;
}
/// Load master data
Future<void> loadInitialMasterData() async {
await fetchGlobalProjects();
await fetchMasterData();
}
Future<void> deleteExpense(String expenseId) async {
try {
logSafe("Attempting to delete expense: $expenseId");
final success = await ApiService.deleteExpense(expenseId);
if (success) {
expenses.removeWhere((e) => e.id == expenseId);
logSafe("Expense deleted successfully.");
showAppSnackbar(
title: "Deleted",
message: "Expense has been deleted successfully.",
type: SnackbarType.success,
);
} else {
logSafe("Failed to delete expense: $expenseId", level: LogLevel.error);
showAppSnackbar(
title: "Failed",
message: "Failed to delete expense.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Exception in deleteExpense: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while deleting.",
type: SnackbarType.error,
);
}
}
Future<void> searchEmployees(String searchQuery) async {
if (searchQuery.trim().isEmpty) {
employeeSearchResults.clear();
return;
}
isSearchingEmployees.value = true;
try {
final results = await ApiService.searchEmployeesBasic(
searchString: searchQuery.trim(),
);
if (results != null) {
employeeSearchResults.assignAll(
results.map((e) => EmployeeModel.fromJson(e)),
);
} else {
employeeSearchResults.clear();
}
} catch (e) {
logSafe("Error searching employees: $e", level: LogLevel.error);
employeeSearchResults.clear();
} finally {
isSearchingEmployees.value = false;
}
}
/// Fetch expenses using filters
Future<void> fetchExpenses({
List<String>? projectIds,
List<String>? statusIds,
List<String>? createdByIds,
List<String>? paidByIds,
DateTime? startDate,
DateTime? endDate,
int pageSize = 20,
int pageNumber = 1,
}) async {
isLoading.value = true;
errorMessage.value = '';
expenses.clear();
_pageSize = pageSize;
_pageNumber = pageNumber;
final Map<String, dynamic> filterMap = {
"projectIds": projectIds ??
(selectedProject.value.isEmpty
? []
: [projectsMap[selectedProject.value] ?? '']),
"statusIds": statusIds ??
(selectedStatus.value.isEmpty ? [] : [selectedStatus.value]),
"createdByIds":
createdByIds ?? selectedCreatedByEmployees.map((e) => e.id).toList(),
"paidByIds":
paidByIds ?? selectedPaidByEmployees.map((e) => e.id).toList(),
"startDate": (startDate ?? this.startDate.value)?.toIso8601String(),
"endDate": (endDate ?? this.endDate.value)?.toIso8601String(),
"isTransactionDate": selectedDateType.value == 'Transaction Date',
};
try {
logSafe("Fetching expenses with filter: ${jsonEncode(filterMap)}");
final result = await ApiService.getExpenseListApi(
filter: jsonEncode(filterMap),
pageSize: _pageSize,
pageNumber: _pageNumber,
);
if (result != null) {
try {
final expenseResponse = ExpenseResponse.fromJson(result);
// If the backend returns no data, treat it as empty list
if (expenseResponse.data.data.isEmpty) {
expenses.clear();
errorMessage.value = ''; // no error
logSafe("Expense list is empty.");
} else {
expenses.assignAll(expenseResponse.data.data);
logSafe("Expenses loaded: ${expenses.length}");
logSafe(
"Pagination Info: Page ${expenseResponse.data.currentPage} of ${expenseResponse.data.totalPages} | Total: ${expenseResponse.data.totalEntites}");
}
} catch (e) {
errorMessage.value = 'Failed to parse expenses: $e';
logSafe("Parse error in fetchExpenses: $e", level: LogLevel.error);
}
} else {
// Only treat as error if this means a network or server failure
errorMessage.value = 'Unable to connect to the server.';
logSafe("fetchExpenses failed: null response", level: LogLevel.error);
}
} catch (e, stack) {
errorMessage.value = 'An unexpected error occurred.';
logSafe("Exception in fetchExpenses: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
} finally {
isLoading.value = false;
}
}
/// Clear all filters
void clearFilters() {
selectedProject.value = '';
selectedStatus.value = '';
startDate.value = null;
endDate.value = null;
selectedPaidByEmployees.clear();
selectedCreatedByEmployees.clear();
}
/// Fetch master data: expense types, payment modes, and expense status
Future<void> fetchMasterData() async {
try {
final expenseTypesData = await ApiService.getMasterExpenseTypes();
if (expenseTypesData is List) {
expenseTypes.value =
expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList();
}
final paymentModesData = await ApiService.getMasterPaymentModes();
if (paymentModesData is List) {
paymentModes.value =
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
}
final expenseStatusData = await ApiService.getMasterExpenseStatus();
if (expenseStatusData is List) {
expenseStatuses.value = expenseStatusData
.map((e) => ExpenseStatusModel.fromJson(e))
.toList();
}
} catch (e) {
showAppSnackbar(
title: "Error",
message: "Failed to fetch master data: $e",
type: SnackbarType.error,
);
}
}
/// Fetch global projects
Future<void> fetchGlobalProjects() async {
try {
final response = await ApiService.getGlobalProjects();
if (response != null) {
final names = <String>[];
for (var item in response) {
final name = item['name']?.toString().trim();
final id = item['id']?.toString().trim();
if (name != null && id != null && name.isNotEmpty) {
projectsMap[name] = id;
names.add(name);
}
}
globalProjects.assignAll(names);
logSafe("Fetched ${names.length} global projects");
}
} catch (e) {
logSafe("Failed to fetch global projects: $e", level: LogLevel.error);
}
}
/// Fetch all employees
Future<void> fetchAllEmployees() async {
isLoading.value = true;
try {
final response = await ApiService.getAllEmployees();
if (response != null && response.isNotEmpty) {
allEmployees
.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
logSafe(
"All Employees fetched for Manage Bucket: ${allEmployees.length}",
level: LogLevel.info,
);
} else {
allEmployees.clear();
logSafe("No employees found for Manage Bucket.",
level: LogLevel.warning);
}
} catch (e) {
allEmployees.clear();
logSafe("Error fetching employees in Manage Bucket",
level: LogLevel.error, error: e);
}
isLoading.value = false;
update();
}
Future<void> loadMoreExpenses() async {
if (isLoading.value) return;
_pageNumber += 1;
isLoading.value = true;
final Map<String, dynamic> filterMap = {
"projectIds": selectedProject.value.isEmpty
? []
: [projectsMap[selectedProject.value] ?? ''],
"statusIds": selectedStatus.value.isEmpty ? [] : [selectedStatus.value],
"createdByIds": selectedCreatedByEmployees.map((e) => e.id).toList(),
"paidByIds": selectedPaidByEmployees.map((e) => e.id).toList(),
"startDate": startDate.value?.toIso8601String(),
"endDate": endDate.value?.toIso8601String(),
"isTransactionDate": selectedDateType.value == 'Transaction Date',
};
try {
final result = await ApiService.getExpenseListApi(
filter: jsonEncode(filterMap),
pageSize: _pageSize,
pageNumber: _pageNumber,
);
if (result != null) {
final expenseResponse = ExpenseResponse.fromJson(result);
expenses.addAll(expenseResponse.data.data);
}
} catch (e) {
logSafe("Error in loadMoreExpenses: $e", level: LogLevel.error);
} finally {
isLoading.value = false;
}
}
/// Update expense status
Future<bool> updateExpenseStatus(String expenseId, String statusId) async {
isLoading.value = true;
errorMessage.value = '';
try {
logSafe("Updating status for expense: $expenseId -> $statusId");
final success = await ApiService.updateExpenseStatusApi(
expenseId: expenseId,
statusId: statusId,
);
if (success) {
logSafe("Expense status updated successfully.");
await fetchExpenses();
return true;
} else {
errorMessage.value = "Failed to update expense status.";
return false;
}
} catch (e, stack) {
errorMessage.value = 'An unexpected error occurred.';
logSafe("Exception in updateExpenseStatus: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
return false;
} finally {
isLoading.value = false;
}
}
}

View File

@ -0,0 +1,17 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart';
import 'package:marco/model/time_line.dart';
class TimeLineController extends MyController {
List<TimeLineModel> timeline = [];
List<String> dummyTexts = List.generate(12, (index) => MyTextUtils.getDummyText(60));
@override
void onInit() {
TimeLineModel.dummyList.then((value) {
timeline = value.sublist(0, 6);
update();
});
super.onInit();
}
}

View File

@ -1,112 +1,46 @@
import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:marco/model/project_model.dart';
class LayoutController extends GetxController {
// Theme Customization
ThemeCustomizer themeCustomizer = ThemeCustomizer();
// Global Keys
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
final GlobalKey<State<StatefulWidget>> scrollKey = GlobalKey();
// Scroll
final ScrollController scrollController = ScrollController();
// Reactive State
final RxBool isLoading = true.obs;
final RxBool isLoadingProjects = true.obs;
final RxBool isProjectSelectionExpanded = true.obs;
final RxBool isProjectListExpanded = false.obs;
final RxBool isProjectDropdownExpanded = false.obs;
final RxList<ProjectModel> projects = <ProjectModel>[].obs;
final RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
// Selected Project
RxString? selectedProjectId;
ScrollController scrollController = ScrollController();
bool isLastIndex = false;
@override
void onInit() {
super.onInit();
fetchProjects();
}
@override
void onReady() {
super.onReady();
ThemeCustomizer.addListener(onChangeTheme);
}
@override
void dispose() {
ThemeCustomizer.removeListener(onChangeTheme);
scrollController.dispose();
super.dispose();
}
/// Fetch project list from API and initialize the selection.
Future<void> fetchProjects() async {
isLoading.value = true;
isLoadingProjects.value = true;
try {
final response = await ApiService.getProjects();
if (response != null && response.isNotEmpty) {
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();
projects.assignAll(fetchedProjects);
selectedProjectId = RxString(fetchedProjects.first.id.toString());
logSafe("Projects fetched: ${fetchedProjects.length}", level: LogLevel.info);
} else {
logSafe("No projects found or API call failed.", level: LogLevel.warning);
}
} catch (e, st) {
logSafe("Error fetching projects", level: LogLevel.error, error: e, stackTrace: st);
}
isLoadingProjects.value = false;
isLoading.value = false;
update(['dashboard_controller']);
}
/// Update selected project ID
void updateSelectedProject(String projectId) {
selectedProjectId?.value = projectId;
logSafe("Selected project updated", level: LogLevel.info);
}
/// Toggle expansion of the project list section
void toggleProjectListExpanded() {
isProjectListExpanded.toggle();
logSafe("Project list expanded: ${isProjectListExpanded.value}", level: LogLevel.debug);
}
/// Handle theme changes (light/dark, drawer toggles)
void onChangeTheme(ThemeCustomizer oldVal, ThemeCustomizer newVal) {
themeCustomizer = newVal;
update();
if (newVal.rightBarOpen) {
scaffoldKey.currentState?.openEndDrawer();
logSafe("Theme changed — end drawer opened", level: LogLevel.debug);
} else {
scaffoldKey.currentState?.closeEndDrawer();
logSafe("Theme changed — end drawer closed", level: LogLevel.debug);
}
}
/// Optional notification toggles (placeholder)
void enableNotificationShade() {
logSafe("Notification shade enabled (not implemented)", level: LogLevel.verbose);
enableNotificationShade() {
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
}
void disableNotificationShade() {
logSafe("Notification shade disabled (not implemented)", level: LogLevel.verbose);
disableNotificationShade() {
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
}
@override
void dispose() {
super.dispose();
ThemeCustomizer.removeListener(onChangeTheme);
scrollController.dispose();
}
}

View File

@ -0,0 +1,45 @@
import 'dart:math';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/extensions/string.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart';
import 'package:marco/model/visitor_by_channels_model.dart';
import 'package:marco/view/other/basic_table_screen.dart';
import 'package:flutter/material.dart';
class BasicTableController extends MyController {
List<Data> datas = Data.factory();
DataTableSource? data;
List<VisitorByChannelsModel> visitorByChannel = [];
@override
void onInit() {
super.onInit();
VisitorByChannelsModel.dummyList.then((value) {
visitorByChannel = value;
update();
});
data = MyData(datas);
}
void removeData(index) {
visitorByChannel.removeAt(index);
update();
}
}
class Data {
final int id, qty;
final String name;
final String code;
final double amount;
Data(this.id, this.qty, this.name, this.code, this.amount);
static factory([int seeds = 30]) {
return List.generate(
seeds,
(index) => Data(index + 1, Random().nextInt(100), MyTextUtils.getDummyText(2, withStop: false),
MyTextUtils.getDummyText(1, withStop: false).toLowerCase(), (Random().nextDouble() * 100).toStringAsPrecision(2).toDouble()));
}
}

View File

@ -0,0 +1,12 @@
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:marco/controller/my_controller.dart';
class GoogleMapScreenController extends MyController {
late GoogleMapController mapController;
LatLng center = LatLng(37.7749, -122.4194);
void onMapCreated(GoogleMapController controller) {
mapController = controller;
}
}

View File

@ -0,0 +1,480 @@
import 'dart:async';
import 'package:marco/controller/my_controller.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_maps/maps.dart';
class MapController extends MyController {
late List<Model> data;
late MapShapeSource dataSource;
late MapShapeSource source;
late List<CountryTimeInGMT> timeZones;
late MapShapeSource mapSource;
late List<CountryDensity> worldPopulationDensity;
final NumberFormat numberFormat = NumberFormat('#.#');
late MapShapeSource mapSource1;
late List<TimeDetails> worldClockData;
late MapShapeSource mapSource2;
@override
void onInit() {
data = <Model>[
Model('New South Wales', 'New South Wales'),
Model('Queensland', 'Queensland'),
Model('Northern Territory', 'Northern sTerritory'),
Model('Victoria', 'Victoria'),
Model('South Australia', 'South Australia'),
Model('Western Australia', 'Western Australia'),
Model('Tasmania', 'Tasmania'),
];
dataSource = MapShapeSource.asset(
'assets/data/australia.json',
shapeDataField: 'STATE_NAME',
dataCount: data.length,
primaryValueMapper: (int index) => data[index].state,
dataLabelMapper: (int index) => data[index].dataLabel,
);
source = const MapShapeSource.network(
'http://www.json-generator.com/api/json/get/bVqXoJvfjC?indent=2',
shapeDataField: 'name',
);
timeZones = <CountryTimeInGMT>[
CountryTimeInGMT('Albania', 'GMT+2'),
CountryTimeInGMT('Aland', 'GMT+3'),
CountryTimeInGMT('Andorra', 'GMT+1'),
CountryTimeInGMT('Austria', 'GMT+2'),
CountryTimeInGMT('Belgium', 'GMT+2'),
CountryTimeInGMT('Bulgaria', 'GMT+3'),
CountryTimeInGMT('Bosnia and Herz.', 'GMT+2'),
CountryTimeInGMT('Belarus', 'GMT+3'),
CountryTimeInGMT('Switzerland', 'GMT+2'),
CountryTimeInGMT('Czech Rep.', 'GMT+2'),
CountryTimeInGMT('Germany', 'GMT+2'),
CountryTimeInGMT('Denmark', 'GMT+2'),
CountryTimeInGMT('Spain', 'GMT+2'),
CountryTimeInGMT('Estonia', 'GMT+3'),
CountryTimeInGMT('Finland', 'GMT+3'),
CountryTimeInGMT('France', 'GMT+2'),
CountryTimeInGMT('Faeroe Is.', 'GMT+1'),
CountryTimeInGMT('United Kingdom', 'GMT+1'),
CountryTimeInGMT('Guernsey', 'GMT+1'),
CountryTimeInGMT('Greece', 'GMT+3'),
CountryTimeInGMT('Croatia', 'GMT+2'),
CountryTimeInGMT('Hungary', 'GMT+2'),
CountryTimeInGMT('Isle of Man', 'GMT+1'),
CountryTimeInGMT('Ireland', 'GMT+1'),
CountryTimeInGMT('Iceland', 'GMT+0'),
CountryTimeInGMT('Italy', 'GMT+2'),
CountryTimeInGMT('Jersey', 'GMT+1'),
CountryTimeInGMT('Kosovo', 'GMT+2'),
CountryTimeInGMT('Liechtenstein', 'GMT+2'),
CountryTimeInGMT('Lithuania', 'GMT+3'),
CountryTimeInGMT('Luxembourg', 'GMT+2'),
CountryTimeInGMT('Latvia', 'GMT+3'),
CountryTimeInGMT('Monaco', 'GMT+2'),
CountryTimeInGMT('Moldova', 'GMT+3'),
CountryTimeInGMT('Macedonia', 'GMT+2'),
CountryTimeInGMT('Malta', 'GMT+2'),
CountryTimeInGMT('Montenegro', 'GMT+2'),
CountryTimeInGMT('Netherlands', 'GMT+2'),
CountryTimeInGMT('Norway', 'GMT+2'),
CountryTimeInGMT('Poland', 'GMT+2'),
CountryTimeInGMT('Portugal', 'GMT+1'),
CountryTimeInGMT('Romania', 'GMT+3'),
CountryTimeInGMT('San Marino', 'GMT+2'),
CountryTimeInGMT('Serbia', 'GMT+2'),
CountryTimeInGMT('Slovakia', 'GMT+2'),
CountryTimeInGMT('Slovenia', 'GMT+2'),
CountryTimeInGMT('Sweden', 'GMT+2'),
CountryTimeInGMT('Ukraine', 'GMT+3'),
CountryTimeInGMT('Vatican', 'GMT+1'),
];
mapSource = MapShapeSource.asset(
'assets/data/europe_map.json',
shapeDataField: 'name',
dataCount: timeZones.length,
primaryValueMapper: (int index) => timeZones[index].countryName,
shapeColorValueMapper: (int index) => timeZones[index].gmtTime,
shapeColorMappers: <MapColorMapper>[
const MapColorMapper(value: 'GMT+0', color: Colors.lightBlue, text: 'GMT+0'),
const MapColorMapper(value: 'GMT+1', color: Colors.orangeAccent, text: 'GMT+1'),
const MapColorMapper(value: 'GMT+2', color: Colors.lightGreen, text: 'GMT+2'),
const MapColorMapper(value: 'GMT+3', color: Colors.purple, text: 'GMT+3'),
],
);
worldPopulationDensity = <CountryDensity>[
CountryDensity('Monaco', 26337),
CountryDensity('Macao', 21717),
CountryDensity('Singapore', 8358),
CountryDensity('Hong kong', 7140),
CountryDensity('Gibraltar', 3369),
CountryDensity('Bahrain', 2239),
CountryDensity('Holy See', 1820),
CountryDensity('Maldives', 1802),
CountryDensity('Malta', 1380),
CountryDensity('Bangladesh', 1265),
CountryDensity('Sint Maarten', 1261),
CountryDensity('Bermuda', 1246),
CountryDensity('Channel Islands', 915),
CountryDensity('State of Palestine', 847),
CountryDensity('Saint-Martin', 729),
CountryDensity('Mayotte', 727),
CountryDensity('Taiwan', 672),
CountryDensity('Barbados', 668),
CountryDensity('Lebanon', 667),
CountryDensity('Mauritius', 626),
CountryDensity('Aruba', 593),
CountryDensity('San Marino', 565),
CountryDensity('Nauru', 541),
CountryDensity('Korea', 527),
CountryDensity('Rwanda', 525),
CountryDensity('Netherlands', 508),
CountryDensity('Comoros', 467),
CountryDensity('India', 464),
CountryDensity('Burundi', 463),
CountryDensity('Saint-Barthélemy', 449),
CountryDensity('Haiti', 413),
CountryDensity('Israel', 400),
CountryDensity('Tuvalu', 393),
CountryDensity('Belgium', 382),
CountryDensity('Curacao', 369),
CountryDensity('Philippines', 367),
CountryDensity('Reunion', 358),
CountryDensity('Martinique', 354),
CountryDensity('Japan', 346),
CountryDensity('Sri Lanka', 341),
CountryDensity('Grenada', 331),
CountryDensity('Marshall Islands', 328),
CountryDensity('Puerto Rico', 322),
CountryDensity('Vietnam', 313),
CountryDensity('El Salvador', 313),
CountryDensity('Guam', 312),
CountryDensity('Saint Lucia', 301),
CountryDensity('United States Virgin Islands', 298),
CountryDensity('Pakistan', 286),
CountryDensity('Saint Vincent and the Grenadines', 284),
CountryDensity('United Kingdom', 280),
CountryDensity('American Samoa', 276),
CountryDensity('Cayman Islands', 273),
CountryDensity('Jamaica', 273),
CountryDensity('Trinidad and Tobago', 272),
CountryDensity('Qatar', 248),
CountryDensity('Guadeloupe', 245),
CountryDensity('Luxembourg', 241),
CountryDensity('Germany', 240),
CountryDensity('Kuwait', 239),
CountryDensity('Gambia', 238),
CountryDensity('Liechtenstein', 238),
CountryDensity('Uganda', 228),
CountryDensity('Sao Tome and Principe', 228),
CountryDensity('Nigeria', 226),
CountryDensity('Dominican Rep.', 224),
CountryDensity('Antigua and Barbuda', 222),
CountryDensity('Switzerland', 219),
CountryDensity('Dem. Rep. Korea', 214),
CountryDensity('Seychelles', 213),
CountryDensity('Italy', 205),
CountryDensity('Saint Kitts and Nevis', 204),
CountryDensity('Nepal', 203),
CountryDensity('Malawi', 202),
CountryDensity('British Virgin Islands', 201),
CountryDensity('Guatemala', 167),
CountryDensity('Anguilla', 166),
CountryDensity('Andorra', 164),
CountryDensity('Micronesia', 164),
CountryDensity('China', 153),
CountryDensity('Togo', 152),
CountryDensity('Indonesia', 151),
CountryDensity('Isle of Man', 149),
CountryDensity('Kiribati', 147),
CountryDensity('Tonga', 146),
CountryDensity('Czech Rep.', 138),
CountryDensity('Cabo Verde', 138),
CountryDensity('Thailand', 136),
CountryDensity('Ghana', 136),
CountryDensity('Denmark', 136),
CountryDensity('Tokelau', 135),
CountryDensity('Cyprus', 130),
CountryDensity('Northern Mariana Islands', 125),
CountryDensity('Poland', 123),
CountryDensity('Moldova', 122),
CountryDensity('Azerbaijan', 122),
CountryDensity('France', 119),
CountryDensity('United Arab Emirates', 118),
CountryDensity('Ethiopia', 115),
CountryDensity('Jordan', 114),
CountryDensity('Slovakia', 113),
CountryDensity('Portugal', 111),
CountryDensity('Sierra Leone', 110),
CountryDensity('Turkey', 109),
CountryDensity('Austria', 109),
CountryDensity('Benin', 107),
CountryDensity('Hungary', 106),
CountryDensity('Cuba', 106),
CountryDensity('Albania', 105),
CountryDensity('Armenia', 104),
CountryDensity('Slovenia', 103),
CountryDensity('Egypt', 102),
CountryDensity('Serbia', 99),
CountryDensity('Costa Rica', 99),
CountryDensity('Malaysia', 98),
CountryDensity('Dominica', 95),
CountryDensity('Syria', 95),
CountryDensity('Cambodia', 94),
CountryDensity('Kenya', 94),
CountryDensity('Spain', 93),
CountryDensity('Iraq', 92),
CountryDensity('Timor-Leste', 88),
CountryDensity('Honduras', 88),
CountryDensity('Senegal', 86),
CountryDensity('Romania', 83),
CountryDensity('Myanmar', 83),
CountryDensity('Brunei Darussalam', 83),
CountryDensity("Côte d'Ivoire", 82),
CountryDensity('Morocco', 82),
CountryDensity('Macedonia', 82),
CountryDensity('Greece', 80),
CountryDensity('Wallis and Futuna Islands', 80),
CountryDensity('Bonaire, Sint Eustatius and Saba', 79),
CountryDensity('Uzbekistan', 78),
CountryDensity('French Polynesia', 76),
CountryDensity('Burkina Faso', 76),
CountryDensity('Tunisia', 76),
CountryDensity('Ukraine', 75),
CountryDensity('Croatia', 73),
CountryDensity('Cook Islands', 73),
CountryDensity('Ireland', 71),
CountryDensity('Ecuador', 71),
CountryDensity('Lesotho', 70),
CountryDensity('Samoa', 70),
CountryDensity('Guinea-Bissau', 69),
CountryDensity('Tajikistan', 68),
CountryDensity('Eswatini', 67),
CountryDensity('Tanzania', 67),
CountryDensity('Mexico', 66),
CountryDensity('Bosnia and Herz.', 64),
CountryDensity('Bulgaria', 64),
CountryDensity('Afghanistan', 59),
CountryDensity('Panama', 58),
CountryDensity('Georgia', 57),
CountryDensity('Yemen', 56),
CountryDensity('Cameroon', 56),
CountryDensity('Nicaragua', 55),
CountryDensity('Guinea', 53),
CountryDensity('Liberia', 52),
CountryDensity('Iran', 51),
CountryDensity('Eq. Guinea', 50),
CountryDensity('Montserrat', 49),
CountryDensity('Fiji', 49),
CountryDensity('South Africa', 48),
CountryDensity('Madagascar', 47),
CountryDensity('Montenegro', 46),
CountryDensity('Belarus', 46),
CountryDensity('Colombia', 45),
CountryDensity('Lithuania', 43),
CountryDensity('Djibouti', 42),
CountryDensity('Turks and Caicos Islands', 40),
CountryDensity('Mozambique', 39),
CountryDensity('Dem. Rep. Congo', 39),
CountryDensity('Palau', 39),
CountryDensity('Bahamas', 39),
CountryDensity('Zimbabwe', 38),
CountryDensity('United States of America', 36),
CountryDensity('Eritrea', 35),
CountryDensity('Faroe Islands', 35),
CountryDensity('Kyrgyzstan', 34),
CountryDensity('Venezuela', 32),
CountryDensity('Lao PDR', 31),
CountryDensity('Estonia', 31),
CountryDensity('Latvia', 30),
CountryDensity('Angola', 26),
CountryDensity('Peru', 25),
CountryDensity('Chile', 25),
CountryDensity('Brazil', 25),
CountryDensity('Somalia', 25),
CountryDensity('Vanuatu', 25),
CountryDensity('Saint Pierre and Miquelon', 25),
CountryDensity('Sudan', 24),
CountryDensity('Zambia', 24),
CountryDensity('Sweden', 24),
CountryDensity('Solomon Islands', 24),
CountryDensity('Bhutan', 20),
CountryDensity('Uruguay', 19),
CountryDensity('Papua New Guinea', 19),
CountryDensity('Niger', 19),
CountryDensity('Algeria', 18),
CountryDensity('S. Sudan', 18),
CountryDensity('New Zealand', 18),
CountryDensity('Finland', 18),
CountryDensity('Paraguay', 17),
CountryDensity('Belize', 17),
CountryDensity('Mali', 16),
CountryDensity('Argentina', 16),
CountryDensity('Oman', 16),
CountryDensity('Saudi Arabia', 16),
CountryDensity('Congo', 16),
CountryDensity('New Caledonia', 15),
CountryDensity('Saint Helena', 15),
CountryDensity('Norway', 14),
CountryDensity('Chad', 13),
CountryDensity('Turkmenistan', 12),
CountryDensity('Bolivia', 10),
CountryDensity('Russia', 8),
CountryDensity('Gabon', 8),
CountryDensity('Central African Rep.', 7),
CountryDensity('Kazakhstan', 6),
CountryDensity('Niue', 6),
CountryDensity('Mauritania', 4),
CountryDensity('Canada', 4),
CountryDensity('Botswana', 4),
CountryDensity('Guyana', 3),
CountryDensity('Libya', 3),
CountryDensity('Suriname', 3),
CountryDensity('French Guiana', 3),
CountryDensity('Iceland', 3),
CountryDensity('Australia', 3),
CountryDensity('Namibia', 3),
CountryDensity('W. Sahara', 2),
CountryDensity('Mongolia', 2),
CountryDensity('Falkland Is.', 0.2),
CountryDensity('Greenland', 0.1),
];
mapSource1 = MapShapeSource.asset(
'assets/data/world_map.json',
shapeDataField: 'name',
dataCount: worldPopulationDensity.length,
primaryValueMapper: (int index) => worldPopulationDensity[index].countryName,
shapeColorValueMapper: (int index) => worldPopulationDensity[index].density,
shapeColorMappers: <MapColorMapper>[
const MapColorMapper(from: 0, to: 100, color: Color.fromRGBO(128, 159, 255, 1), text: '{0},{100}'),
const MapColorMapper(from: 100, to: 500, color: Color.fromRGBO(51, 102, 255, 1), text: '500'),
const MapColorMapper(from: 500, to: 1000, color: Color.fromRGBO(0, 57, 230, 1), text: '1k'),
const MapColorMapper(from: 1000, to: 5000, color: Color.fromRGBO(0, 45, 179, 1), text: '5k'),
const MapColorMapper(from: 5000, to: 50000, color: Color.fromRGBO(0, 26, 102, 1), text: '50k'),
],
);
final DateTime currentTime = DateTime.now().toUtc();
worldClockData = <TimeDetails>[
TimeDetails('Seattle', 47.60621, -122.332071, currentTime.subtract(const Duration(hours: 7))),
TimeDetails('Belem', -1.455833, -48.503887, currentTime.subtract(const Duration(hours: 3))),
TimeDetails('Greenland', 71.706936, -42.604303, currentTime.subtract(const Duration(hours: 2))),
TimeDetails('Yakutsk', 62.035452, 129.675475, currentTime.add(const Duration(hours: 9))),
TimeDetails('Delhi', 28.704059, 77.10249, currentTime.add(const Duration(hours: 5, minutes: 30))),
TimeDetails('Brisbane', -27.469771, 153.025124, currentTime.add(const Duration(hours: 10))),
TimeDetails('Harare', -17.825166, 31.03351, currentTime.add(const Duration(hours: 2))),
];
mapSource2 = const MapShapeSource.asset(
'assets/data/world_map.json',
shapeDataField: 'name',
);
super.onInit();
}
@override
void dispose() {
timeZones.clear();
worldPopulationDensity.clear();
super.dispose();
}
}
class TimeDetails {
TimeDetails(this.countryName, this.latitude, this.longitude, this.date);
final String countryName;
final double latitude;
final double longitude;
final DateTime date;
}
class CountryDensity {
CountryDensity(this.countryName, this.density);
final String countryName;
final double density;
}
class CountryTimeInGMT {
CountryTimeInGMT(this.countryName, this.gmtTime);
final String countryName;
final String gmtTime;
}
class Model {
Model(this.state, this.dataLabel);
String state;
String dataLabel;
}
class ClockWidget extends StatefulWidget {
const ClockWidget({super.key, required this.countryName, required this.date});
final String countryName;
final DateTime date;
@override
_ClockWidgetState createState() => _ClockWidgetState();
}
class _ClockWidgetState extends State<ClockWidget> {
late String _currentTime;
late DateTime _date;
Timer? _timer;
@override
void initState() {
_date = widget.date;
_currentTime = _getFormattedDateTime(widget.date);
_timer = Timer.periodic(const Duration(seconds: 1), (Timer t) => _updateTime(_date));
super.initState();
}
@override
void dispose() {
_timer!.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Center(
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.red),
),
),
Text(
widget.countryName,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontWeight: FontWeight.bold),
),
Center(
child: Text(_currentTime, style: Theme.of(context).textTheme.labelSmall!.copyWith(letterSpacing: 0.5, fontWeight: FontWeight.w500)),
),
],
);
}
void _updateTime(DateTime currentDate) {
_date = currentDate.add(const Duration(seconds: 1));
setState(() {
_currentTime = DateFormat('hh:mm:ss a').format(_date);
});
}
String _getFormattedDateTime(DateTime dateTime) {
return DateFormat('hh:mm:ss a').format(dateTime);
}
}

View File

@ -0,0 +1,320 @@
import 'package:flutter/material.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'dart:ui' as ui;
class ChartData {
ChartData(this.x, this.y, this.y2);
final double x;
final double y;
final double y2;
}
class SyncfusionChartController extends MyController {
List<ChartData>? chartData;
List<ChartSampleData>? gdpChartData;
TooltipBehavior? tooltipBehavior;
TooltipBehavior? areaTooltipBehavior;
List<ChartSampleData>? barChartData;
TooltipBehavior? bubbleTooltipBehavior;
List<ChartSampleData>? scatterChartData;
TooltipBehavior? scatterTooltipBehavior;
List<ChartSampleData>? stepLineChartData;
@override
void onInit() {
stepLineChartData = <ChartSampleData>[
ChartSampleData(x: 2006, y: 378, yValue: 463, secondSeriesYValue: 519, thirdSeriesYValue: 570),
ChartSampleData(x: 2007, y: 416, yValue: 449, secondSeriesYValue: 508, thirdSeriesYValue: 579),
ChartSampleData(x: 2008, y: 404, yValue: 458, secondSeriesYValue: 502, thirdSeriesYValue: 563),
ChartSampleData(x: 2009, y: 390, yValue: 450, secondSeriesYValue: 495, thirdSeriesYValue: 550),
ChartSampleData(x: 2010, y: 376, yValue: 425, secondSeriesYValue: 485, thirdSeriesYValue: 545),
ChartSampleData(x: 2011, y: 365, yValue: 430, secondSeriesYValue: 470, thirdSeriesYValue: 525)
];
scatterTooltipBehavior = TooltipBehavior(enable: true, header: '', canShowMarker: false);
scatterChartData = <ChartSampleData>[
ChartSampleData(x: 1950, y: 0.8, secondSeriesYValue: 1.4, thirdSeriesYValue: 2),
ChartSampleData(x: 1955, y: 1.2, secondSeriesYValue: 1.7, thirdSeriesYValue: 2.4),
ChartSampleData(x: 1960, y: 0.9, secondSeriesYValue: 1.5, thirdSeriesYValue: 2.2),
ChartSampleData(x: 1965, y: 1, secondSeriesYValue: 1.6, thirdSeriesYValue: 2.5),
ChartSampleData(x: 1970, y: 0.8, secondSeriesYValue: 1.4, thirdSeriesYValue: 2.2),
ChartSampleData(x: 1975, y: 1, secondSeriesYValue: 1.8, thirdSeriesYValue: 2.4),
ChartSampleData(x: 1980, y: 1, secondSeriesYValue: 1.7, thirdSeriesYValue: 2),
ChartSampleData(x: 1985, y: 1.2, secondSeriesYValue: 1.9, thirdSeriesYValue: 2.3),
ChartSampleData(x: 1990, y: 1.1, secondSeriesYValue: 1.4, thirdSeriesYValue: 2),
ChartSampleData(x: 1995, y: 1.2, secondSeriesYValue: 1.8, thirdSeriesYValue: 2.2),
ChartSampleData(x: 2000, y: 1.4, secondSeriesYValue: 2, thirdSeriesYValue: 2.4),
];
bubbleTooltipBehavior = TooltipBehavior(
enable: true, header: '', canShowMarker: false, format: 'Literacy rate : point.x%\nGDP growth rate : point.y\nPopulation : point.sizeB');
areaTooltipBehavior = TooltipBehavior(enable: true, canShowMarker: false, tooltipPosition: TooltipPosition.pointer);
chartData = <ChartData>[
ChartData(2005, 21, 28),
ChartData(2006, 24, 44),
ChartData(2007, 36, 48),
ChartData(2008, 38, 50),
ChartData(2009, 54, 66),
ChartData(2010, 57, 78),
ChartData(2011, 70, 84)
];
barChartData = <ChartSampleData>[
ChartSampleData(x: 'France', y: 84452000, secondSeriesYValue: 82682000, thirdSeriesYValue: 86861000),
ChartSampleData(x: 'Spain', y: 68175000, secondSeriesYValue: 75315000, thirdSeriesYValue: 81786000),
ChartSampleData(x: 'US', y: 77774000, secondSeriesYValue: 76407000, thirdSeriesYValue: 76941000),
ChartSampleData(x: 'Italy', y: 50732000, secondSeriesYValue: 52372000, thirdSeriesYValue: 58253000),
ChartSampleData(x: 'Mexico', y: 32093000, secondSeriesYValue: 35079000, thirdSeriesYValue: 39291000),
ChartSampleData(x: 'UK', y: 34436000, secondSeriesYValue: 35814000, thirdSeriesYValue: 37651000),
];
gdpChartData = <ChartSampleData>[
ChartSampleData(x: 1997, y: 17.79, secondSeriesYValue: 20.32, thirdSeriesYValue: 22.44),
ChartSampleData(x: 1998, y: 18.20, secondSeriesYValue: 21.46, thirdSeriesYValue: 25.18),
ChartSampleData(x: 1999, y: 17.44, secondSeriesYValue: 21.72, thirdSeriesYValue: 24.15),
ChartSampleData(x: 2000, y: 19, secondSeriesYValue: 22.86, thirdSeriesYValue: 25.83),
ChartSampleData(x: 2001, y: 18.93, secondSeriesYValue: 22.87, thirdSeriesYValue: 25.69),
ChartSampleData(x: 2002, y: 17.58, secondSeriesYValue: 21.87, thirdSeriesYValue: 24.75),
ChartSampleData(x: 2003, y: 16.83, secondSeriesYValue: 21.67, thirdSeriesYValue: 27.38),
ChartSampleData(x: 2004, y: 17.93, secondSeriesYValue: 21.65, thirdSeriesYValue: 25.31)
];
tooltipBehavior = TooltipBehavior(enable: true, canShowMarker: false, header: '');
super.onInit();
}
List<StepLineSeries<ChartSampleData, num>> getDashedStepLineSeries() {
return <StepLineSeries<ChartSampleData, num>>[
StepLineSeries<ChartSampleData, num>(
dataSource: stepLineChartData,
xValueMapper: (ChartSampleData data, _) => data.x as num,
yValueMapper: (ChartSampleData data, _) => data.y,
name: 'USA',
dashArray: const <double>[10, 5]),
StepLineSeries<ChartSampleData, num>(
dataSource: stepLineChartData,
xValueMapper: (ChartSampleData data, _) => data.x as num,
yValueMapper: (ChartSampleData data, _) => data.yValue,
name: 'UK',
dashArray: const <double>[10, 5]),
StepLineSeries<ChartSampleData, num>(
dataSource: stepLineChartData,
xValueMapper: (ChartSampleData data, _) => data.x as num,
yValueMapper: (ChartSampleData data, _) => data.secondSeriesYValue,
name: 'Korea',
dashArray: const <double>[10, 5]),
StepLineSeries<ChartSampleData, num>(
dataSource: stepLineChartData,
xValueMapper: (ChartSampleData data, _) => data.x as num,
yValueMapper: (ChartSampleData data, _) => data.thirdSeriesYValue,
name: 'Japan',
dashArray: const <double>[10, 5])
];
}
List<ScatterSeries<ChartSampleData, num>> getScatterShapesSeries() {
return <ScatterSeries<ChartSampleData, num>>[
ScatterSeries<ChartSampleData, num>(
dataSource: scatterChartData,
xValueMapper: (ChartSampleData sales, _) => sales.x as num,
yValueMapper: (ChartSampleData sales, _) => sales.y,
markerSettings: const MarkerSettings(width: 15, height: 15, shape: DataMarkerType.diamond),
name: 'India'),
ScatterSeries<ChartSampleData, num>(
dataSource: scatterChartData,
xValueMapper: (ChartSampleData sales, _) => sales.x as num,
yValueMapper: (ChartSampleData sales, _) => sales.secondSeriesYValue,
markerSettings: const MarkerSettings(width: 15, height: 15, shape: DataMarkerType.triangle),
name: 'China'),
ScatterSeries<ChartSampleData, num>(
dataSource: scatterChartData,
xValueMapper: (ChartSampleData sales, _) => sales.x as num,
yValueMapper: (ChartSampleData sales, _) => sales.thirdSeriesYValue,
markerSettings: const MarkerSettings(width: 15, height: 15, shape: DataMarkerType.pentagon),
name: 'Japan')
];
}
List<BubbleSeries<ChartSampleData, num>> getMultipleBubbleSeries() {
return <BubbleSeries<ChartSampleData, num>>[
BubbleSeries<ChartSampleData, num>(
opacity: 0.7,
name: 'North America',
dataSource: <ChartSampleData>[
ChartSampleData(x: 'US', xValue: 99.4, y: 2.2, size: 0.312),
ChartSampleData(x: 'Mexico', xValue: 86.1, y: 4.0, size: 0.115)
],
xValueMapper: (ChartSampleData sales, _) => sales.xValue as num,
yValueMapper: (ChartSampleData sales, _) => sales.y,
sizeValueMapper: (ChartSampleData sales, _) => sales.size),
BubbleSeries<ChartSampleData, num>(
opacity: 0.7,
name: 'Europe',
dataSource: <ChartSampleData>[
ChartSampleData(x: 'Germany', xValue: 99, y: 0.7, size: 0.0818),
ChartSampleData(x: 'Russia', xValue: 99.6, y: 3.4, size: 0.143),
ChartSampleData(x: 'Netherland', xValue: 79.2, y: 3.9, size: 0.162)
],
xValueMapper: (ChartSampleData sales, _) => sales.xValue as num,
yValueMapper: (ChartSampleData sales, _) => sales.y,
sizeValueMapper: (ChartSampleData sales, _) => sales.size),
BubbleSeries<ChartSampleData, num>(
opacity: 0.7,
dataSource: <ChartSampleData>[
ChartSampleData(x: 'China', xValue: 92.2, y: 7.8, size: 1.347),
ChartSampleData(x: 'India', xValue: 74, y: 6.5, size: 1.241),
ChartSampleData(x: 'Indonesia', xValue: 90.4, y: 6.0, size: 0.238),
ChartSampleData(x: 'Japan', xValue: 99, y: 0.2, size: 0.128),
ChartSampleData(x: 'Philippines', xValue: 92.6, y: 6.6, size: 0.096),
ChartSampleData(x: 'Hong Kong', xValue: 82.2, y: 3.97, size: 0.7),
ChartSampleData(x: 'Jordan', xValue: 72.5, y: 4.5, size: 0.7),
ChartSampleData(x: 'Australia', xValue: 81, y: 3.5, size: 0.21),
ChartSampleData(x: 'Mongolia', xValue: 66.8, y: 3.9, size: 0.028),
ChartSampleData(x: 'Taiwan', xValue: 78.4, y: 2.9, size: 0.231),
],
name: 'Asia',
xValueMapper: (ChartSampleData sales, _) => sales.xValue as num,
yValueMapper: (ChartSampleData sales, _) => sales.y,
sizeValueMapper: (ChartSampleData sales, _) => sales.size),
BubbleSeries<ChartSampleData, num>(
opacity: 0.7,
name: 'Africa',
dataSource: <ChartSampleData>[
ChartSampleData(x: 'Egypt', xValue: 72, y: 2.0, size: 0.0826),
ChartSampleData(x: 'Nigeria', xValue: 61.3, y: 1.45, size: 0.162),
],
xValueMapper: (ChartSampleData sales, _) => sales.xValue as num,
yValueMapper: (ChartSampleData sales, _) => sales.y,
sizeValueMapper: (ChartSampleData sales, _) => sales.size),
];
}
List<BarSeries<ChartSampleData, String>> getDefaultBarSeries() {
return <BarSeries<ChartSampleData, String>>[
BarSeries<ChartSampleData, String>(
dataSource: barChartData,
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
yValueMapper: (ChartSampleData sales, _) => sales.y,
name: '2015'),
BarSeries<ChartSampleData, String>(
dataSource: barChartData,
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
yValueMapper: (ChartSampleData sales, _) => sales.secondSeriesYValue,
name: '2016'),
BarSeries<ChartSampleData, String>(
dataSource: barChartData,
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
yValueMapper: (ChartSampleData sales, _) => sales.thirdSeriesYValue,
name: '2017')
];
}
List<CartesianSeries<ChartSampleData, String>> getAreaZoneSeries() {
return <CartesianSeries<ChartSampleData, String>>[
AreaSeries<ChartSampleData, String>(
dataSource: <ChartSampleData>[
ChartSampleData(x: 'Jan', y: 35.53),
ChartSampleData(
x: 'Feb',
y: 46.06,
),
ChartSampleData(
x: 'Mar',
y: 46.06,
),
ChartSampleData(
x: 'Apr',
y: 50.86,
),
ChartSampleData(
x: 'May',
y: 60.89,
),
ChartSampleData(
x: 'Jun',
y: 70.27,
),
ChartSampleData(
x: 'Jul',
y: 75.65,
),
ChartSampleData(x: 'Aug', y: 74.7),
ChartSampleData(
x: 'Sep',
y: 65.91,
),
ChartSampleData(x: 'Oct', y: 54.28),
ChartSampleData(x: 'Nov', y: 46.33),
ChartSampleData(x: 'Dec', y: 35.71),
],
name: 'US',
onCreateShader: (ShaderDetails details) {
return ui.Gradient.linear(details.rect.bottomLeft, details.rect.bottomRight, const <Color>[
Color.fromRGBO(116, 182, 194, 1),
Color.fromRGBO(75, 189, 138, 1),
Color.fromRGBO(75, 189, 138, 1),
Color.fromRGBO(255, 186, 83, 1),
Color.fromRGBO(255, 186, 83, 1),
Color.fromRGBO(194, 110, 21, 1),
Color.fromRGBO(194, 110, 21, 1),
Color.fromRGBO(116, 182, 194, 1),
], <double>[
0.165,
0.165,
0.416,
0.416,
0.666,
0.666,
0.918,
0.918
]);
},
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
yValueMapper: (ChartSampleData sales, _) => sales.y,
),
];
}
List<LineSeries<ChartData, num>> getDefaultLineSeries() {
return <LineSeries<ChartData, num>>[
LineSeries<ChartData, num>(
dataSource: chartData,
xValueMapper: (ChartData sales, _) => sales.x,
yValueMapper: (ChartData sales, _) => sales.y,
name: 'Germany',
markerSettings: MarkerSettings(isVisible: true)),
LineSeries<ChartData, num>(
dataSource: chartData,
name: 'England',
xValueMapper: (ChartData sales, _) => sales.x,
yValueMapper: (ChartData sales, _) => sales.y2,
markerSettings: MarkerSettings(isVisible: true))
];
}
List<SplineSeries<ChartSampleData, num>> getDashedSplineSeries() {
return <SplineSeries<ChartSampleData, num>>[
SplineSeries<ChartSampleData, num>(
dataSource: gdpChartData,
xValueMapper: (ChartSampleData sales, _) => sales.x as num,
yValueMapper: (ChartSampleData sales, _) => sales.y,
name: 'Brazil',
dashArray: <double>[12, 3, 3, 3],
markerSettings: MarkerSettings(isVisible: true)),
SplineSeries<ChartSampleData, num>(
dataSource: gdpChartData,
name: 'Sweden',
dashArray: <double>[12, 3, 3, 3],
xValueMapper: (ChartSampleData sales, _) => sales.x as num,
yValueMapper: (ChartSampleData sales, _) => sales.secondSeriesYValue,
markerSettings: MarkerSettings(isVisible: true)),
SplineSeries<ChartSampleData, num>(
dataSource: gdpChartData,
dashArray: <double>[12, 3, 3, 3],
name: 'Greece',
xValueMapper: (ChartSampleData sales, _) => sales.x as num,
yValueMapper: (ChartSampleData sales, _) => sales.thirdSeriesYValue,
markerSettings: MarkerSettings(isVisible: true))
];
}
}

View File

@ -2,74 +2,26 @@ import 'dart:async';
import 'dart:convert';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/services/permission_service.dart';
import 'package:marco/model/user_permission.dart';
import 'package:marco/model/employees/employee_info.dart';
import 'package:marco/model/employee_info.dart';
import 'package:marco/model/projects_model.dart';
final log = Logger();
class PermissionController extends GetxController {
var permissions = <UserPermission>[].obs;
var employeeInfo = Rxn<EmployeeInfo>();
var projectsInfo = <ProjectInfo>[].obs;
Timer? _refreshTimer;
var isLoading = true.obs;
@override
void onInit() {
super.onInit();
_initialize();
}
Future<void> _initialize() async {
final token = await _getAuthToken();
if (token?.isNotEmpty ?? false) {
await loadData(token!);
_startAutoRefresh();
} else {
logSafe("Token is null or empty. Skipping API load and auto-refresh.",
level: LogLevel.warning);
}
}
Future<String?> _getAuthToken() async {
try {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('jwt_token');
logSafe("Auth token retrieved: $token", level: LogLevel.debug);
return token;
} catch (e, stacktrace) {
logSafe("Error retrieving auth token",
level: LogLevel.error, error: e, stackTrace: stacktrace);
return null;
}
}
Future<void> loadData(String token) async {
try {
isLoading.value = true;
final userData = await PermissionService.fetchAllUserData(token);
_updateState(userData);
await _storeData();
logSafe("Data loaded and state updated successfully.");
} catch (e, stacktrace) {
logSafe("Error loading data from API",
level: LogLevel.error, error: e, stackTrace: stacktrace);
} finally {
isLoading.value = false;
}
}
void _updateState(Map<String, dynamic> userData) {
try {
permissions.assignAll(userData['permissions']);
employeeInfo.value = userData['employeeInfo'];
projectsInfo.assignAll(userData['projects']);
logSafe("State updated with user data.");
} catch (e, stacktrace) {
logSafe("Error updating state",
level: LogLevel.error, error: e, stackTrace: stacktrace);
}
_loadDataFromAPI();
_startAutoRefresh();
}
Future<void> _storeData() async {
@ -95,61 +47,76 @@ class PermissionController extends GetxController {
);
}
logSafe("User data successfully stored in SharedPreferences.");
log.i("User data successfully stored in SharedPreferences.");
} catch (e, stacktrace) {
logSafe("Error storing data",
level: LogLevel.error, error: e, stackTrace: stacktrace);
log.e("Error storing data", error: e, stackTrace: stacktrace);
}
}
Future<void> _loadDataFromAPI() async {
final token = await _getAuthToken();
if (token?.isNotEmpty ?? false) {
await loadData(token!);
} else {
log.w("No token found for loading API data.");
}
}
Future<void> loadData(String token) async {
try {
final userData = await PermissionService.fetchAllUserData(token);
_updateState(userData);
await _storeData();
log.i("Data loaded and state updated successfully.");
} catch (e, stacktrace) {
log.e("Error loading data from API", error: e, stackTrace: stacktrace);
}
}
void _updateState(Map<String, dynamic> userData) {
try {
permissions.assignAll(userData['permissions']);
employeeInfo.value = userData['employeeInfo'];
projectsInfo.assignAll(userData['projects']);
log.i("State updated with new user data.");
} catch (e, stacktrace) {
log.e("Error updating state", error: e, stackTrace: stacktrace);
}
}
Future<String?> _getAuthToken() async {
try {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('jwt_token');
} catch (e, stacktrace) {
log.e("Error retrieving auth token", error: e, stackTrace: stacktrace);
return null;
}
}
void _startAutoRefresh() {
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
logSafe("Auto-refresh triggered.");
final token = await _getAuthToken();
if (token?.isNotEmpty ?? false) {
await loadData(token!);
} else {
logSafe("Token missing during auto-refresh. Skipping.",
level: LogLevel.warning);
}
log.i("Auto-refresh triggered.");
await _loadDataFromAPI();
});
}
bool hasPermission(String permissionId) {
final hasPerm = permissions.any((p) => p.id == permissionId);
logSafe("Checking permission $permissionId: $hasPerm",
level: LogLevel.debug);
log.d("Checking permission $permissionId: $hasPerm");
return hasPerm;
}
bool isUserAssignedToProject(String projectId) {
final assigned = projectsInfo.any((project) => project.id == projectId);
logSafe("Checking project assignment for $projectId: $assigned",
level: LogLevel.debug);
log.d("Checking project assignment for $projectId: $assigned");
return assigned;
}
List<String> get allowedPermissionIds {
final ids = permissions.map((p) => p.id).toList();
logSafe("[PermissionController] Allowed Permission IDs: $ids",
level: LogLevel.debug);
return ids;
}
bool hasAnyPermission(List<String> ids) {
logSafe("[PermissionController] Checking if any of these are allowed: $ids",
level: LogLevel.debug);
final allowed = allowedPermissionIds;
final result = ids.any((id) => allowed.contains(id));
logSafe("[PermissionController] Permission match result: $result",
level: LogLevel.debug);
return result;
}
@override
void onClose() {
_refreshTimer?.cancel();
logSafe("PermissionController disposed and auto-refresh timer cancelled.");
log.i("PermissionController disposed and timer cancelled.");
super.onClose();
}
}

View File

@ -1,83 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/global_project_model.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
class ProjectController extends GetxController {
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;
RxString selectedProjectId = ''.obs;
RxBool isProjectListExpanded = false.obs;
RxBool isProjectSelectionExpanded = false.obs;
RxBool isProjectDropdownExpanded = false.obs;
RxBool isLoading = true.obs;
RxBool isLoadingProjects = true.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
GlobalProjectModel? get selectedProject {
if (selectedProjectId.value.isEmpty) return null;
return projects.firstWhereOrNull((p) => p.id == selectedProjectId.value);
}
@override
void onInit() {
super.onInit();
fetchProjects();
}
void clearProjects() {
projects.clear();
selectedProjectId.value = '';
isProjectSelectionExpanded.value = false;
isProjectListExpanded.value = false;
isProjectDropdownExpanded.value = false;
isLoadingProjects.value = false;
isLoading.value = false;
uploadingStates.clear();
LocalStorage.saveString('selectedProjectId', '');
logSafe("Projects cleared and UI states reset.");
update();
}
/// Fetches projects and initializes selected project.
Future<void> fetchProjects() async {
isLoadingProjects.value = true;
isLoading.value = true;
final response = await ApiService.getGlobalProjects();
if (response != null && response.isNotEmpty) {
projects.assignAll(
response.map((json) => GlobalProjectModel.fromJson(json)).toList(),
);
String? savedId = LocalStorage.getString('selectedProjectId');
if (savedId != null && projects.any((p) => p.id == savedId)) {
selectedProjectId.value = savedId;
} else {
selectedProjectId.value = projects.first.id.toString();
LocalStorage.saveString('selectedProjectId', selectedProjectId.value);
}
isProjectSelectionExpanded.value = false;
logSafe("Projects fetched: ${projects.length}");
} else {
logSafe("No Global projects found or API call failed.", level: LogLevel.warning);
}
isLoadingProjects.value = false;
isLoading.value = false;
update(['dashboard_controller']);
}
Future<void> updateSelectedProject(String projectId) async {
selectedProjectId.value = projectId;
await LocalStorage.saveString('selectedProjectId', projectId);
logSafe("Selected project updated to $projectId");
update(['selected_project']);
}
}

View File

@ -1,152 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/dailyTaskPlanning/master_work_category_model.dart';
class AddTaskController extends GetxController {
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator();
RxnString selectedCategoryId = RxnString();
RxnString selectedCategoryName = RxnString();
var categoryIdNameMap = <String, String>{}.obs;
List<Map<String, dynamic>> roles = [];
RxnString selectedRoleId = RxnString();
RxBool isLoadingWorkMasterCategories = false.obs;
RxList<WorkCategoryModel> workMasterCategories = <WorkCategoryModel>[].obs;
RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
fetchWorkMasterCategories();
}
String? formFieldValidator(String? value, {required String fieldType}) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
if (fieldType == "description" && value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
return null;
}
Future<bool> assignDailyTask({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
}) async {
logSafe("Starting task assignment...", level: LogLevel.info);
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
);
if (response == true) {
logSafe("Task assigned successfully.", level: LogLevel.info);
showAppSnackbar(
title: "Success",
message: "Task assigned successfully!",
type: SnackbarType.success,
);
return true;
} else {
logSafe("Failed to assign task.", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Failed to assign task.",
type: SnackbarType.error,
);
return false;
}
}
Future<bool> createTask({
required String parentTaskId,
required String workAreaId,
required String activityId,
required int plannedTask,
required String comment,
required String categoryId,
DateTime? assignmentDate,
}) async {
logSafe("Creating new task...", level: LogLevel.info);
final response = await ApiService.createTask(
parentTaskId: parentTaskId,
plannedTask: plannedTask,
comment: comment,
workAreaId: workAreaId,
activityId: activityId,
assignmentDate: assignmentDate,
categoryId: categoryId,
);
if (response == true) {
logSafe("Task created successfully.", level: LogLevel.info);
showAppSnackbar(
title: "Success",
message: "Task created successfully!",
type: SnackbarType.success,
);
return true;
} else {
logSafe("Failed to create task.", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Failed to create task.",
type: SnackbarType.error,
);
return false;
}
}
Future<void> fetchWorkMasterCategories() async {
isLoadingWorkMasterCategories.value = true;
try {
final response = await ApiService.getMasterWorkCategories();
if (response != null) {
final dataList = response['data'] ?? [];
final parsedList = List<WorkCategoryModel>.from(
dataList.map((e) => WorkCategoryModel.fromJson(e)),
);
workMasterCategories.assignAll(parsedList);
final mapped = {for (var item in parsedList) item.id: item.name};
categoryIdNameMap.assignAll(mapped);
logSafe("Work categories fetched: ${dataList.length}", level: LogLevel.info);
} else {
logSafe("No work categories found or API call failed.", level: LogLevel.warning);
}
} catch (e, st) {
logSafe("Error parsing work categories", level: LogLevel.error, error: e, stackTrace: st);
workMasterCategories.clear();
categoryIdNameMap.clear();
}
isLoadingWorkMasterCategories.value = false;
update();
}
void selectCategory(String id) {
selectedCategoryId.value = id;
selectedCategoryName.value = categoryIdNameMap[id];
logSafe("Category selected", level: LogLevel.debug, );
}
}

Some files were not shown because too many files have changed in this diff Show More