Compare commits
No commits in common. "main" and "Vaibhav_Refactoring-#169" have entirely different histories.
main
...
Vaibhav_Re
@ -3,84 +3,42 @@ plugins {
|
|||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
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 {
|
android {
|
||||||
// Define the namespace for your Android application
|
namespace = "com.example.marco"
|
||||||
namespace = "com.marco.aiot"
|
|
||||||
// Set the compile SDK version based on Flutter's configuration
|
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
// Set the NDK version based on Flutter's configuration
|
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
// Configure Java compatibility options
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = 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 {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8
|
jvmTarget = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration for your application
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// Specify your unique Application ID. This identifies your app on Google Play.
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId = "com.marco.aiot"
|
applicationId = "com.example.marco"
|
||||||
// Set minimum and target SDK versions based on Flutter's configuration
|
// You can update the following values to match your application needs.
|
||||||
minSdk = 23
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
// Set version code and name based on Flutter's configuration (from pubspec.yaml)
|
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
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 {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// Apply the 'release' signing configuration defined above to the release build
|
// TODO: Add your own signing config for the release build.
|
||||||
signingConfig signingConfigs.release
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
// Enable code minification to reduce app size
|
signingConfig = signingConfigs.debug
|
||||||
minifyEnabled true
|
|
||||||
// Enable resource shrinking to remove unused resources
|
|
||||||
shrinkResources true
|
|
||||||
// Other release specific configurations can be added here, e.g., ProGuard rules
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure Flutter specific settings, pointing to the root of your Flutter project
|
|
||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Add required dependencies for desugaring
|
|
||||||
dependencies {
|
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
|
||||||
}
|
|
||||||
@ -6,6 +6,5 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@ -1,14 +1,9 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.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
|
<application
|
||||||
android:label="Marco"
|
android:label="marco"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
@ -38,9 +33,6 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
<meta-data
|
|
||||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
|
||||||
android:value="high_importance_channel"/>
|
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.marco.aiot
|
package com.example.marco
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 11 KiB |
@ -1,3 +1,3 @@
|
|||||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=false
|
android.enableJetifier=true
|
||||||
|
|||||||
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
||||||
|
|||||||
@ -18,9 +18,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.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 "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||||
id("com.google.gms.google-services") version "4.4.2" apply false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
|||||||
95737
assets/data/australia.json
Normal file
192
assets/data/chat.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
82
assets/data/coin_growth.json
Normal 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
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
72
assets/data/drag_n_drop_data.json
Normal 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
102
assets/data/job_recent_application.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
112
assets/data/leads_report_data.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
197
assets/data/product_data.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
102
assets/data/product_order.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
82
assets/data/project_summary.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
47
assets/data/recent_order.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
91
assets/data/task_list.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
62
assets/data/time_line.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
56
assets/data/visitors_by_channels_data.json
Normal 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
BIN
assets/dummy/dummy_1.jpg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
assets/dummy/dummy_2.jpg
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
assets/dummy/dummy_3.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
assets/dummy/dummy_4.jpg
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
assets/dummy/dummy_5.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
assets/dummy/ecommerce/product_1.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/dummy/ecommerce/product_10.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/dummy/ecommerce/product_2.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/dummy/ecommerce/product_3.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/dummy/ecommerce/product_4.jpg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
assets/dummy/ecommerce/product_5.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
assets/dummy/ecommerce/product_6.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/dummy/ecommerce/product_7.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
assets/dummy/ecommerce/product_8.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
assets/dummy/ecommerce/product_9.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/dummy/single_product/single_product_1.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
assets/dummy/single_product/single_product_2.png
Normal file
|
After Width: | Height: | Size: 341 KiB |
BIN
assets/dummy/single_product/single_product_3.png
Normal file
|
After Width: | Height: | Size: 396 KiB |
BIN
assets/dummy/single_product/single_product_4.png
Normal file
|
After Width: | Height: | Size: 316 KiB |
BIN
assets/dummy/single_product/single_product_5.png
Normal file
|
After Width: | Height: | Size: 375 KiB |
BIN
assets/dummy/single_product/single_product_6.png
Normal file
|
After Width: | Height: | Size: 380 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 11 KiB |
@ -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"
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -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:
|
|
||||||
@ -368,7 +368,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -384,7 +384,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@ -401,7 +401,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@ -416,7 +416,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@ -547,7 +547,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@ -569,7 +569,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,17 +4,13 @@ import 'package:marco/controller/my_controller.dart';
|
|||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_validators.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 {
|
class ForgotPasswordController extends MyController {
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
final RxBool isLoading = false.obs;
|
bool showPassword = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
|
||||||
basicValidator.addField(
|
basicValidator.addField(
|
||||||
'email',
|
'email',
|
||||||
required: true,
|
required: true,
|
||||||
@ -22,49 +18,24 @@ class ForgotPasswordController extends MyController {
|
|||||||
validators: [MyEmailValidator()],
|
validators: [MyEmailValidator()],
|
||||||
controller: TextEditingController(text: "demo@example.com"),
|
controller: TextEditingController(text: "demo@example.com"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
super.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onForgotPassword() async {
|
Future<void> onLogin() async {
|
||||||
if (!basicValidator.validateForm()) return;
|
if (basicValidator.validateForm()) {
|
||||||
|
update();
|
||||||
isLoading.value = true;
|
var errors = await AuthService.loginUser(basicValidator.getData());
|
||||||
final data = basicValidator.getData();
|
if (errors != null) {
|
||||||
final email = data['email']?.toString() ?? '';
|
basicValidator.validateForm();
|
||||||
|
basicValidator.clearErrors();
|
||||||
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, );
|
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
Get.toNamed('/auth/reset_password');
|
||||||
logSafe("Error during forgot password", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
update();
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Something went wrong. Please try again later.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void gotoLogIn() {
|
void gotoLogIn() {
|
||||||
Get.offAllNamed('/auth/login-option');
|
Get.toNamed('/auth/login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,115 +4,61 @@ import 'package:marco/controller/my_controller.dart';
|
|||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_validators.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 {
|
class LoginController extends MyController {
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
|
|
||||||
final RxBool isLoading = false.obs;
|
bool showPassword = false, isChecked = false;
|
||||||
final RxBool showPassword = false.obs;
|
RxBool isLoading = false.obs; // Add reactive loading state
|
||||||
final RxBool isChecked = false.obs;
|
|
||||||
final RxBool showSplash = false.obs;
|
final String _dummyEmail = "admin@marcobms.com";
|
||||||
|
final String _dummyPassword = "User@123";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
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();
|
super.onInit();
|
||||||
_initializeForm();
|
|
||||||
_loadSavedCredentials();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeForm() {
|
void onChangeCheckBox(bool? value) {
|
||||||
basicValidator.addField(
|
isChecked = value ?? isChecked;
|
||||||
'username',
|
update();
|
||||||
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 = value ?? false;
|
void onChangeShowPassword() {
|
||||||
|
showPassword = !showPassword;
|
||||||
void onChangeShowPassword() => showPassword.toggle();
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> onLogin() async {
|
Future<void> onLogin() async {
|
||||||
if (!basicValidator.validateForm()) return;
|
if (basicValidator.validateForm()) {
|
||||||
|
// Set loading to true
|
||||||
showSplash.value = true;
|
isLoading.value = true;
|
||||||
|
update();
|
||||||
try {
|
|
||||||
final loginData = basicValidator.getData();
|
|
||||||
logSafe("Attempting login for user: ${loginData['username']}");
|
|
||||||
|
|
||||||
final errors = await AuthService.loginUser(loginData);
|
|
||||||
|
|
||||||
|
var errors = await AuthService.loginUser(basicValidator.getData());
|
||||||
if (errors != null) {
|
if (errors != null) {
|
||||||
showAppSnackbar(
|
|
||||||
title: "Login Failed",
|
|
||||||
message: "Username or password is incorrect",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
basicValidator.addErrors(errors);
|
basicValidator.addErrors(errors);
|
||||||
basicValidator.validateForm();
|
basicValidator.validateForm();
|
||||||
basicValidator.clearErrors();
|
basicValidator.clearErrors();
|
||||||
} else {
|
} else {
|
||||||
await _handleRememberMe();
|
String nextUrl = Uri.parse(ModalRoute.of(Get.context!)?.settings.name ?? "").queryParameters['next'] ?? "/home";
|
||||||
enableRemoteLogging();
|
Get.toNamed(nextUrl);
|
||||||
logSafe("Login successful for user: ${loginData['username']}");
|
|
||||||
Get.offNamed('/select-tenant');
|
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
|
||||||
showAppSnackbar(
|
// Set loading to false after the API call is complete
|
||||||
title: "Login Error",
|
isLoading.value = false;
|
||||||
message: "An unexpected error occurred",
|
update();
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
logSafe("Exception during login",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
} finally {
|
|
||||||
showSplash.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleRememberMe() async {
|
void goToForgotPassword() {
|
||||||
if (isChecked.value) {
|
Get.toNamed('/auth/forgot_password');
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadSavedCredentials() async {
|
void gotoRegister() {
|
||||||
final savedUsername = LocalStorage.getToken('username');
|
Get.offAndToNamed('/auth/register_account');
|
||||||
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 goToForgotPassword() => Get.toNamed('/auth/forgot_password');
|
|
||||||
|
|
||||||
void gotoRegister() => Get.offAndToNamed('/auth/register_account');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,17 +3,16 @@ import 'package:get/get.dart';
|
|||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_validators.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 {
|
class RegisterAccountController extends MyController {
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
|
|
||||||
bool showPassword = false;
|
bool showPassword = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
logSafe("[RegisterAccountController] onInit called");
|
|
||||||
|
|
||||||
basicValidator.addField(
|
basicValidator.addField(
|
||||||
'email',
|
'email',
|
||||||
required: true,
|
required: true,
|
||||||
@ -39,40 +38,29 @@ class RegisterAccountController extends MyController {
|
|||||||
validators: [MyLengthValidator(min: 6, max: 10)],
|
validators: [MyLengthValidator(min: 6, max: 10)],
|
||||||
controller: TextEditingController(),
|
controller: TextEditingController(),
|
||||||
);
|
);
|
||||||
|
|
||||||
super.onInit();
|
super.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onLogin() async {
|
Future<void> onLogin() async {
|
||||||
if (basicValidator.validateForm()) {
|
if (basicValidator.validateForm()) {
|
||||||
update();
|
update();
|
||||||
final data = basicValidator.getData();
|
var errors = await AuthService.loginUser(basicValidator.getData());
|
||||||
logSafe("[RegisterAccountController] Submitting registration data");
|
|
||||||
|
|
||||||
final errors = await AuthService.loginUser(data);
|
|
||||||
if (errors != null) {
|
if (errors != null) {
|
||||||
logSafe("[RegisterAccountController] Login errors: $errors", level: LogLevel.warning);
|
|
||||||
basicValidator.addErrors(errors);
|
basicValidator.addErrors(errors);
|
||||||
basicValidator.validateForm();
|
basicValidator.validateForm();
|
||||||
basicValidator.clearErrors();
|
basicValidator.clearErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
logSafe("[RegisterAccountController] Redirecting to /starter");
|
|
||||||
Get.toNamed('/starter');
|
Get.toNamed('/starter');
|
||||||
update();
|
update();
|
||||||
} else {
|
|
||||||
logSafe("[RegisterAccountController] Validation failed", level: LogLevel.warning);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onChangeShowPassword() {
|
void onChangeShowPassword() {
|
||||||
showPassword = !showPassword;
|
showPassword = !showPassword;
|
||||||
logSafe("[RegisterAccountController] showPassword toggled: $showPassword");
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void gotoLogin() {
|
void gotoLogin() {
|
||||||
logSafe("[RegisterAccountController] Navigating to /auth/login-option");
|
Get.toNamed('/auth/login');
|
||||||
Get.toNamed('/auth/login-option');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,68 +4,56 @@ import 'package:marco/controller/my_controller.dart';
|
|||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
|
||||||
|
|
||||||
class ResetPasswordController extends MyController {
|
class ResetPasswordController extends MyController {
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
bool showPassword = false;
|
bool showPassword = false;
|
||||||
|
|
||||||
bool confirmPassword = false;
|
bool confirmPassword = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
logSafe("[ResetPasswordController] onInit called");
|
|
||||||
|
|
||||||
basicValidator.addField(
|
basicValidator.addField(
|
||||||
'password',
|
'password',
|
||||||
required: true,
|
required: true,
|
||||||
validators: [MyLengthValidator(min: 6, max: 10)],
|
validators: [
|
||||||
|
MyLengthValidator(min: 6, max: 10),
|
||||||
|
],
|
||||||
controller: TextEditingController(),
|
controller: TextEditingController(),
|
||||||
);
|
);
|
||||||
|
|
||||||
basicValidator.addField(
|
basicValidator.addField(
|
||||||
'confirm_password',
|
'confirm_password',
|
||||||
required: true,
|
required: true,
|
||||||
label: "Confirm password",
|
label: "Confirm password",
|
||||||
validators: [MyLengthValidator(min: 6, max: 10)],
|
validators: [
|
||||||
|
MyLengthValidator(min: 6, max: 10),
|
||||||
|
],
|
||||||
controller: TextEditingController(),
|
controller: TextEditingController(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onResetPassword() async {
|
Future<void> onResetPassword() async {
|
||||||
logSafe("[ResetPasswordController] onResetPassword triggered");
|
|
||||||
|
|
||||||
if (basicValidator.validateForm()) {
|
if (basicValidator.validateForm()) {
|
||||||
final data = basicValidator.getData();
|
|
||||||
logSafe("[ResetPasswordController] Reset password form data");
|
|
||||||
|
|
||||||
update();
|
update();
|
||||||
|
var errors = await AuthService.loginUser(basicValidator.getData());
|
||||||
final errors = await AuthService.loginUser(data); // Consider renaming this to resetPassword() for clarity
|
|
||||||
if (errors != null) {
|
if (errors != null) {
|
||||||
logSafe("[ResetPasswordController] Received errors: $errors", level: LogLevel.warning);
|
|
||||||
basicValidator.addErrors(errors);
|
basicValidator.addErrors(errors);
|
||||||
basicValidator.validateForm();
|
basicValidator.validateForm();
|
||||||
basicValidator.clearErrors();
|
basicValidator.clearErrors();
|
||||||
}
|
}
|
||||||
|
Get.toNamed('/home');
|
||||||
logSafe("[ResetPasswordController] Navigating to /dashboard");
|
|
||||||
Get.toNamed('/dashboard');
|
|
||||||
update();
|
update();
|
||||||
} else {
|
|
||||||
logSafe("[ResetPasswordController] Form validation failed", level: LogLevel.warning);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onChangeShowPassword() {
|
void onChangeShowPassword() {
|
||||||
showPassword = !showPassword;
|
showPassword = !showPassword;
|
||||||
logSafe("[ResetPasswordController] showPassword toggled: $showPassword");
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onConfirmPassword() {
|
void onConfirmPassword() {
|
||||||
confirmPassword = !confirmPassword;
|
confirmPassword = !confirmPassword;
|
||||||
logSafe("[ResetPasswordController] confirmPassword toggled: $confirmPassword");
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
52
lib/controller/dashboard/analytics_controller.dart
Normal 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)
|
||||||
|
];
|
||||||
|
}
|
||||||
224
lib/controller/dashboard/attendance_screen_controller.dart
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:geolocator/geolocator.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';
|
||||||
|
|
||||||
|
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; // Added loading flag
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchProjects() async {
|
||||||
|
isLoading.value = true; // Set loading to true before API call
|
||||||
|
final response = await ApiService.getProjects();
|
||||||
|
isLoading.value = false; // Set loading to false after API call completes
|
||||||
|
|
||||||
|
if (response != null && response.isNotEmpty) {
|
||||||
|
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
||||||
|
selectedProjectId = projects.first.id.toString();
|
||||||
|
await fetchProjectData(selectedProjectId);
|
||||||
|
update(['attendance_dashboard_controller']);
|
||||||
|
} else {
|
||||||
|
print("No projects data found or failed to fetch data.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchProjectData(String? projectId) async {
|
||||||
|
if (projectId == null) return;
|
||||||
|
|
||||||
|
isLoading.value = true; // Set loading to true before API call
|
||||||
|
await Future.wait([
|
||||||
|
fetchEmployeesByProject(projectId),
|
||||||
|
fetchAttendanceLogs(projectId,
|
||||||
|
dateFrom: startDateAttendance, dateTo: endDateAttendance),
|
||||||
|
fetchRegularizationLogs(projectId),
|
||||||
|
]);
|
||||||
|
isLoading.value = false; // Set loading to false after data is fetched
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
|
if (projectId == null) return;
|
||||||
|
|
||||||
|
isLoading.value = true; // Set loading to true before API call
|
||||||
|
final response =
|
||||||
|
await ApiService.getEmployeesByProject(projectId);
|
||||||
|
isLoading.value = false; // Set loading to false after API call completes
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
|
||||||
|
update();
|
||||||
|
} else {
|
||||||
|
print("Failed to fetch employees for project $projectId.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> captureAndUploadAttendance(
|
||||||
|
String id, // Change from int to String
|
||||||
|
String employeeId, // Change from int to String
|
||||||
|
String projectId, {
|
||||||
|
String comment = "Marked via mobile app",
|
||||||
|
required int action,
|
||||||
|
bool imageCapture = true, // <- add this flag
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
XFile? image;
|
||||||
|
if (imageCapture) {
|
||||||
|
image = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 80,
|
||||||
|
);
|
||||||
|
if (image == null) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final position = await Geolocator.getCurrentPosition(
|
||||||
|
desiredAccuracy: LocationAccuracy.high,
|
||||||
|
);
|
||||||
|
|
||||||
|
final imageName = imageCapture
|
||||||
|
? ApiService.generateImageName(employeeId, employees.length + 1)
|
||||||
|
: ""; // Empty or null if not capturing image
|
||||||
|
|
||||||
|
return await ApiService.uploadAttendanceImage(
|
||||||
|
id,
|
||||||
|
employeeId,
|
||||||
|
image,
|
||||||
|
position.latitude,
|
||||||
|
position.longitude,
|
||||||
|
imageName: imageName,
|
||||||
|
projectId: projectId,
|
||||||
|
comment: comment,
|
||||||
|
action: action,
|
||||||
|
imageCapture: imageCapture, // <- pass flag down
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print("Error capturing or uploading attendance: $e");
|
||||||
|
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;
|
||||||
|
|
||||||
|
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; // Set loading to true before API call
|
||||||
|
final response = await ApiService.getAttendanceLogs(
|
||||||
|
projectId,
|
||||||
|
dateFrom: dateFrom,
|
||||||
|
dateTo: dateTo,
|
||||||
|
);
|
||||||
|
isLoading.value = false; // Set loading to false after API call completes
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
attendanceLogs =
|
||||||
|
response.map((json) => AttendanceLogModel.fromJson(json)).toList();
|
||||||
|
print("Attendance logs fetched: ${response}");
|
||||||
|
update();
|
||||||
|
} else {
|
||||||
|
print("Failed to fetch attendance logs for project $projectId.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
update();
|
||||||
|
} else {
|
||||||
|
print("Failed to fetch regularization logs for project $projectId.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchLogsView(String? id) async {
|
||||||
|
if (id == null) return;
|
||||||
|
|
||||||
|
isLoading.value = true; // Set loading to true before API call
|
||||||
|
final response = await ApiService.getAttendanceLogView(id);
|
||||||
|
isLoading.value = false; // Set loading to false after API call completes
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
attendenceLogsView = response
|
||||||
|
.map((json) => AttendanceLogViewModel.fromJson(json))
|
||||||
|
.toList();
|
||||||
|
update();
|
||||||
|
} else {
|
||||||
|
print("Failed to fetch regularization logs for project $id.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
lib/controller/dashboard/crm_controller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
43
lib/controller/dashboard/crypto_controller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
69
lib/controller/dashboard/ecommerce_controller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
42
lib/controller/dashboard/job_controller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
46
lib/controller/dashboard/project_controller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/controller/dashboard/sales_controller.dart
Normal 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;
|
||||||
|
}
|
||||||
@ -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()}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
lib/controller/extra_pages/time_line_controller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,112 +1,46 @@
|
|||||||
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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 {
|
class LayoutController extends GetxController {
|
||||||
// Theme Customization
|
|
||||||
ThemeCustomizer themeCustomizer = ThemeCustomizer();
|
ThemeCustomizer themeCustomizer = ThemeCustomizer();
|
||||||
|
|
||||||
// Global Keys
|
|
||||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
||||||
final GlobalKey<State<StatefulWidget>> scrollKey = GlobalKey();
|
final GlobalKey<State<StatefulWidget>> scrollKey = GlobalKey();
|
||||||
|
|
||||||
// Scroll
|
ScrollController scrollController = ScrollController();
|
||||||
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;
|
|
||||||
|
|
||||||
bool isLastIndex = false;
|
bool isLastIndex = false;
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
fetchProjects();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onReady() {
|
void onReady() {
|
||||||
super.onReady();
|
super.onReady();
|
||||||
ThemeCustomizer.addListener(onChangeTheme);
|
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) {
|
void onChangeTheme(ThemeCustomizer oldVal, ThemeCustomizer newVal) {
|
||||||
themeCustomizer = newVal;
|
themeCustomizer = newVal;
|
||||||
update();
|
update();
|
||||||
|
|
||||||
if (newVal.rightBarOpen) {
|
if (newVal.rightBarOpen) {
|
||||||
scaffoldKey.currentState?.openEndDrawer();
|
scaffoldKey.currentState?.openEndDrawer();
|
||||||
logSafe("Theme changed — end drawer opened", level: LogLevel.debug);
|
|
||||||
} else {
|
} else {
|
||||||
scaffoldKey.currentState?.closeEndDrawer();
|
scaffoldKey.currentState?.closeEndDrawer();
|
||||||
logSafe("Theme changed — end drawer closed", level: LogLevel.debug);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Optional notification toggles (placeholder)
|
enableNotificationShade() {
|
||||||
void enableNotificationShade() {
|
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
|
||||||
logSafe("Notification shade enabled (not implemented)", level: LogLevel.verbose);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void disableNotificationShade() {
|
disableNotificationShade() {
|
||||||
logSafe("Notification shade disabled (not implemented)", level: LogLevel.verbose);
|
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
ThemeCustomizer.removeListener(onChangeTheme);
|
||||||
|
scrollController.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
lib/controller/other/basic_table_controller.dart
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/controller/other/google_map_screen_controller.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
480
lib/controller/other/map_controller.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
320
lib/controller/other/syncfusion_chart_controller.dart
Normal 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))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,154 +2,96 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
|
||||||
import 'package:marco/helpers/services/permission_service.dart';
|
import 'package:marco/helpers/services/permission_service.dart';
|
||||||
import 'package:marco/model/user_permission.dart';
|
import 'package:marco/model/user_permission.dart';
|
||||||
import 'package:marco/model/employees/employee_info.dart';
|
import 'package:marco/model/employee_info.dart'; // Import the EmployeeInfo model
|
||||||
import 'package:marco/model/projects_model.dart';
|
|
||||||
|
|
||||||
class PermissionController extends GetxController {
|
class PermissionController extends GetxController {
|
||||||
var permissions = <UserPermission>[].obs;
|
var permissions = <UserPermission>[].obs;
|
||||||
var employeeInfo = Rxn<EmployeeInfo>();
|
var employeeInfo = Rxn<EmployeeInfo>(); // Observable for employee info
|
||||||
var projectsInfo = <ProjectInfo>[].obs;
|
|
||||||
Timer? _refreshTimer;
|
Timer? _refreshTimer;
|
||||||
var isLoading = true.obs;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
@override
|
||||||
super.onInit();
|
void onInit() {
|
||||||
_initialize();
|
super.onInit();
|
||||||
}
|
_loadDataFromAPI(); // Always fetch from API at start
|
||||||
|
_startAutoRefresh(); // Schedule auto-refresh
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _initialize() async {
|
// Save permissions and employee info to SharedPreferences
|
||||||
final token = await _getAuthToken();
|
Future<void> _storeData() async {
|
||||||
if (token?.isNotEmpty ?? false) {
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await loadData(token!);
|
|
||||||
_startAutoRefresh();
|
// Store permissions
|
||||||
} else {
|
final permissionsJson = permissions.map((e) => e.toJson()).toList();
|
||||||
logSafe("Token is null or empty. Skipping API load and auto-refresh.",
|
print("Storing Permissions: $permissionsJson");
|
||||||
level: LogLevel.warning);
|
await prefs.setString('user_permissions', jsonEncode(permissionsJson));
|
||||||
}
|
|
||||||
}
|
// Store employee info
|
||||||
|
if (employeeInfo.value != null) {
|
||||||
Future<String?> _getAuthToken() async {
|
final employeeInfoJson = employeeInfo.value!.toJson();
|
||||||
try {
|
print("Storing Employee Info: $employeeInfoJson");
|
||||||
final prefs = await SharedPreferences.getInstance();
|
await prefs.setString('employee_info', jsonEncode(employeeInfoJson));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public method to load permissions and employee info (usually called from outside)
|
||||||
Future<void> loadData(String token) async {
|
Future<void> loadData(String token) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
final result = await PermissionService.fetchPermissions(token);
|
||||||
final userData = await PermissionService.fetchAllUserData(token);
|
print("Fetched Permissions from API: $result");
|
||||||
_updateState(userData);
|
|
||||||
await _storeData();
|
permissions.assignAll(result); // Update observable list
|
||||||
logSafe("Data loaded and state updated successfully.");
|
await _storeData(); // Cache locally
|
||||||
} catch (e, stacktrace) {
|
|
||||||
logSafe("Error loading data from API",
|
// Also fetch employee info from the API (you can extend the service if needed)
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
await _loadEmployeeInfoFromAPI(token);
|
||||||
} finally {
|
} catch (e) {
|
||||||
isLoading.value = false;
|
print('Error loading data from API: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateState(Map<String, dynamic> userData) {
|
// Internal helper to load token and fetch permissions and employee info from API
|
||||||
try {
|
Future<void> _loadDataFromAPI() async {
|
||||||
permissions.assignAll(userData['permissions']);
|
final token = await _getAuthToken();
|
||||||
employeeInfo.value = userData['employeeInfo'];
|
if (token != null && token.isNotEmpty) {
|
||||||
projectsInfo.assignAll(userData['projects']);
|
await loadData(token);
|
||||||
logSafe("State updated with user data.");
|
} else {
|
||||||
} catch (e, stacktrace) {
|
print("No token available for fetching data.");
|
||||||
logSafe("Error updating state",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _storeData() async {
|
// Retrieve token from SharedPreferences
|
||||||
try {
|
Future<String?> _getAuthToken() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
return prefs.getString('jwt_token'); // Or 'auth_token' if that’s the key you're using
|
||||||
await prefs.setString(
|
|
||||||
'user_permissions',
|
|
||||||
jsonEncode(permissions.map((e) => e.toJson()).toList()),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (employeeInfo.value != null) {
|
|
||||||
await prefs.setString(
|
|
||||||
'employee_info',
|
|
||||||
jsonEncode(employeeInfo.value!.toJson()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projectsInfo.isNotEmpty) {
|
|
||||||
await prefs.setString(
|
|
||||||
'projects_info',
|
|
||||||
jsonEncode(projectsInfo.map((e) => e.toJson()).toList()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logSafe("User data successfully stored in SharedPreferences.");
|
|
||||||
} catch (e, stacktrace) {
|
|
||||||
logSafe("Error storing data",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-refresh every 30 minutes
|
||||||
void _startAutoRefresh() {
|
void _startAutoRefresh() {
|
||||||
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
|
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
|
||||||
logSafe("Auto-refresh triggered.");
|
await _loadDataFromAPI();
|
||||||
final token = await _getAuthToken();
|
|
||||||
if (token?.isNotEmpty ?? false) {
|
|
||||||
await loadData(token!);
|
|
||||||
} else {
|
|
||||||
logSafe("Token missing during auto-refresh. Skipping.",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load employee info from the API
|
||||||
|
Future<void> _loadEmployeeInfoFromAPI(String token) async {
|
||||||
|
final employeeInfoResponse = await PermissionService.fetchEmployeeInfo(token);
|
||||||
|
print("Fetched Employee Info from API: $employeeInfoResponse");
|
||||||
|
|
||||||
|
employeeInfo.value = employeeInfoResponse; // Update observable employee info
|
||||||
|
await _storeData(); // Cache employee info locally
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for specific permission
|
||||||
bool hasPermission(String permissionId) {
|
bool hasPermission(String permissionId) {
|
||||||
final hasPerm = permissions.any((p) => p.id == permissionId);
|
return permissions.any((p) => p.id == permissionId);
|
||||||
logSafe("Checking permission $permissionId: $hasPerm",
|
|
||||||
level: LogLevel.debug);
|
|
||||||
return hasPerm;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isUserAssignedToProject(String projectId) {
|
|
||||||
final assigned = projectsInfo.any((project) => project.id == projectId);
|
|
||||||
logSafe("Checking project assignment for $projectId: $assigned",
|
|
||||||
level: LogLevel.debug);
|
|
||||||
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
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
_refreshTimer?.cancel();
|
_refreshTimer?.cancel();
|
||||||
logSafe("PermissionController disposed and auto-refresh timer cancelled.");
|
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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, );
|
|
||||||
}
|
|
||||||
}
|
|
||||||