Feature_Global_Project_Selection #47
@ -21,7 +21,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.marco"
|
||||
applicationId = "com.example.marcostage"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
|
||||
@ -1,192 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -1,82 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
@ -1,202 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@ -1,72 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@ -1,102 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@ -1,112 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
@ -1,197 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@ -1,102 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@ -1,82 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@ -1,47 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@ -1,91 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@ -1,56 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 341 KiB |
|
Before Width: | Height: | Size: 396 KiB |
|
Before Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 375 KiB |
|
Before Width: | Height: | Size: 380 KiB |
@ -1,52 +0,0 @@
|
||||
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)
|
||||
];
|
||||
}
|
||||
@ -15,6 +15,7 @@ import 'package:marco/model/employee_model.dart';
|
||||
import 'package:marco/model/attendance_log_model.dart';
|
||||
import 'package:marco/model/regularization_log_model.dart';
|
||||
import 'package:marco/model/attendance_log_view_model.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
final Logger log = Logger();
|
||||
|
||||
@ -28,7 +29,6 @@ class AttendanceController extends GetxController {
|
||||
List<AttendanceLogViewModel> attendenceLogsView = [];
|
||||
|
||||
// Selected values
|
||||
String? selectedProjectId;
|
||||
String selectedTab = 'Employee List';
|
||||
|
||||
// Date range for attendance filtering
|
||||
@ -93,12 +93,10 @@ class AttendanceController extends GetxController {
|
||||
|
||||
if (response != null && response.isNotEmpty) {
|
||||
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
||||
selectedProjectId = projects.first.id.toString();
|
||||
log.i("Projects fetched: ${projects.length}");
|
||||
|
||||
await fetchProjectData(selectedProjectId);
|
||||
} else {
|
||||
log.w("No projects found or API call failed.");
|
||||
log.e("Failed to fetch projects or no projects available.");
|
||||
projects = [];
|
||||
}
|
||||
|
||||
isLoadingProjects.value = false;
|
||||
@ -107,6 +105,13 @@ class AttendanceController extends GetxController {
|
||||
update(['attendance_dashboard_controller']);
|
||||
}
|
||||
|
||||
Future<void> loadAttendanceData(String projectId) async {
|
||||
await fetchEmployeesByProject(projectId);
|
||||
await fetchAttendanceLogs(projectId);
|
||||
await fetchRegularizationLogs(projectId);
|
||||
await fetchProjectData(projectId);
|
||||
}
|
||||
|
||||
/// Fetches employees, attendance logs and regularization logs for a project.
|
||||
Future<void> fetchProjectData(String? projectId) async {
|
||||
if (projectId == null) return;
|
||||
@ -176,7 +181,8 @@ class AttendanceController extends GetxController {
|
||||
return false;
|
||||
}
|
||||
|
||||
final compressedBytes = await compressImageToUnder100KB(File(image.path));
|
||||
final compressedBytes =
|
||||
await compressImageToUnder100KB(File(image.path));
|
||||
if (compressedBytes == null) {
|
||||
log.e("Image compression failed.");
|
||||
uploadingStates[employeeId]?.value = false;
|
||||
@ -239,9 +245,9 @@ class AttendanceController extends GetxController {
|
||||
lastDate: todayDateOnly.subtract(const Duration(days: 1)),
|
||||
initialDateRange: DateTimeRange(
|
||||
start: startDateAttendance ?? today.subtract(const Duration(days: 7)),
|
||||
end: endDateAttendance ?? todayDateOnly.subtract(const Duration(days: 1)),
|
||||
end: endDateAttendance ??
|
||||
todayDateOnly.subtract(const Duration(days: 1)),
|
||||
),
|
||||
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
@ -273,7 +279,7 @@ class AttendanceController extends GetxController {
|
||||
log.i("Date range selected: $startDateAttendance to $endDateAttendance");
|
||||
|
||||
await controller.fetchAttendanceLogs(
|
||||
controller.selectedProjectId,
|
||||
Get.find<ProjectController>().selectedProject?.id,
|
||||
dateFrom: picked.start,
|
||||
dateTo: picked.end,
|
||||
);
|
||||
@ -332,7 +338,8 @@ class AttendanceController extends GetxController {
|
||||
return dateB.compareTo(dateA);
|
||||
});
|
||||
|
||||
final sortedMap = Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
|
||||
final sortedMap =
|
||||
Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
|
||||
|
||||
log.i("Logs grouped and sorted by check-in date.");
|
||||
return sortedMap;
|
||||
@ -352,8 +359,9 @@ class AttendanceController extends GetxController {
|
||||
final response = await ApiService.getRegularizationLogs(projectId);
|
||||
|
||||
if (response != null) {
|
||||
regularizationLogs =
|
||||
response.map((json) => RegularizationLogModel.fromJson(json)).toList();
|
||||
regularizationLogs = response
|
||||
.map((json) => RegularizationLogModel.fromJson(json))
|
||||
.toList();
|
||||
log.i("Regularization logs fetched: ${regularizationLogs.length}");
|
||||
update();
|
||||
} else {
|
||||
@ -374,8 +382,9 @@ class AttendanceController extends GetxController {
|
||||
final response = await ApiService.getAttendanceLogView(id);
|
||||
|
||||
if (response != null) {
|
||||
attendenceLogsView =
|
||||
response.map((json) => AttendanceLogViewModel.fromJson(json)).toList();
|
||||
attendenceLogsView = response
|
||||
.map((json) => AttendanceLogViewModel.fromJson(json))
|
||||
.toList();
|
||||
|
||||
attendenceLogsView.sort((a, b) {
|
||||
if (a.activityTime == null || b.activityTime == null) return 0;
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -57,10 +57,8 @@ class DailyTaskController extends GetxController {
|
||||
}
|
||||
|
||||
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
|
||||
selectedProjectId = projects.first.id.toString();
|
||||
log.i("Projects fetched: ${projects.length} projects loaded.");
|
||||
update();
|
||||
await fetchTaskData(selectedProjectId);
|
||||
}
|
||||
|
||||
Future<void> fetchTaskData(String? projectId) async {
|
||||
|
||||
54
lib/controller/dashboard/dashboard_controller.dart
Normal file
@ -0,0 +1,54 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/project_model.dart';
|
||||
|
||||
final Logger log = Logger();
|
||||
|
||||
class DashboardController extends GetxController {
|
||||
RxList<ProjectModel> projects = <ProjectModel>[].obs;
|
||||
RxString? selectedProjectId;
|
||||
var isProjectListExpanded = false.obs;
|
||||
RxBool isProjectSelectionExpanded = true.obs;
|
||||
|
||||
void toggleProjectListExpanded() {
|
||||
isProjectListExpanded.value = !isProjectListExpanded.value;
|
||||
}
|
||||
|
||||
var isProjectDropdownExpanded = false.obs;
|
||||
|
||||
RxBool isLoading = true.obs;
|
||||
RxBool isLoadingProjects = true.obs;
|
||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchProjects();
|
||||
}
|
||||
|
||||
/// Fetches projects and initializes selected project.
|
||||
Future<void> fetchProjects() async {
|
||||
isLoadingProjects.value = true;
|
||||
isLoading.value = true;
|
||||
|
||||
final response = await ApiService.getProjects();
|
||||
|
||||
if (response != null && response.isNotEmpty) {
|
||||
projects.assignAll(
|
||||
response.map((json) => ProjectModel.fromJson(json)).toList());
|
||||
selectedProjectId = RxString(projects.first.id.toString());
|
||||
log.i("Projects fetched: ${projects.length}");
|
||||
} else {
|
||||
log.w("No projects found or API call failed.");
|
||||
}
|
||||
|
||||
isLoadingProjects.value = false;
|
||||
isLoading.value = false;
|
||||
update(['dashboard_controller']);
|
||||
}
|
||||
|
||||
void updateSelectedProject(String projectId) {
|
||||
selectedProjectId?.value = projectId;
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ 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/employees/employee_details_model.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
final Logger log = Logger();
|
||||
|
||||
@ -12,8 +13,9 @@ class EmployeesScreenController extends GetxController {
|
||||
List<AttendanceModel> attendances = [];
|
||||
List<ProjectModel> projects = [];
|
||||
String? selectedProjectId;
|
||||
List<EmployeeModel> employees = [];
|
||||
List<EmployeeDetailsModel> employeeDetails = [];
|
||||
RxBool isAllEmployeeSelected = false.obs;
|
||||
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
|
||||
|
||||
RxBool isLoading = false.obs;
|
||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||
@ -24,7 +26,17 @@ class EmployeesScreenController extends GetxController {
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchAllProjects();
|
||||
fetchAllEmployees();
|
||||
|
||||
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 {
|
||||
@ -41,18 +53,27 @@ class EmployeesScreenController extends GetxController {
|
||||
update();
|
||||
}
|
||||
|
||||
void clearEmployees() {
|
||||
employees.clear(); // Correct way to clear RxList
|
||||
log.i("Employees cleared");
|
||||
update(['employee_screen_controller']);
|
||||
}
|
||||
|
||||
Future<void> fetchAllEmployees() async {
|
||||
isLoading.value = true;
|
||||
await _handleApiCall(
|
||||
ApiService.getAllEmployees,
|
||||
onSuccess: (data) {
|
||||
employees = data.map((json) => EmployeeModel.fromJson(json)).toList();
|
||||
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||
log.i("All Employees fetched: ${employees.length} employees loaded.");
|
||||
},
|
||||
onEmpty: () => log.w("No Employee data found or API call failed."),
|
||||
onEmpty: () {
|
||||
employees.clear(); // Always clear on empty
|
||||
log.w("No Employee data found or API call failed.");
|
||||
},
|
||||
);
|
||||
isLoading.value = false;
|
||||
update();
|
||||
update(['employee_screen_controller']);
|
||||
}
|
||||
|
||||
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||
@ -65,22 +86,21 @@ class EmployeesScreenController extends GetxController {
|
||||
await _handleApiCall(
|
||||
() => ApiService.getAllEmployeesByProject(projectId),
|
||||
onSuccess: (data) {
|
||||
employees = data.map((json) => EmployeeModel.fromJson(json)).toList();
|
||||
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||
for (var emp in employees) {
|
||||
uploadingStates[emp.id] = false.obs;
|
||||
}
|
||||
log.i("Employees fetched: ${employees.length} for project $projectId");
|
||||
update();
|
||||
},
|
||||
onEmpty: () {
|
||||
employees.clear();
|
||||
log.w("No employees found for project $projectId.");
|
||||
employees = [];
|
||||
update();
|
||||
},
|
||||
onError: (e) =>
|
||||
log.e("Error fetching employees for project $projectId: $e"),
|
||||
);
|
||||
isLoading.value = false;
|
||||
update(['employee_screen_controller']);
|
||||
}
|
||||
|
||||
Future<void> _handleApiCall(
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
||||
import 'package:marco/model/chart_model.dart';
|
||||
import 'package:marco/model/job_recent_application_model.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class 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();
|
||||
}
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
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,23 +1,87 @@
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:logger/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';
|
||||
|
||||
|
||||
final Logger log = Logger();
|
||||
|
||||
class LayoutController extends GetxController {
|
||||
// Theme Customization
|
||||
ThemeCustomizer themeCustomizer = ThemeCustomizer();
|
||||
|
||||
// Global Keys
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
||||
final GlobalKey<State<StatefulWidget>> scrollKey = GlobalKey();
|
||||
|
||||
ScrollController scrollController = ScrollController();
|
||||
// Scroll
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
// Reactive State
|
||||
final RxBool isLoading = true.obs;
|
||||
final RxBool isLoadingProjects = true.obs;
|
||||
final RxBool isProjectSelectionExpanded = true.obs;
|
||||
final RxBool isProjectListExpanded = false.obs;
|
||||
final RxBool isProjectDropdownExpanded = false.obs;
|
||||
final RxList<ProjectModel> projects = <ProjectModel>[].obs;
|
||||
final RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||
|
||||
// Selected Project
|
||||
RxString? selectedProjectId;
|
||||
|
||||
bool isLastIndex = false;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchProjects();
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
ThemeCustomizer.addListener(onChangeTheme);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ThemeCustomizer.removeListener(onChangeTheme);
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Project Handling
|
||||
Future<void> fetchProjects() async {
|
||||
isLoading.value = true;
|
||||
isLoadingProjects.value = true;
|
||||
|
||||
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());
|
||||
log.i("Projects fetched: ${fetchedProjects.length}");
|
||||
} else {
|
||||
log.w("No projects found or API call failed.");
|
||||
}
|
||||
|
||||
isLoadingProjects.value = false;
|
||||
isLoading.value = false;
|
||||
update(['dashboard_controller']);
|
||||
}
|
||||
|
||||
void updateSelectedProject(String projectId) {
|
||||
selectedProjectId?.value = projectId;
|
||||
}
|
||||
|
||||
void toggleProjectListExpanded() {
|
||||
isProjectListExpanded.toggle();
|
||||
}
|
||||
|
||||
// Theme Updates
|
||||
void onChangeTheme(ThemeCustomizer oldVal, ThemeCustomizer newVal) {
|
||||
themeCustomizer = newVal;
|
||||
update();
|
||||
@ -29,18 +93,12 @@ class LayoutController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
enableNotificationShade() {
|
||||
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
|
||||
// Notification Shade (placeholders)
|
||||
void enableNotificationShade() {
|
||||
// Add implementation if needed
|
||||
}
|
||||
|
||||
disableNotificationShade() {
|
||||
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
ThemeCustomizer.removeListener(onChangeTheme);
|
||||
scrollController.dispose();
|
||||
void disableNotificationShade() {
|
||||
// Add implementation if needed
|
||||
}
|
||||
}
|
||||
|
||||
82
lib/controller/project_controller.dart
Normal file
@ -0,0 +1,82 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/project_model.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
|
||||
final Logger log = Logger();
|
||||
|
||||
class ProjectController extends GetxController {
|
||||
RxList<ProjectModel> projects = <ProjectModel>[].obs;
|
||||
RxString? selectedProjectId;
|
||||
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;
|
||||
ProjectModel? get selectedProject {
|
||||
if (selectedProjectId == null || selectedProjectId!.value.isEmpty)
|
||||
return null;
|
||||
return projects.firstWhereOrNull((p) => p.id == selectedProjectId!.value);
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchProjects();
|
||||
}
|
||||
|
||||
void clearProjects() {
|
||||
projects.clear();
|
||||
selectedProjectId = null;
|
||||
isProjectSelectionExpanded.value = false;
|
||||
isProjectListExpanded.value = false;
|
||||
isProjectDropdownExpanded.value = false;
|
||||
isLoadingProjects.value = false;
|
||||
isLoading.value = false;
|
||||
uploadingStates.clear();
|
||||
LocalStorage.saveString('selectedProjectId', '');
|
||||
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) => ProjectModel.fromJson(json)).toList());
|
||||
|
||||
String? savedId = LocalStorage.getString('selectedProjectId');
|
||||
if (savedId != null && projects.any((p) => p.id == savedId)) {
|
||||
selectedProjectId = RxString(savedId);
|
||||
} else {
|
||||
selectedProjectId = RxString(projects.first.id.toString());
|
||||
LocalStorage.saveString(
|
||||
'selectedProjectId', projects.first.id.toString());
|
||||
}
|
||||
|
||||
isProjectSelectionExpanded.value = false;
|
||||
log.i("Projects fetched: ${projects.length}");
|
||||
} else {
|
||||
log.w("No projects found or API call failed.");
|
||||
}
|
||||
|
||||
isLoadingProjects.value = false;
|
||||
isLoading.value = false;
|
||||
update(['dashboard_controller']);
|
||||
}
|
||||
|
||||
Future<void> updateSelectedProject(String projectId) async {
|
||||
selectedProjectId?.value = projectId;
|
||||
await LocalStorage.saveString('selectedProjectId', projectId);
|
||||
update([
|
||||
'selected_project'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,6 @@ final Logger log = Logger();
|
||||
|
||||
class DailyTaskPlaningController extends GetxController {
|
||||
List<ProjectModel> projects = [];
|
||||
String? selectedProjectId;
|
||||
List<EmployeeModel> employees = [];
|
||||
List<TaskPlanningDetailsModel> dailyTasks = [];
|
||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||
@ -121,11 +120,8 @@ class DailyTaskPlaningController extends GetxController {
|
||||
}
|
||||
|
||||
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
|
||||
selectedProjectId = projects.first.id.toString();
|
||||
log.i("Projects fetched: ${projects.length} projects loaded.");
|
||||
update();
|
||||
|
||||
await fetchTaskData(selectedProjectId);
|
||||
} catch (e, stack) {
|
||||
log.e("Error fetching projects", error: e, stackTrace: stack);
|
||||
} finally {
|
||||
|
||||
@ -9,6 +9,8 @@ import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||
|
||||
final Logger logger = Logger();
|
||||
|
||||
@ -90,12 +92,13 @@ class ReportTaskController extends MyController {
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future<void> reportTask({
|
||||
Future<bool> reportTask({
|
||||
required String projectId,
|
||||
required String comment,
|
||||
required int completedTask,
|
||||
required List<Map<String, dynamic>> checklist,
|
||||
required DateTime reportedDate,
|
||||
List<File>? images,
|
||||
}) async {
|
||||
logger.i("Starting task report...");
|
||||
|
||||
@ -107,7 +110,7 @@ class ReportTaskController extends MyController {
|
||||
message: "Completed work is required.",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
final completedWorkInt = int.tryParse(completedWork);
|
||||
@ -117,7 +120,7 @@ class ReportTaskController extends MyController {
|
||||
message: "Completed work must be a positive integer.",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
final commentField = commentController.text.trim();
|
||||
@ -127,48 +130,100 @@ class ReportTaskController extends MyController {
|
||||
message: "Comment is required.",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
reportStatus.value = ApiStatus.loading;
|
||||
isLoading.value = true;
|
||||
|
||||
List<Map<String, dynamic>>? imageData;
|
||||
if (images != null && images.isNotEmpty) {
|
||||
final imageFutures = images.map((file) async {
|
||||
final compressedBytes = await compressImageToUnder100KB(file);
|
||||
if (compressedBytes == null) return null;
|
||||
|
||||
final base64Image = base64Encode(compressedBytes);
|
||||
final fileName = file.path.split('/').last;
|
||||
final contentType = _getContentTypeFromFileName(fileName);
|
||||
|
||||
return {
|
||||
"fileName": fileName,
|
||||
"base64Data": base64Image,
|
||||
"contentType": contentType,
|
||||
"fileSize": compressedBytes.lengthInBytes,
|
||||
"description": "Image uploaded for task report",
|
||||
};
|
||||
}).toList();
|
||||
|
||||
final results = await Future.wait(imageFutures);
|
||||
imageData = results.whereType<Map<String, dynamic>>().toList();
|
||||
}
|
||||
|
||||
final success = await ApiService.reportTask(
|
||||
id: projectId,
|
||||
comment: commentField,
|
||||
completedTask: completedTask,
|
||||
completedTask: completedWorkInt,
|
||||
checkList: checklist,
|
||||
images: imageData,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
reportStatus.value = ApiStatus.success;
|
||||
showAppSnackbar(
|
||||
title: "Success",
|
||||
message: "Task reported successfully!",
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
await taskController.fetchTaskData(projectId);
|
||||
return true;
|
||||
} else {
|
||||
reportStatus.value = ApiStatus.failure;
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to report task.",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("Error reporting task: $e");
|
||||
reportStatus.value = ApiStatus.failure;
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "An error occurred while reporting the task.",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
reportStatus.value = ApiStatus.idle;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _getContentTypeFromFileName(String fileName) {
|
||||
final ext = fileName.split('.').last.toLowerCase();
|
||||
switch (ext) {
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
return 'image/jpeg';
|
||||
case 'png':
|
||||
return 'image/png';
|
||||
case 'webp':
|
||||
return 'image/webp';
|
||||
case 'gif':
|
||||
return 'image/gif';
|
||||
default:
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> commentTask({
|
||||
required String projectId,
|
||||
required String comment,
|
||||
List<File>? images,
|
||||
}) async {
|
||||
logger.i("Starting task comment...");
|
||||
|
||||
@ -184,11 +239,38 @@ class ReportTaskController extends MyController {
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
List<Map<String, dynamic>>? imageData;
|
||||
|
||||
if (images != null && images.isNotEmpty) {
|
||||
final imageFutures = images.map((file) async {
|
||||
final compressedBytes = await compressImageToUnder100KB(file);
|
||||
if (compressedBytes == null) return null;
|
||||
|
||||
final base64Image = base64Encode(compressedBytes);
|
||||
final fileName = file.path.split('/').last;
|
||||
final contentType = _getContentTypeFromFileName(fileName);
|
||||
|
||||
return {
|
||||
"fileName": fileName,
|
||||
"base64Data": base64Image,
|
||||
"contentType": contentType,
|
||||
"fileSize": compressedBytes.lengthInBytes,
|
||||
"description": "Image uploaded for task comment",
|
||||
};
|
||||
}).toList();
|
||||
|
||||
final results = await Future.wait(imageFutures);
|
||||
imageData = results.whereType<Map<String, dynamic>>().toList();
|
||||
}
|
||||
|
||||
final success = await ApiService.commentTask(
|
||||
id: projectId,
|
||||
comment: commentField,
|
||||
);
|
||||
images: imageData,
|
||||
).timeout(const Duration(seconds: 30), onTimeout: () {
|
||||
logger.e("Request timed out.");
|
||||
throw Exception("Request timed out.");
|
||||
});
|
||||
|
||||
if (success) {
|
||||
showAppSnackbar(
|
||||
@ -196,7 +278,6 @@ class ReportTaskController extends MyController {
|
||||
message: "Task commented successfully!",
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
|
||||
await taskController.fetchTaskData(projectId);
|
||||
} else {
|
||||
showAppSnackbar(
|
||||
|
||||
@ -4,6 +4,7 @@ class ApiEndpoints {
|
||||
|
||||
// Attendance Screen API Endpoints
|
||||
static const String getProjects = "/project/list";
|
||||
static const String getGlobalProjects = "/project/list/basic";
|
||||
static const String getEmployeesByProject = "/attendance/project/team";
|
||||
static const String getAttendanceLogs = "/attendance/project/log";
|
||||
static const String getAttendanceLogView = "/attendance/log/attendance";
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
|
||||
final Logger logger = Logger();
|
||||
|
||||
@ -14,13 +14,11 @@ class ApiService {
|
||||
static const Duration timeout = Duration(seconds: 10);
|
||||
static const bool enableLogs = true;
|
||||
|
||||
// ===== Helpers =====
|
||||
// === Helpers ===
|
||||
|
||||
static Future<String?> _getToken() async {
|
||||
final token = await LocalStorage.getJwtToken();
|
||||
if (token == null && enableLogs) {
|
||||
logger.w("No JWT token found. Please log in.");
|
||||
}
|
||||
if (token == null && enableLogs) logger.w("No JWT token found.");
|
||||
return token;
|
||||
}
|
||||
|
||||
@ -47,13 +45,12 @@ class ApiService {
|
||||
return null;
|
||||
}
|
||||
|
||||
static dynamic _parseResponseForAllData(http.Response response,
|
||||
{String label = ''}) {
|
||||
static dynamic _parseResponseForAllData(http.Response response, {String label = ''}) {
|
||||
_log("$label Response: ${response.body}");
|
||||
try {
|
||||
final json = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && json['success'] == true) {
|
||||
return json; // 👈 Return full response, not just json['data']
|
||||
return json;
|
||||
}
|
||||
_log("API Error [$label]: ${json['message'] ?? 'Unknown error'}");
|
||||
} catch (e) {
|
||||
@ -62,28 +59,23 @@ class ApiService {
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<http.Response?> _getRequest(String endpoint,
|
||||
{Map<String, String>? queryParams, bool hasRetried = false}) async {
|
||||
static Future<http.Response?> _getRequest(
|
||||
String endpoint, {
|
||||
Map<String, String>? queryParams,
|
||||
bool hasRetried = false,
|
||||
}) async {
|
||||
String? token = await _getToken();
|
||||
if (token == null) return null;
|
||||
|
||||
Uri uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
|
||||
.replace(queryParameters: queryParams);
|
||||
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint").replace(queryParameters: queryParams);
|
||||
_log("GET $uri");
|
||||
|
||||
try {
|
||||
http.Response response =
|
||||
await http.get(uri, headers: _headers(token)).timeout(timeout);
|
||||
|
||||
final response = await http.get(uri, headers: _headers(token)).timeout(timeout);
|
||||
if (response.statusCode == 401 && !hasRetried) {
|
||||
_log("Unauthorized. Attempting token refresh...");
|
||||
bool refreshed = await AuthService.refreshToken();
|
||||
if (refreshed) {
|
||||
token = await _getToken();
|
||||
if (token != null) {
|
||||
return await _getRequest(endpoint,
|
||||
queryParams: queryParams, hasRetried: true);
|
||||
}
|
||||
if (await AuthService.refreshToken()) {
|
||||
return await _getRequest(endpoint, queryParams: queryParams, hasRetried: true);
|
||||
}
|
||||
_log("Token refresh failed.");
|
||||
}
|
||||
@ -95,78 +87,68 @@ class ApiService {
|
||||
}
|
||||
|
||||
static Future<http.Response?> _postRequest(
|
||||
String endpoint, dynamic body) async {
|
||||
String endpoint,
|
||||
dynamic body, {
|
||||
Duration customTimeout = timeout,
|
||||
bool hasRetried = false,
|
||||
}) async {
|
||||
String? token = await _getToken();
|
||||
if (token == null) return null;
|
||||
|
||||
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
|
||||
|
||||
_log("POST $uri");
|
||||
_log("Headers: ${_headers(token)}");
|
||||
_log("Body: $body");
|
||||
_log("POST $uri\nHeaders: ${_headers(token)}\nBody: $body");
|
||||
|
||||
try {
|
||||
final response = await http
|
||||
.post(uri, headers: _headers(token), body: jsonEncode(body))
|
||||
.timeout(timeout);
|
||||
.timeout(customTimeout);
|
||||
|
||||
_log("Response Status: ${response.statusCode}");
|
||||
if (response.statusCode == 401 && !hasRetried) {
|
||||
_log("Unauthorized POST. Attempting token refresh...");
|
||||
if (await AuthService.refreshToken()) {
|
||||
return await _postRequest(endpoint, body, customTimeout: customTimeout, hasRetried: true);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
_log("HTTP POST Exception: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// ===== Attendence Screen API Calls =====
|
||||
|
||||
static Future<List<dynamic>?> getProjects() async {
|
||||
final response = await _getRequest(ApiEndpoints.getProjects);
|
||||
return response != null
|
||||
? _parseResponse(response, label: 'Projects')
|
||||
: null;
|
||||
}
|
||||
// === Attendance APIs ===
|
||||
|
||||
static Future<List<dynamic>?> getEmployeesByProject(String projectId) async {
|
||||
final response = await _getRequest(ApiEndpoints.getEmployeesByProject,
|
||||
queryParams: {"projectId": projectId});
|
||||
return response != null
|
||||
? _parseResponse(response, label: 'Employees')
|
||||
: null;
|
||||
}
|
||||
static Future<List<dynamic>?> getProjects() async =>
|
||||
_getRequest(ApiEndpoints.getProjects).then((res) => res != null ? _parseResponse(res, label: 'Projects') : null);
|
||||
|
||||
static Future<List<dynamic>?> getAttendanceLogs(String projectId,
|
||||
{DateTime? dateFrom, DateTime? dateTo}) async {
|
||||
static Future<List<dynamic>?> getGlobalProjects() async =>
|
||||
_getRequest(ApiEndpoints.getProjects).then((res) => res != null ? _parseResponse(res, label: 'Global Projects') : null);
|
||||
|
||||
static Future<List<dynamic>?> getEmployeesByProject(String projectId) async =>
|
||||
_getRequest(ApiEndpoints.getEmployeesByProject, queryParams: {"projectId": projectId})
|
||||
.then((res) => res != null ? _parseResponse(res, label: 'Employees') : null);
|
||||
|
||||
static Future<List<dynamic>?> getAttendanceLogs(
|
||||
String projectId, {
|
||||
DateTime? dateFrom,
|
||||
DateTime? dateTo,
|
||||
}) async {
|
||||
final query = {
|
||||
"projectId": projectId,
|
||||
if (dateFrom != null)
|
||||
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
|
||||
if (dateFrom != null) "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
|
||||
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
|
||||
};
|
||||
|
||||
final response =
|
||||
await _getRequest(ApiEndpoints.getAttendanceLogs, queryParams: query);
|
||||
return response != null
|
||||
? _parseResponse(response, label: 'Attendance Logs')
|
||||
: null;
|
||||
return _getRequest(ApiEndpoints.getAttendanceLogs, queryParams: query)
|
||||
.then((res) => res != null ? _parseResponse(res, label: 'Attendance Logs') : null);
|
||||
}
|
||||
|
||||
static Future<List<dynamic>?> getAttendanceLogView(String id) async {
|
||||
final response =
|
||||
await _getRequest("${ApiEndpoints.getAttendanceLogView}/$id");
|
||||
return response != null
|
||||
? _parseResponse(response, label: 'Log Details')
|
||||
: null;
|
||||
}
|
||||
static Future<List<dynamic>?> getAttendanceLogView(String id) async =>
|
||||
_getRequest("${ApiEndpoints.getAttendanceLogView}/$id")
|
||||
.then((res) => res != null ? _parseResponse(res, label: 'Log Details') : null);
|
||||
|
||||
static Future<List<dynamic>?> getRegularizationLogs(String projectId) async {
|
||||
final response = await _getRequest(ApiEndpoints.getRegularizationLogs,
|
||||
queryParams: {"projectId": projectId});
|
||||
return response != null
|
||||
? _parseResponse(response, label: 'Regularization Logs')
|
||||
: null;
|
||||
}
|
||||
|
||||
// ===== Upload Attendance Image =====
|
||||
static Future<List<dynamic>?> getRegularizationLogs(String projectId) async =>
|
||||
_getRequest(ApiEndpoints.getRegularizationLogs, queryParams: {"projectId": projectId})
|
||||
.then((res) => res != null ? _parseResponse(res, label: 'Regularization Logs') : null);
|
||||
|
||||
static Future<bool> uploadAttendanceImage(
|
||||
String id,
|
||||
@ -179,7 +161,7 @@ class ApiService {
|
||||
String comment = "",
|
||||
required int action,
|
||||
bool imageCapture = true,
|
||||
String? markTime, // <-- Optional markTime parameter
|
||||
String? markTime,
|
||||
}) async {
|
||||
final now = DateTime.now();
|
||||
final body = {
|
||||
@ -197,16 +179,14 @@ class ApiService {
|
||||
if (imageCapture && imageFile != null) {
|
||||
try {
|
||||
final bytes = await imageFile.readAsBytes();
|
||||
final base64Image = base64Encode(bytes);
|
||||
final fileSize = await imageFile.length();
|
||||
final contentType = "image/${imageFile.path.split('.').last}";
|
||||
|
||||
body["image"] = {
|
||||
"fileName": imageName,
|
||||
"contentType": contentType,
|
||||
"fileSize": fileSize,
|
||||
"description": "Employee attendance photo",
|
||||
"base64Data": base64Image,
|
||||
"base64Data": base64Encode(bytes),
|
||||
};
|
||||
} catch (e) {
|
||||
_log("Image encoding error: $e");
|
||||
@ -214,22 +194,16 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
final response =
|
||||
await _postRequest(ApiEndpoints.uploadAttendanceImage, body);
|
||||
final response = await _postRequest(ApiEndpoints.uploadAttendanceImage, body);
|
||||
if (response == null) return false;
|
||||
|
||||
final json = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && json['success'] == true) {
|
||||
return true;
|
||||
} else {
|
||||
_log("Failed to upload image: ${json['message'] ?? 'Unknown error'}");
|
||||
}
|
||||
if (response.statusCode == 200 && json['success'] == true) return true;
|
||||
|
||||
_log("Failed to upload image: ${json['message'] ?? 'Unknown error'}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===== Utilities =====
|
||||
|
||||
static String generateImageName(String employeeId, int count) {
|
||||
final now = DateTime.now();
|
||||
final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now);
|
||||
@ -237,35 +211,19 @@ class ApiService {
|
||||
return "${employeeId}_${dateStr}_$imageNumber.jpg";
|
||||
}
|
||||
|
||||
// ===== Employee Screen API Calls =====
|
||||
static Future<List<dynamic>?> getAllEmployeesByProject(
|
||||
String projectId) async {
|
||||
if (projectId.isEmpty) {
|
||||
throw ArgumentError('projectId must not be empty');
|
||||
}
|
||||
// === Employee APIs ===
|
||||
|
||||
final String endpoint =
|
||||
"${ApiEndpoints.getAllEmployeesByProject}/$projectId";
|
||||
final response = await _getRequest(endpoint);
|
||||
|
||||
return response != null
|
||||
? _parseResponse(response, label: 'Employees by Project')
|
||||
: null;
|
||||
static Future<List<dynamic>?> getAllEmployeesByProject(String projectId) async {
|
||||
if (projectId.isEmpty) throw ArgumentError('projectId must not be empty');
|
||||
final endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId";
|
||||
return _getRequest(endpoint).then((res) => res != null ? _parseResponse(res, label: 'Employees by Project') : null);
|
||||
}
|
||||
|
||||
static Future<List<dynamic>?> getAllEmployees() async {
|
||||
final response = await _getRequest(ApiEndpoints.getAllEmployees);
|
||||
return response != null
|
||||
? _parseResponse(response, label: 'All Employees')
|
||||
: null;
|
||||
}
|
||||
static Future<List<dynamic>?> getAllEmployees() async =>
|
||||
_getRequest(ApiEndpoints.getAllEmployees).then((res) => res != null ? _parseResponse(res, label: 'All Employees') : null);
|
||||
|
||||
static Future<List<dynamic>?> getRoles() async {
|
||||
final response = await _getRequest(ApiEndpoints.getRoles);
|
||||
return response != null
|
||||
? _parseResponse(response, label: 'All Employees')
|
||||
: null;
|
||||
}
|
||||
static Future<List<dynamic>?> getRoles() async =>
|
||||
_getRequest(ApiEndpoints.getRoles).then((res) => res != null ? _parseResponse(res, label: 'Roles') : null);
|
||||
|
||||
static Future<bool> createEmployee({
|
||||
required String firstName,
|
||||
@ -281,61 +239,33 @@ class ApiService {
|
||||
"gender": gender,
|
||||
"jobRoleId": jobRoleId,
|
||||
};
|
||||
|
||||
// Make the API request
|
||||
final response = await _postRequest(ApiEndpoints.createEmployee, body);
|
||||
|
||||
if (response == null) {
|
||||
_log("Error: No response from server.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response == null) return false;
|
||||
final json = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
if (json['success'] == true) {
|
||||
return true;
|
||||
} else {
|
||||
_log(
|
||||
"Failed to create employee: ${json['message'] ?? 'Unknown error'}");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
_log(
|
||||
"Failed to create employee. Status code: ${response.statusCode}, Response: ${json['message'] ?? 'No message'}");
|
||||
return false;
|
||||
}
|
||||
return response.statusCode == 200 && json['success'] == true;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>?> getEmployeeDetails(
|
||||
String employeeId) async {
|
||||
static Future<Map<String, dynamic>?> getEmployeeDetails(String employeeId) async {
|
||||
final url = "${ApiEndpoints.getEmployeeInfo}/$employeeId";
|
||||
|
||||
final response = await _getRequest(url);
|
||||
final data = response != null
|
||||
? _parseResponse(response, label: 'Employee Details')
|
||||
: null;
|
||||
if (data is Map<String, dynamic>) {
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
final data = response != null ? _parseResponse(response, label: 'Employee Details') : null;
|
||||
return data is Map<String, dynamic> ? data : null;
|
||||
}
|
||||
|
||||
// ===== Daily Tasks API Calls =====
|
||||
static Future<List<dynamic>?> getDailyTasks(String projectId,
|
||||
{DateTime? dateFrom, DateTime? dateTo}) async {
|
||||
// === Daily Task APIs ===
|
||||
|
||||
static Future<List<dynamic>?> getDailyTasks(
|
||||
String projectId, {
|
||||
DateTime? dateFrom,
|
||||
DateTime? dateTo,
|
||||
}) async {
|
||||
final query = {
|
||||
"projectId": projectId,
|
||||
if (dateFrom != null)
|
||||
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
|
||||
if (dateFrom != null) "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
|
||||
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
|
||||
};
|
||||
|
||||
final response =
|
||||
await _getRequest(ApiEndpoints.getDailyTask, queryParams: query);
|
||||
return response != null
|
||||
? _parseResponse(response, label: 'Daily Tasks')
|
||||
: null;
|
||||
return _getRequest(ApiEndpoints.getDailyTask, queryParams: query)
|
||||
.then((res) => res != null ? _parseResponse(res, label: 'Daily Tasks') : null);
|
||||
}
|
||||
|
||||
static Future<bool> reportTask({
|
||||
@ -343,6 +273,7 @@ class ApiService {
|
||||
required int completedTask,
|
||||
required String comment,
|
||||
required List<Map<String, dynamic>> checkList,
|
||||
List<Map<String, dynamic>>? images,
|
||||
}) async {
|
||||
final body = {
|
||||
"id": id,
|
||||
@ -350,63 +281,43 @@ class ApiService {
|
||||
"comment": comment,
|
||||
"reportedDate": DateTime.now().toUtc().toIso8601String(),
|
||||
"checkList": checkList,
|
||||
if (images != null && images.isNotEmpty) "images": images,
|
||||
};
|
||||
|
||||
final response = await _postRequest(ApiEndpoints.reportTask, body);
|
||||
|
||||
if (response == null) {
|
||||
_log("Error: No response from server.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response == null) return false;
|
||||
final json = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 && json['success'] == true) {
|
||||
Get.back();
|
||||
return true;
|
||||
} else {
|
||||
_log("Failed to report task: ${json['message'] ?? 'Unknown error'}");
|
||||
return false;
|
||||
}
|
||||
_log("Failed to report task: ${json['message'] ?? 'Unknown error'}");
|
||||
return false;
|
||||
}
|
||||
|
||||
static Future<bool> commentTask({
|
||||
required String id,
|
||||
required String comment,
|
||||
List<Map<String, dynamic>>? images,
|
||||
}) async {
|
||||
final body = {
|
||||
"taskAllocationId": id,
|
||||
"comment": comment,
|
||||
"commentDate": DateTime.now().toUtc().toIso8601String(),
|
||||
if (images != null && images.isNotEmpty) "images": images,
|
||||
};
|
||||
|
||||
final response = await _postRequest(ApiEndpoints.commentTask, body);
|
||||
|
||||
if (response == null) {
|
||||
_log("Error: No response from server.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response == null) return false;
|
||||
final json = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 && json['success'] == true) {
|
||||
return true;
|
||||
} else {
|
||||
_log("Failed to comment task: ${json['message'] ?? 'Unknown error'}");
|
||||
return false;
|
||||
}
|
||||
return response.statusCode == 200 && json['success'] == true;
|
||||
}
|
||||
|
||||
// Daily Task Planing //
|
||||
|
||||
static Future<Map<String, dynamic>?> getDailyTasksDetails(
|
||||
String projectId) async {
|
||||
static Future<Map<String, dynamic>?> getDailyTasksDetails(String projectId) async {
|
||||
final url = "${ApiEndpoints.dailyTaskDetails}/$projectId";
|
||||
|
||||
final response = await _getRequest(url);
|
||||
return response != null
|
||||
? _parseResponseForAllData(response, label: 'Daily Task Details')
|
||||
as Map<String, dynamic>?
|
||||
? _parseResponseForAllData(response, label: 'Daily Task Details') as Map<String, dynamic>?
|
||||
: null;
|
||||
}
|
||||
|
||||
@ -422,26 +333,16 @@ class ApiService {
|
||||
"plannedTask": plannedTask,
|
||||
"description": description,
|
||||
"taskTeam": taskTeam,
|
||||
"assignmentDate":
|
||||
(assignmentDate ?? DateTime.now()).toUtc().toIso8601String(),
|
||||
"assignmentDate": (assignmentDate ?? DateTime.now()).toUtc().toIso8601String(),
|
||||
};
|
||||
|
||||
final response = await _postRequest(ApiEndpoints.assignDailyTask, body);
|
||||
|
||||
if (response == null) {
|
||||
_log("Error: No response from server.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response == null) return false;
|
||||
final json = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 && json['success'] == true) {
|
||||
Get.back();
|
||||
return true;
|
||||
} else {
|
||||
_log(
|
||||
"Failed to assign daily task: ${json['message'] ?? 'Unknown error'}");
|
||||
return false;
|
||||
}
|
||||
_log("Failed to assign daily task: ${json['message'] ?? 'Unknown error'}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
28
lib/helpers/services/app_initializer.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:url_strategy/url_strategy.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
final Logger logger = Logger();
|
||||
|
||||
Future<void> initializeApp() async {
|
||||
setPathUrlStrategy();
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||
statusBarColor: Color.fromARGB(255, 255, 0, 0),
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
));
|
||||
|
||||
await LocalStorage.init();
|
||||
await ThemeCustomizer.init();
|
||||
Get.put(PermissionController());
|
||||
Get.put(ProjectController(), permanent: true);
|
||||
AppStyle.init();
|
||||
|
||||
logger.i("App initialization completed successfully.");
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
|
||||
final Logger logger = Logger();
|
||||
|
||||
@ -13,9 +14,11 @@ class AuthService {
|
||||
static const Map<String, String> _headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
static bool isLoggedIn = false;
|
||||
static Future<Map<String, String>?> loginUser(
|
||||
Map<String, dynamic> data) async {
|
||||
|
||||
/// Login with email and password
|
||||
static Future<Map<String, String>?> loginUser(Map<String, dynamic> data) async {
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse("$_baseUrl/auth/login-mobile"),
|
||||
@ -30,9 +33,7 @@ class AuthService {
|
||||
} else if (response.statusCode == 401) {
|
||||
return {"password": "Invalid email or password"};
|
||||
} else {
|
||||
return {
|
||||
"error": responseData['message'] ?? "Unexpected error occurred"
|
||||
};
|
||||
return {"error": responseData['message'] ?? "Unexpected error occurred"};
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("Login error: $e");
|
||||
@ -40,16 +41,13 @@ class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Refreshes the JWT token using the refresh token.
|
||||
/// Refresh JWT token
|
||||
static Future<bool> refreshToken() async {
|
||||
final accessToken = await LocalStorage.getJwtToken();
|
||||
final refreshToken = await LocalStorage.getRefreshToken();
|
||||
|
||||
if (accessToken == null ||
|
||||
refreshToken == null ||
|
||||
accessToken.isEmpty ||
|
||||
refreshToken.isEmpty) {
|
||||
logger.w("Missing token or refresh token for refresh.");
|
||||
if (accessToken == null || refreshToken == null || accessToken.isEmpty || refreshToken.isEmpty) {
|
||||
logger.w("Missing access/refresh token.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -58,82 +56,50 @@ class AuthService {
|
||||
"refreshToken": refreshToken,
|
||||
};
|
||||
|
||||
logger.i("Sending refresh token request with body: $requestBody");
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse("$_baseUrl/auth/refresh-token"),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(requestBody),
|
||||
);
|
||||
|
||||
logger.i(
|
||||
"Refresh token API response (${response.statusCode}): ${response.body}");
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && data['success'] == true) {
|
||||
final newAccessToken = data['data']['token'];
|
||||
final newRefreshToken = data['data']['refreshToken'];
|
||||
|
||||
if (newAccessToken == null || newRefreshToken == null) {
|
||||
logger.w("Invalid tokens received during refresh.");
|
||||
return false;
|
||||
}
|
||||
|
||||
await LocalStorage.setJwtToken(newAccessToken);
|
||||
await LocalStorage.setRefreshToken(newRefreshToken);
|
||||
await LocalStorage.setLoggedInUser(true);
|
||||
|
||||
logger.i("Token refreshed successfully.");
|
||||
return true;
|
||||
} else {
|
||||
logger.w("Refresh failed: ${data['message']}");
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("Exception during token refresh: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Forgot password API
|
||||
static Future<Map<String, String>?> forgotPassword(String email) async {
|
||||
final requestBody = {"email": email};
|
||||
|
||||
logger.i("Sending forgot password request with email: $email");
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse("$_baseUrl/auth/forgot-password"),
|
||||
headers: _headers,
|
||||
body: jsonEncode(requestBody),
|
||||
);
|
||||
|
||||
logger.i(
|
||||
"Forgot password API response (${response.statusCode}): ${response.body}");
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 && responseData['success'] == true) {
|
||||
logger.i("Forgot password request successful.");
|
||||
return null;
|
||||
final data = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && data['success'] == true) {
|
||||
await LocalStorage.setJwtToken(data['data']['token']);
|
||||
await LocalStorage.setRefreshToken(data['data']['refreshToken']);
|
||||
await LocalStorage.setLoggedInUser(true);
|
||||
logger.i("Token refreshed.");
|
||||
return true;
|
||||
} else {
|
||||
return {
|
||||
"error":
|
||||
responseData['message'] ?? "Failed to send password reset link."
|
||||
};
|
||||
logger.w("Refresh token failed: ${data['message']}");
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("Exception during forgot password request: $e");
|
||||
logger.e("Token refresh error: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Forgot password
|
||||
static Future<Map<String, String>?> forgotPassword(String email) async {
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse("$_baseUrl/auth/forgot-password"),
|
||||
headers: _headers,
|
||||
body: jsonEncode({"email": email}),
|
||||
);
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
||||
return {"error": data['message'] ?? "Failed to send reset link."};
|
||||
} catch (e) {
|
||||
logger.e("Forgot password error: $e");
|
||||
return {"error": "Network error. Please check your connection."};
|
||||
}
|
||||
}
|
||||
|
||||
// Request demo API
|
||||
static Future<Map<String, String>?> requestDemo(
|
||||
Map<String, dynamic> demoData) async {
|
||||
/// Request demo
|
||||
static Future<Map<String, String>?> requestDemo(Map<String, dynamic> demoData) async {
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse("$_baseUrl/market/inquiry"),
|
||||
@ -141,22 +107,16 @@ class AuthService {
|
||||
body: jsonEncode(demoData),
|
||||
);
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 && responseData['success'] == true) {
|
||||
logger.i("Request Demo submitted successfully.");
|
||||
return null;
|
||||
} else {
|
||||
return {
|
||||
"error": responseData['message'] ?? "Failed to submit demo request."
|
||||
};
|
||||
}
|
||||
final data = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
||||
return {"error": data['message'] ?? "Failed to submit demo request."};
|
||||
} catch (e) {
|
||||
logger.e("Exception during request demo: $e");
|
||||
logger.e("Request demo error: $e");
|
||||
return {"error": "Network error. Please check your connection."};
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list of industries
|
||||
static Future<List<Map<String, dynamic>>?> getIndustries() async {
|
||||
try {
|
||||
final response = await http.get(
|
||||
@ -164,201 +124,129 @@ class AuthService {
|
||||
headers: _headers,
|
||||
);
|
||||
|
||||
logger.i(
|
||||
"Get Industries API response (${response.statusCode}): ${response.body}");
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 && responseData['success'] == true) {
|
||||
// Return the list of industries as List<Map<String, dynamic>>
|
||||
final List<dynamic> industriesData = responseData['data'];
|
||||
return industriesData.cast<Map<String, dynamic>>();
|
||||
} else {
|
||||
logger.w("Failed to fetch industries: ${responseData['message']}");
|
||||
return null;
|
||||
final data = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && data['success'] == true) {
|
||||
return List<Map<String, dynamic>>.from(data['data']);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
logger.e("Exception during getIndustries: $e");
|
||||
logger.e("Get industries error: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a new MPIN for the user.
|
||||
/// Generate MPIN
|
||||
static Future<Map<String, String>?> generateMpin({
|
||||
required String employeeId,
|
||||
required String mpin,
|
||||
}) async {
|
||||
final jwtToken = await LocalStorage.getJwtToken();
|
||||
|
||||
final requestBody = {
|
||||
"employeeId": employeeId,
|
||||
"mpin": mpin,
|
||||
};
|
||||
|
||||
logger.i("Sending MPIN generation request: $requestBody");
|
||||
final token = await LocalStorage.getJwtToken();
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse("$_baseUrl/auth/generate-mpin"),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
if (jwtToken != null && jwtToken.isNotEmpty)
|
||||
'Authorization': 'Bearer $jwtToken',
|
||||
..._headers,
|
||||
if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token',
|
||||
},
|
||||
body: jsonEncode(requestBody),
|
||||
body: jsonEncode({"employeeId": employeeId, "mpin": mpin}),
|
||||
);
|
||||
|
||||
logger.i(
|
||||
"Generate MPIN API response (${response.statusCode}): ${response.body}");
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 && responseData['success'] == true) {
|
||||
logger.i("MPIN generated successfully.");
|
||||
return null;
|
||||
} else {
|
||||
return {"error": responseData['message'] ?? "Failed to generate MPIN."};
|
||||
}
|
||||
final data = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
||||
return {"error": data['message'] ?? "Failed to generate MPIN."};
|
||||
} catch (e) {
|
||||
logger.e("Exception during generate MPIN: $e");
|
||||
logger.e("Generate MPIN error: $e");
|
||||
return {"error": "Network error. Please check your connection."};
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify MPIN
|
||||
static Future<Map<String, String>?> verifyMpin({
|
||||
required String mpin,
|
||||
required String mpinToken,
|
||||
}) async {
|
||||
// Get employee info from local storage
|
||||
final employeeInfo = LocalStorage.getEmployeeInfo();
|
||||
if (employeeInfo == null) return {"error": "Employee info not found."};
|
||||
|
||||
if (employeeInfo == null) {
|
||||
logger.w("Employee info not found in local storage.");
|
||||
return {"error": "Employee info not found. Please login again."};
|
||||
}
|
||||
|
||||
final employeeId = employeeInfo.id;
|
||||
|
||||
final jwtToken = await LocalStorage.getJwtToken();
|
||||
|
||||
final requestBody = {
|
||||
"employeeId": employeeId,
|
||||
"mpin": mpin,
|
||||
"mpinToken": mpinToken,
|
||||
};
|
||||
|
||||
logger.i("Sending MPIN verification request: $requestBody");
|
||||
final token = await LocalStorage.getJwtToken();
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse("$_baseUrl/auth/login-mpin"),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
if (jwtToken != null && jwtToken.isNotEmpty)
|
||||
'Authorization': 'Bearer $jwtToken',
|
||||
..._headers,
|
||||
if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token',
|
||||
},
|
||||
body: jsonEncode(requestBody),
|
||||
body: jsonEncode({
|
||||
"employeeId": employeeInfo.id,
|
||||
"mpin": mpin,
|
||||
"mpinToken": mpinToken,
|
||||
}),
|
||||
);
|
||||
|
||||
logger.i(
|
||||
"Verify MPIN API response (${response.statusCode}): ${response.body}");
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 && responseData['success'] == true) {
|
||||
logger.i("MPIN verified successfully.");
|
||||
return null;
|
||||
} else {
|
||||
return {"error": responseData['message'] ?? "Failed to verify MPIN."};
|
||||
}
|
||||
final data = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
||||
return {"error": data['message'] ?? "MPIN verification failed."};
|
||||
} catch (e) {
|
||||
logger.e("Exception during verify MPIN: $e");
|
||||
logger.e("Verify MPIN error: $e");
|
||||
return {"error": "Network error. Please check your connection."};
|
||||
}
|
||||
}
|
||||
|
||||
// Generate OTP API
|
||||
/// Generate OTP
|
||||
static Future<Map<String, String>?> generateOtp(String email) async {
|
||||
final requestBody = {"email": email};
|
||||
|
||||
logger.i("Sending generate OTP request: $requestBody");
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse("$_baseUrl/auth/send-otp"),
|
||||
headers: _headers,
|
||||
body: jsonEncode(requestBody),
|
||||
body: jsonEncode({"email": email}),
|
||||
);
|
||||
|
||||
logger.i(
|
||||
"Generate OTP API response (${response.statusCode}): ${response.body}");
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 && responseData['success'] == true) {
|
||||
logger.i("OTP generated successfully.");
|
||||
return null;
|
||||
} else {
|
||||
return {"error": responseData['message'] ?? "Failed to generate OTP."};
|
||||
}
|
||||
final data = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
||||
return {"error": data['message'] ?? "Failed to generate OTP."};
|
||||
} catch (e) {
|
||||
logger.e("Exception during generate OTP: $e");
|
||||
logger.e("Generate OTP error: $e");
|
||||
return {"error": "Network error. Please check your connection."};
|
||||
}
|
||||
}
|
||||
|
||||
// Verify OTP API
|
||||
/// Verify OTP and login
|
||||
static Future<Map<String, String>?> verifyOtp({
|
||||
required String email,
|
||||
required String otp,
|
||||
}) async {
|
||||
final requestBody = {
|
||||
"email": email,
|
||||
"otp": otp,
|
||||
};
|
||||
|
||||
logger.i("Sending verify OTP request: $requestBody");
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse("$_baseUrl/auth/login-otp"),
|
||||
headers: _headers,
|
||||
body: jsonEncode(requestBody),
|
||||
body: jsonEncode({"email": email, "otp": otp}),
|
||||
);
|
||||
|
||||
logger.i(
|
||||
"Verify OTP API response (${response.statusCode}): ${response.body}");
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 && responseData['data'] != null) {
|
||||
await _handleLoginSuccess(responseData['data']);
|
||||
logger.i("OTP verified and login state initialized successfully.");
|
||||
final data = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && data['data'] != null) {
|
||||
await _handleLoginSuccess(data['data']);
|
||||
return null;
|
||||
} else {
|
||||
return {"error": responseData['message'] ?? "Failed to verify OTP."};
|
||||
}
|
||||
return {"error": data['message'] ?? "OTP verification failed."};
|
||||
} catch (e) {
|
||||
logger.e("Exception during verify OTP: $e");
|
||||
logger.e("Verify OTP error: $e");
|
||||
return {"error": "Network error. Please check your connection."};
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle login success flow
|
||||
static Future<void> _handleLoginSuccess(Map<String, dynamic> data) async {
|
||||
final jwtToken = data['token'];
|
||||
final refreshToken = data['refreshToken'];
|
||||
final mpinToken = data['mpinToken'];
|
||||
|
||||
logger.i("JWT Token: $jwtToken");
|
||||
if (refreshToken != null) logger.i("Refresh Token: $refreshToken");
|
||||
if (mpinToken != null) logger.i("MPIN Token: $mpinToken");
|
||||
|
||||
await LocalStorage.setJwtToken(jwtToken);
|
||||
await LocalStorage.setLoggedInUser(true);
|
||||
|
||||
if (refreshToken != null) {
|
||||
await LocalStorage.setRefreshToken(refreshToken);
|
||||
}
|
||||
if (refreshToken != null) await LocalStorage.setRefreshToken(refreshToken);
|
||||
|
||||
if (mpinToken != null && mpinToken.isNotEmpty) {
|
||||
await LocalStorage.setMpinToken(mpinToken);
|
||||
await LocalStorage.setIsMpin(true);
|
||||
@ -367,7 +255,11 @@ class AuthService {
|
||||
await LocalStorage.removeMpinToken();
|
||||
}
|
||||
|
||||
Get.put(PermissionController());
|
||||
final permissionController = Get.put(PermissionController());
|
||||
await permissionController.loadData(jwtToken);
|
||||
await Get.find<ProjectController>().fetchProjects();
|
||||
|
||||
isLoggedIn = true;
|
||||
logger.i("Login success initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,31 +1,35 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'package:marco/model/user_permission.dart';
|
||||
import 'package:marco/model/employee_info.dart';
|
||||
import 'package:marco/model/projects_model.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||
|
||||
final Logger logger = Logger();
|
||||
|
||||
class PermissionService {
|
||||
static final Map<String, Map<String, dynamic>> _userDataCache = {};
|
||||
|
||||
static Future<Map<String, dynamic>> fetchAllUserData(String token,
|
||||
{bool hasRetried = false}) async {
|
||||
// Return from cache if available
|
||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
||||
/// Fetches all user-related data (permissions, employee info, projects)
|
||||
static Future<Map<String, dynamic>> fetchAllUserData(
|
||||
String token, {
|
||||
bool hasRetried = false,
|
||||
}) async {
|
||||
// Return cached data if already available
|
||||
if (_userDataCache.containsKey(token)) {
|
||||
return _userDataCache[token]!;
|
||||
}
|
||||
|
||||
final uri = Uri.parse("$_baseUrl/user/profile");
|
||||
final headers = {'Authorization': 'Bearer $token'};
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('https://stageapi.marcoaiot.com/api/user/profile'),
|
||||
// Uri.parse('https://api.marcoaiot.com/api/user/profile'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
final response = await http.get(uri, headers: headers);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body)['data'];
|
||||
@ -40,11 +44,12 @@ class PermissionService {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Handle 401 by attempting a single retry with refreshed token
|
||||
if (response.statusCode == 401 && !hasRetried) {
|
||||
final refreshed = await AuthService.refreshToken();
|
||||
if (refreshed) {
|
||||
final newToken = await LocalStorage.getJwtToken();
|
||||
if (newToken != null) {
|
||||
if (newToken != null && newToken.isNotEmpty) {
|
||||
return fetchAllUserData(newToken, hasRetried: true);
|
||||
}
|
||||
}
|
||||
@ -53,15 +58,15 @@ class PermissionService {
|
||||
throw Exception('Unauthorized. Token refresh failed.');
|
||||
}
|
||||
|
||||
final errorMessage =
|
||||
json.decode(response.body)['message'] ?? 'Unknown error';
|
||||
throw Exception('Failed to load data: $errorMessage');
|
||||
final error = json.decode(response.body)['message'] ?? 'Unknown error';
|
||||
throw Exception('Failed to fetch user data: $error');
|
||||
} catch (e) {
|
||||
logger.e('Error fetching user data: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears auth data and redirects to login
|
||||
static Future<void> _handleUnauthorized() async {
|
||||
await LocalStorage.removeToken('jwt_token');
|
||||
await LocalStorage.removeToken('refresh_token');
|
||||
@ -69,15 +74,20 @@ class PermissionService {
|
||||
Get.offAllNamed('/auth/login-option');
|
||||
}
|
||||
|
||||
static List<UserPermission> _parsePermissions(
|
||||
List<dynamic> featurePermissions) =>
|
||||
featurePermissions
|
||||
.map((id) => UserPermission.fromJson({'id': id}))
|
||||
.toList();
|
||||
/// Converts raw permission data into list of `UserPermission`
|
||||
static List<UserPermission> _parsePermissions(List<dynamic> permissions) {
|
||||
return permissions
|
||||
.map((id) => UserPermission.fromJson({'id': id}))
|
||||
.toList();
|
||||
}
|
||||
|
||||
static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic> employeeData) =>
|
||||
EmployeeInfo.fromJson(employeeData);
|
||||
/// Converts raw employee JSON into `EmployeeInfo`
|
||||
static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic> data) {
|
||||
return EmployeeInfo.fromJson(data);
|
||||
}
|
||||
|
||||
static List<ProjectInfo> _parseProjectsInfo(List<dynamic> projects) =>
|
||||
projects.map((proj) => ProjectInfo.fromJson(proj)).toList();
|
||||
/// Converts raw projects JSON into list of `ProjectInfo`
|
||||
static List<ProjectInfo> _parseProjectsInfo(List<dynamic> projects) {
|
||||
return projects.map((proj) => ProjectInfo.fromJson(proj)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,9 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:marco/model/user_permission.dart';
|
||||
import 'package:marco/model/employee_info.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:get/route_manager.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
|
||||
class LocalStorage {
|
||||
static const String _loggedInUserKey = "user";
|
||||
@ -134,20 +136,25 @@ class LocalStorage {
|
||||
return setToken(_refreshTokenKey, refreshToken);
|
||||
}
|
||||
|
||||
static Future<void> logout() async {
|
||||
await removeLoggedInUser();
|
||||
await removeToken(_jwtTokenKey);
|
||||
await removeToken(_refreshTokenKey);
|
||||
await removeUserPermissions();
|
||||
await removeEmployeeInfo();
|
||||
await removeMpinToken();
|
||||
await removeIsMpin();
|
||||
await preferences.remove("mpin_verified");
|
||||
await preferences.remove(_languageKey);
|
||||
await preferences.remove(_themeCustomizerKey);
|
||||
Get.offAllNamed('/auth/login-option');
|
||||
static Future<void> logout() async {
|
||||
await removeLoggedInUser();
|
||||
await removeToken(_jwtTokenKey);
|
||||
await removeToken(_refreshTokenKey);
|
||||
await removeUserPermissions();
|
||||
await removeEmployeeInfo();
|
||||
await removeMpinToken();
|
||||
await removeIsMpin();
|
||||
await preferences.remove("mpin_verified");
|
||||
await preferences.remove(_languageKey);
|
||||
await preferences.remove(_themeCustomizerKey);
|
||||
await preferences.remove('selectedProjectId');
|
||||
if (Get.isRegistered<ProjectController>()) {
|
||||
Get.find<ProjectController>().clearProjects();
|
||||
}
|
||||
|
||||
Get.offAllNamed('/auth/login-option');
|
||||
}
|
||||
|
||||
static Future<bool> setMpinToken(String token) {
|
||||
return preferences.setString(_mpinTokenKey, token);
|
||||
}
|
||||
@ -180,4 +187,13 @@ class LocalStorage {
|
||||
static bool? getBool(String key) {
|
||||
return preferences.getBool(key);
|
||||
}
|
||||
// Save and retrieve String values
|
||||
static String? getString(String key) {
|
||||
return preferences.getString(key);
|
||||
}
|
||||
|
||||
static Future<bool> saveString(String key, String value) async {
|
||||
return preferences.setString(key, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
122
lib/helpers/widgets/image_viewer_dialog.dart
Normal file
@ -0,0 +1,122 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImageViewerDialog extends StatefulWidget {
|
||||
final List<dynamic> imageSources;
|
||||
final int initialIndex;
|
||||
|
||||
const ImageViewerDialog({
|
||||
Key? key,
|
||||
required this.imageSources,
|
||||
required this.initialIndex,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ImageViewerDialog> createState() => _ImageViewerDialogState();
|
||||
}
|
||||
|
||||
class _ImageViewerDialogState extends State<ImageViewerDialog> {
|
||||
late final PageController _controller;
|
||||
late int currentIndex;
|
||||
|
||||
bool isFile(dynamic item) => item is File;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentIndex = widget.initialIndex;
|
||||
_controller = PageController(initialPage: widget.initialIndex);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double dialogHeight = MediaQuery.of(context).size.height * 0.55;
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 100),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
height: dialogHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Top Close Button
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close, size: 26),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
splashRadius: 22,
|
||||
tooltip: 'Close',
|
||||
),
|
||||
),
|
||||
|
||||
// Image Viewer
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: _controller,
|
||||
itemCount: widget.imageSources.length,
|
||||
onPageChanged: (index) {
|
||||
setState(() => currentIndex = index);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final item = widget.imageSources[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: isFile(item)
|
||||
? Image.file(item, fit: BoxFit.contain)
|
||||
: Image.network(
|
||||
item,
|
||||
fit: BoxFit.contain,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
(loadingProgress.expectedTotalBytes ?? 1)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
const Center(
|
||||
child: Icon(Icons.broken_image,
|
||||
size: 48, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Index Indicator
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 12),
|
||||
child: Text(
|
||||
'${currentIndex + 1} / ${widget.imageSources.length}',
|
||||
style: const TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
114
lib/main.dart
@ -1,109 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/extensions/app_localization_delegate.dart';
|
||||
import 'package:marco/helpers/services/localizations/language.dart';
|
||||
import 'package:marco/helpers/services/navigation_services.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/theme/app_notifier.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/routes.dart';
|
||||
import 'package:marco/helpers/services/app_initializer.dart';
|
||||
import 'package:marco/view/my_app.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_strategy/url_strategy.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:marco/helpers/theme/app_notifier.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
final Logger logger = Logger();
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
setPathUrlStrategy();
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
statusBarColor: const Color.fromARGB(255, 255, 0, 0),
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
));
|
||||
|
||||
try {
|
||||
await LocalStorage.init();
|
||||
await ThemeCustomizer.init();
|
||||
AppStyle.init();
|
||||
logger.i("App initialization completed successfully.");
|
||||
await initializeApp();
|
||||
runApp(
|
||||
ChangeNotifierProvider<AppNotifier>(
|
||||
create: (_) => AppNotifier(),
|
||||
child: const MyApp(),
|
||||
),
|
||||
);
|
||||
} catch (e, stacktrace) {
|
||||
logger.e('Error during app initialization:',
|
||||
error: e, stackTrace: stacktrace);
|
||||
return;
|
||||
}
|
||||
|
||||
runApp(ChangeNotifierProvider<AppNotifier>(
|
||||
create: (context) => AppNotifier(),
|
||||
child: const MyApp(),
|
||||
));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
Future<String> _getInitialRoute() async {
|
||||
if (!AuthService.isLoggedIn) {
|
||||
logger.i("User not logged in. Routing to /auth/login-option");
|
||||
return "/auth/login-option";
|
||||
}
|
||||
logger.i("User is logged in.");
|
||||
|
||||
final bool hasMpin = LocalStorage.getIsMpin();
|
||||
logger.i("MPIN enabled: $hasMpin");
|
||||
|
||||
if (hasMpin) {
|
||||
await LocalStorage.setBool("mpin_verified", false);
|
||||
logger.i("Routing to /auth/mpin-auth and setting mpin_verified to false");
|
||||
return "/auth/mpin-auth";
|
||||
} else {
|
||||
logger.i("MPIN not enabled. Routing to /home");
|
||||
return "/home";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<AppNotifier>(
|
||||
builder: (_, notifier, __) {
|
||||
return FutureBuilder<String>(
|
||||
future: _getInitialRoute(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const MaterialApp(
|
||||
home: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return GetMaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ThemeCustomizer.instance.theme,
|
||||
navigatorKey: NavigationService.navigatorKey,
|
||||
initialRoute: snapshot.data!,
|
||||
getPages: getPageRoute(),
|
||||
builder: (context, child) {
|
||||
NavigationService.registerContext(context);
|
||||
return Directionality(
|
||||
textDirection: AppTheme.textDirection,
|
||||
child: child ?? Container(),
|
||||
);
|
||||
},
|
||||
localizationsDelegates: [
|
||||
AppLocalizationsDelegate(context),
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: Language.getLocales(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.e('App failed to initialize:', error: e, stackTrace: stacktrace);
|
||||
runApp(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(child: Text("Failed to initialize the app.")),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||
import 'package:marco/helpers/utils/attendance_actions.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
class AttendanceActionButton extends StatefulWidget {
|
||||
final dynamic employee;
|
||||
@ -19,10 +20,11 @@ class AttendanceActionButton extends StatefulWidget {
|
||||
State<AttendanceActionButton> createState() => _AttendanceActionButtonState();
|
||||
}
|
||||
|
||||
Future<String?> _showCommentBottomSheet(BuildContext context, String actionText) async {
|
||||
Future<String?> _showCommentBottomSheet(
|
||||
BuildContext context, String actionText) async {
|
||||
final TextEditingController commentController = TextEditingController();
|
||||
String? errorText;
|
||||
|
||||
Get.find<ProjectController>().selectedProject?.id;
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@ -80,7 +82,7 @@ Future<String?> _showCommentBottomSheet(BuildContext context, String actionText)
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
final comment = commentController.text.trim();
|
||||
if (comment.isEmpty) {
|
||||
@ -105,7 +107,6 @@ Future<String?> _showCommentBottomSheet(BuildContext context, String actionText)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
String capitalizeFirstLetter(String text) {
|
||||
if (text.isEmpty) return text;
|
||||
return text[0].toUpperCase() + text.substring(1);
|
||||
@ -163,7 +164,10 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
void _handleButtonPressed(BuildContext context) async {
|
||||
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = true;
|
||||
|
||||
if (widget.attendanceController.selectedProjectId == null) {
|
||||
final projectController = Get.find<ProjectController>();
|
||||
final selectedProjectId = projectController.selectedProject?.id;
|
||||
|
||||
if (selectedProjectId == null) {
|
||||
showAppSnackbar(
|
||||
title: "Project Required",
|
||||
message: "Please select a project first",
|
||||
@ -231,7 +235,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
success = await widget.attendanceController.captureAndUploadAttendance(
|
||||
widget.employee.id,
|
||||
widget.employee.employeeId,
|
||||
widget.attendanceController.selectedProjectId!,
|
||||
selectedProjectId,
|
||||
comment: userComment,
|
||||
action: updatedAction,
|
||||
imageCapture: imageCapture,
|
||||
@ -242,7 +246,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
success = await widget.attendanceController.captureAndUploadAttendance(
|
||||
widget.employee.id,
|
||||
widget.employee.employeeId,
|
||||
widget.attendanceController.selectedProjectId!,
|
||||
selectedProjectId,
|
||||
comment: userComment,
|
||||
action: updatedAction,
|
||||
imageCapture: imageCapture,
|
||||
@ -260,14 +264,11 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
|
||||
|
||||
if (success) {
|
||||
widget.attendanceController.fetchEmployeesByProject(
|
||||
widget.attendanceController.selectedProjectId!);
|
||||
widget.attendanceController
|
||||
.fetchAttendanceLogs(widget.attendanceController.selectedProjectId!);
|
||||
await widget.attendanceController.fetchRegularizationLogs(
|
||||
widget.attendanceController.selectedProjectId!);
|
||||
widget.attendanceController.fetchEmployeesByProject(selectedProjectId);
|
||||
widget.attendanceController.fetchAttendanceLogs(selectedProjectId);
|
||||
await widget.attendanceController
|
||||
.fetchProjectData(widget.attendanceController.selectedProjectId!);
|
||||
.fetchRegularizationLogs(selectedProjectId);
|
||||
await widget.attendanceController.fetchProjectData(selectedProjectId);
|
||||
widget.attendanceController.update();
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,14 +24,11 @@ class AttendanceFilterBottomSheet extends StatefulWidget {
|
||||
|
||||
class _AttendanceFilterBottomSheetState
|
||||
extends State<AttendanceFilterBottomSheet> {
|
||||
late String? tempSelectedProjectId;
|
||||
late String tempSelectedTab;
|
||||
bool showProjectList = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
tempSelectedProjectId = widget.controller.selectedProjectId;
|
||||
tempSelectedTab = widget.selectedTab;
|
||||
}
|
||||
|
||||
@ -46,55 +43,7 @@ class _AttendanceFilterBottomSheetState
|
||||
return "Date Range";
|
||||
}
|
||||
|
||||
List<Widget> buildProjectList() {
|
||||
final accessibleProjects = widget.controller.projects
|
||||
.where((project) =>
|
||||
widget.permissionController.isUserAssignedToProject(
|
||||
project.id.toString()))
|
||||
.toList();
|
||||
|
||||
if (accessibleProjects.isEmpty) {
|
||||
return [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Center(child: Text('No Projects Assigned')),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return accessibleProjects.map((project) {
|
||||
final isSelected = tempSelectedProjectId == project.id.toString();
|
||||
return ListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
title: Text(project.name),
|
||||
trailing: isSelected ? const Icon(Icons.check) : null,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
tempSelectedProjectId = project.id.toString();
|
||||
showProjectList = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<Widget> buildMainFilters() {
|
||||
final accessibleProjects = widget.controller.projects
|
||||
.where((project) =>
|
||||
widget.permissionController.isUserAssignedToProject(
|
||||
project.id.toString()))
|
||||
.toList();
|
||||
|
||||
final selectedProject = accessibleProjects.isNotEmpty
|
||||
? accessibleProjects.firstWhere(
|
||||
(p) => p.id.toString() == tempSelectedProjectId,
|
||||
orElse: () => accessibleProjects[0],
|
||||
)
|
||||
: null;
|
||||
|
||||
final selectedProjectName = selectedProject?.name ?? "Select Project";
|
||||
|
||||
final hasRegularizationPermission = widget.permissionController
|
||||
.hasPermission(Permissions.regularizeAttendance);
|
||||
|
||||
@ -112,24 +61,6 @@ class _AttendanceFilterBottomSheetState
|
||||
}).toList();
|
||||
|
||||
List<Widget> widgets = [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: MyText.titleSmall(
|
||||
"Project",
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
title: Text(selectedProjectName),
|
||||
trailing: const Icon(Icons.arrow_drop_down),
|
||||
onTap: () => setState(() => showProjectList = true),
|
||||
),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Align(
|
||||
@ -216,7 +147,6 @@ class _AttendanceFilterBottomSheetState
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Drag handle
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 8),
|
||||
child: Center(
|
||||
@ -230,7 +160,7 @@ class _AttendanceFilterBottomSheetState
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showProjectList) ...buildProjectList() else ...buildMainFilters(),
|
||||
...buildMainFilters(),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
@ -247,7 +177,6 @@ class _AttendanceFilterBottomSheetState
|
||||
child: const Text('Apply Filter'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, {
|
||||
'projectId': tempSelectedProjectId,
|
||||
'selectedTab': tempSelectedTab,
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:marco/helpers/utils/attendance_actions.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:get/get.dart';
|
||||
enum ButtonActions { approve, reject }
|
||||
|
||||
class RegularizeActionButton extends StatefulWidget {
|
||||
@ -51,60 +53,57 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
|
||||
Colors.grey;
|
||||
}
|
||||
|
||||
Future<void> _handlePress() async {
|
||||
if (widget.attendanceController.selectedProjectId == null) {
|
||||
showAppSnackbar(
|
||||
title: 'Warning',
|
||||
message: 'Please select a project first',
|
||||
type: SnackbarType.warning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
isUploading = true;
|
||||
});
|
||||
|
||||
widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value =
|
||||
true;
|
||||
|
||||
final success =
|
||||
await widget.attendanceController.captureAndUploadAttendance(
|
||||
widget.log.id,
|
||||
widget.log.employeeId,
|
||||
widget.attendanceController.selectedProjectId!,
|
||||
comment: _buttonComments[widget.action]!,
|
||||
action: _buttonActionCodes[widget.action]!,
|
||||
imageCapture: false,
|
||||
);
|
||||
Future<void> _handlePress() async {
|
||||
final projectController = Get.find<ProjectController>();
|
||||
final selectedProjectId = projectController.selectedProject?.id;
|
||||
|
||||
if (selectedProjectId == null) {
|
||||
showAppSnackbar(
|
||||
title: success ? 'Success' : 'Error',
|
||||
message: success
|
||||
? '${capitalizeFirstLetter(_buttonTexts[widget.action]!)} marked successfully!'
|
||||
: 'Failed to mark ${capitalizeFirstLetter(_buttonTexts[widget.action]!)}.',
|
||||
type: success ? SnackbarType.success : SnackbarType.error,
|
||||
title: 'Warning',
|
||||
message: 'Please select a project first',
|
||||
type: SnackbarType.warning,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
widget.attendanceController.fetchEmployeesByProject(
|
||||
widget.attendanceController.selectedProjectId!);
|
||||
widget.attendanceController
|
||||
.fetchAttendanceLogs(widget.attendanceController.selectedProjectId!);
|
||||
await widget.attendanceController.fetchRegularizationLogs(
|
||||
widget.attendanceController.selectedProjectId!);
|
||||
await widget.attendanceController
|
||||
.fetchProjectData(widget.attendanceController.selectedProjectId!);
|
||||
}
|
||||
|
||||
widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value =
|
||||
false;
|
||||
|
||||
setState(() {
|
||||
isUploading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
isUploading = true;
|
||||
});
|
||||
|
||||
widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value = true;
|
||||
|
||||
final success = await widget.attendanceController.captureAndUploadAttendance(
|
||||
widget.log.id,
|
||||
widget.log.employeeId,
|
||||
selectedProjectId,
|
||||
comment: _buttonComments[widget.action]!,
|
||||
action: _buttonActionCodes[widget.action]!,
|
||||
imageCapture: false,
|
||||
);
|
||||
|
||||
showAppSnackbar(
|
||||
title: success ? 'Success' : 'Error',
|
||||
message: success
|
||||
? '${capitalizeFirstLetter(_buttonTexts[widget.action]!)} marked successfully!'
|
||||
: 'Failed to mark ${capitalizeFirstLetter(_buttonTexts[widget.action]!)}.',
|
||||
type: success ? SnackbarType.success : SnackbarType.error,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
widget.attendanceController.fetchEmployeesByProject(selectedProjectId);
|
||||
widget.attendanceController.fetchAttendanceLogs(selectedProjectId);
|
||||
await widget.attendanceController.fetchRegularizationLogs(selectedProjectId);
|
||||
await widget.attendanceController.fetchProjectData(selectedProjectId);
|
||||
}
|
||||
|
||||
widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value = false;
|
||||
|
||||
setState(() {
|
||||
isUploading = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final buttonText = _buttonTexts[widget.action]!;
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChartSampleData {
|
||||
ChartSampleData(
|
||||
{this.x,
|
||||
this.y,
|
||||
this.xValue,
|
||||
this.yValue,
|
||||
this.secondSeriesYValue,
|
||||
this.thirdSeriesYValue,
|
||||
this.pointColor,
|
||||
this.size,
|
||||
this.text,
|
||||
this.open,
|
||||
this.close,
|
||||
this.low,
|
||||
this.high,
|
||||
this.volume});
|
||||
|
||||
final dynamic x;
|
||||
final num? y;
|
||||
final dynamic xValue;
|
||||
final num? yValue;
|
||||
final num? secondSeriesYValue;
|
||||
final num? thirdSeriesYValue;
|
||||
final Color? pointColor;
|
||||
final num? size;
|
||||
final String? text;
|
||||
final num? open;
|
||||
final num? close;
|
||||
final num? low;
|
||||
final num? high;
|
||||
final num? volume;
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:marco/helpers/services/json_decoder.dart';
|
||||
import 'package:marco/images.dart';
|
||||
import 'package:marco/model/identifier_model.dart';
|
||||
|
||||
class ChatModel extends IdentifierModel {
|
||||
final String firstName, avatar, email;
|
||||
|
||||
final List<ChatMessageModel> messages;
|
||||
|
||||
ChatModel(super.id, this.firstName, this.avatar, this.messages, this.email);
|
||||
|
||||
static ChatModel fromJSON(Map<String, dynamic> json) {
|
||||
JSONDecoder decoder = JSONDecoder(json);
|
||||
|
||||
String firstName = decoder.getString('first_name');
|
||||
String email = decoder.getString('email');
|
||||
String avatar = Images.randomImage(Images.avatars);
|
||||
|
||||
List<dynamic>? messagesList = decoder.getObjectListOrNull('messages');
|
||||
List<ChatMessageModel> messages = [];
|
||||
if (messagesList != null) {
|
||||
messages = ChatMessageModel.listFromJSON(messagesList);
|
||||
}
|
||||
|
||||
return ChatModel(decoder.getId, firstName, avatar, messages, email);
|
||||
}
|
||||
|
||||
static List<ChatModel> listFromJSON(List<dynamic> list) {
|
||||
return list.map((e) => ChatModel.fromJSON(e)).toList();
|
||||
}
|
||||
|
||||
static List<ChatModel>? _dummyList;
|
||||
|
||||
static Future<List<ChatModel>> get dummyList async {
|
||||
if (_dummyList == null) {
|
||||
dynamic data = json.decode(await getData());
|
||||
_dummyList = listFromJSON(data);
|
||||
}
|
||||
|
||||
return _dummyList!;
|
||||
}
|
||||
|
||||
static Future<String> getData() async {
|
||||
return await rootBundle.loadString('assets/data/chat.json');
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessageModel extends IdentifierModel {
|
||||
final String message, imageSent;
|
||||
final DateTime sendAt;
|
||||
final bool fromMe;
|
||||
|
||||
ChatMessageModel(super.id, this.message, this.sendAt, this.fromMe, this.imageSent);
|
||||
|
||||
static ChatMessageModel fromJSON(Map<String, dynamic> json) {
|
||||
JSONDecoder decoder = JSONDecoder(json);
|
||||
|
||||
String message = decoder.getString('message');
|
||||
String imageSent = Images.randomImage(Images.avatars);
|
||||
DateTime sendAt = decoder.getDateTime('send_at');
|
||||
bool fromMe = decoder.getBool('from_me');
|
||||
|
||||
return ChatMessageModel(decoder.getId, message, sendAt, fromMe, imageSent);
|
||||
}
|
||||
|
||||
static List<ChatMessageModel> listFromJSON(List<dynamic> list) {
|
||||
return list.map((e) => ChatMessageModel.fromJSON(e)).toList();
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:marco/helpers/services/json_decoder.dart';
|
||||
import 'package:marco/model/identifier_model.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CoinGrowthModel extends IdentifierModel {
|
||||
final String asset, ipAddress, status;
|
||||
final int amount;
|
||||
final DateTime date;
|
||||
|
||||
CoinGrowthModel(
|
||||
super.id,
|
||||
this.asset,
|
||||
this.ipAddress,
|
||||
this.status,
|
||||
this.amount,
|
||||
this.date,
|
||||
);
|
||||
|
||||
static CoinGrowthModel fromJSON(Map<String, dynamic> json) {
|
||||
JSONDecoder decoder = JSONDecoder(json);
|
||||
|
||||
String asset = decoder.getString('asset');
|
||||
String ipAddress = decoder.getString('ip_address');
|
||||
String status = decoder.getString('status');
|
||||
int amount = decoder.getInt('amount');
|
||||
DateTime date = decoder.getDateTime('date');
|
||||
|
||||
return CoinGrowthModel(decoder.getId, asset, ipAddress, status, amount, date);
|
||||
}
|
||||
|
||||
static List<CoinGrowthModel> listFromJSON(List<dynamic> list) {
|
||||
return list.map((e) => CoinGrowthModel.fromJSON(e)).toList();
|
||||
}
|
||||
|
||||
static List<CoinGrowthModel>? _dummyList;
|
||||
|
||||
static Future<List<CoinGrowthModel>> get dummyList async {
|
||||
if (_dummyList == null) {
|
||||
dynamic data = json.decode(await getData());
|
||||
_dummyList = listFromJSON(data);
|
||||
}
|
||||
|
||||
return _dummyList!;
|
||||
}
|
||||
|
||||
static Future<String> getData() async {
|
||||
return await rootBundle.loadString('assets/data/coin_growth.json');
|
||||
}
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:marco/helpers/services/json_decoder.dart';
|
||||
import 'package:marco/model/identifier_model.dart';
|
||||
|
||||
class Customer extends IdentifierModel {
|
||||
final String firstName, lastName, phoneNumber, projectName, balance;
|
||||
final double ordersCount;
|
||||
final DateTime lastOrder;
|
||||
|
||||
String get fullName => '$firstName $lastName $projectName';
|
||||
|
||||
Customer(
|
||||
super.id,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.phoneNumber,
|
||||
this.balance,
|
||||
this.ordersCount,
|
||||
this.lastOrder,
|
||||
this.projectName,
|
||||
);
|
||||
|
||||
static Customer fromJSON(Map<String, dynamic> json) {
|
||||
JSONDecoder decoder = JSONDecoder(json);
|
||||
|
||||
String firstName = decoder.getString('first_name');
|
||||
String lastName = decoder.getString('last_name');
|
||||
String phoneNumber = decoder.getString('phone_number');
|
||||
String balance = decoder.getString('balance');
|
||||
double ordersCount = decoder.getDouble('order_count');
|
||||
DateTime lastOrder = decoder.getDateTime('last_order');
|
||||
String projectName = decoder.getString('project_name');
|
||||
|
||||
return Customer(
|
||||
decoder.getId,
|
||||
firstName,
|
||||
lastName,
|
||||
phoneNumber,
|
||||
balance,
|
||||
ordersCount,
|
||||
lastOrder,
|
||||
projectName,
|
||||
);
|
||||
}
|
||||
|
||||
static List<Customer> listFromJSON(List<dynamic> list) {
|
||||
return list.map((e) => Customer.fromJSON(e)).toList();
|
||||
}
|
||||
|
||||
static List<Customer>? _dummyList;
|
||||
|
||||
static Future<List<Customer>> get dummyList async {
|
||||
if (_dummyList == null) {
|
||||
dynamic data = json.decode(await getData());
|
||||
_dummyList = listFromJSON(data);
|
||||
}
|
||||
|
||||
return _dummyList!.sublist(0, 10);
|
||||
}
|
||||
|
||||
static Future<String> getData() async {
|
||||
return await rootBundle.loadString('assets/data/customer.json');
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import 'package:marco/helpers/widgets/my_button.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
class AssignTaskBottomSheet extends StatefulWidget {
|
||||
final String workLocation;
|
||||
@ -34,6 +35,7 @@ class AssignTaskBottomSheet extends StatefulWidget {
|
||||
|
||||
class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
|
||||
final DailyTaskPlaningController controller = Get.find();
|
||||
final ProjectController projectController = Get.find();
|
||||
final TextEditingController targetController = TextEditingController();
|
||||
final TextEditingController descriptionController = TextEditingController();
|
||||
String? selectedProjectId;
|
||||
@ -51,7 +53,8 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedProjectId = controller.selectedProjectId;
|
||||
selectedProjectId = projectController.selectedProjectId?.value;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (selectedProjectId != null) {
|
||||
controller.fetchEmployeesByProject(selectedProjectId!);
|
||||
@ -140,16 +143,13 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
|
||||
),
|
||||
MySpacing.height(8),
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 150,
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: 150),
|
||||
child: _buildEmployeeList(),
|
||||
),
|
||||
MySpacing.height(8),
|
||||
|
||||
Obx(() {
|
||||
if (controller.selectedEmployees.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
if (controller.selectedEmployees.isEmpty) return Container();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
@ -163,21 +163,23 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
|
||||
if (!isSelected) return Container();
|
||||
|
||||
return Chip(
|
||||
label: Text(e.name,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
backgroundColor:
|
||||
const Color.fromARGB(255, 95, 132, 255),
|
||||
deleteIcon:
|
||||
const Icon(Icons.close, color: Colors.white),
|
||||
onDeleted: () {
|
||||
controller.uploadingStates[e.id]?.value = false;
|
||||
controller.updateSelectedEmployees();
|
||||
});
|
||||
label: Text(e.name,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
backgroundColor:
|
||||
const Color.fromARGB(255, 95, 132, 255),
|
||||
deleteIcon:
|
||||
const Icon(Icons.close, color: Colors.white),
|
||||
onDeleted: () {
|
||||
controller.uploadingStates[e.id]?.value = false;
|
||||
controller.updateSelectedEmployees();
|
||||
},
|
||||
);
|
||||
});
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
_buildTextField(
|
||||
icon: Icons.track_changes,
|
||||
label: "Target for Today :",
|
||||
@ -187,6 +189,7 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
|
||||
validatorType: "target",
|
||||
),
|
||||
MySpacing.height(24),
|
||||
|
||||
_buildTextField(
|
||||
icon: Icons.description,
|
||||
label: "Description :",
|
||||
@ -196,6 +199,7 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
|
||||
validatorType: "description",
|
||||
),
|
||||
MySpacing.height(24),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -225,7 +229,6 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
|
||||
}
|
||||
|
||||
final selectedRoleId = controller.selectedRoleId.value;
|
||||
|
||||
final filteredEmployees = selectedRoleId == null
|
||||
? controller.employees
|
||||
: controller.employees
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
|
||||
|
||||
class CommentTaskBottomSheet extends StatefulWidget {
|
||||
final Map<String, dynamic> taskData;
|
||||
@ -59,6 +60,7 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
controller.basicValidator.getController('task_id')?.text =
|
||||
data['taskId'] ?? '';
|
||||
controller.basicValidator.getController('comment')?.clear();
|
||||
controller.selectedImages.clear();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
|
||||
@ -177,6 +179,90 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
icon: Icons.done_all_outlined,
|
||||
),
|
||||
buildTeamMembers(),
|
||||
if ((widget.taskData['reportedPreSignedUrls']
|
||||
as List<dynamic>?)
|
||||
?.isNotEmpty ==
|
||||
true) ...[
|
||||
MySpacing.height(8),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 0.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.image_outlined,
|
||||
size: 18, color: Colors.grey[700]),
|
||||
MySpacing.width(8),
|
||||
MyText.titleSmall(
|
||||
"Reported Images",
|
||||
fontWeight: 600,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
MySpacing.height(8),
|
||||
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final allImageUrls = List<String>.from(
|
||||
widget.taskData['reportedPreSignedUrls'] ?? [],
|
||||
);
|
||||
|
||||
if (allImageUrls.isEmpty) return const SizedBox();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
16.0), // Same horizontal padding
|
||||
child: SizedBox(
|
||||
height: 70,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: allImageUrls.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
const SizedBox(width: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final url = allImageUrls[index];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.black54,
|
||||
builder: (_) => ImageViewerDialog(
|
||||
imageSources: allImageUrls,
|
||||
initialIndex: index,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
child: Image.network(
|
||||
url,
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
color: Colors.grey.shade200,
|
||||
child: Icon(Icons.broken_image,
|
||||
color: Colors.grey[600]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MySpacing.height(16),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.comment_outlined,
|
||||
@ -206,6 +292,148 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
),
|
||||
),
|
||||
MySpacing.height(16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.camera_alt_outlined,
|
||||
size: 18, color: Colors.grey[700]),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall("Attach Photos:",
|
||||
fontWeight: 600),
|
||||
MySpacing.height(12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Obx(() {
|
||||
final images = controller.selectedImages;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (images.isEmpty)
|
||||
Container(
|
||||
height: 70,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300, width: 2),
|
||||
color: Colors.grey.shade100,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(Icons.photo_camera_outlined,
|
||||
size: 48, color: Colors.grey.shade400),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: 70,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: images.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final file = images[index];
|
||||
return Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
ImageViewerDialog(
|
||||
imageSources: images,
|
||||
initialIndex: index,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
child: Image.file(
|
||||
file,
|
||||
height: 70,
|
||||
width: 70,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller
|
||||
.removeImageAt(index),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.close,
|
||||
size: 20,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
MySpacing.height(16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyButton.outlined(
|
||||
onPressed: () => controller.pickImages(
|
||||
fromCamera: true),
|
||||
padding: MySpacing.xy(12, 10),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.camera_alt,
|
||||
size: 16,
|
||||
color: Colors.blueAccent),
|
||||
MySpacing.width(6),
|
||||
MyText.bodySmall('Capture',
|
||||
color: Colors.blueAccent),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: MyButton.outlined(
|
||||
onPressed: () => controller.pickImages(
|
||||
fromCamera: false),
|
||||
padding: MySpacing.xy(12, 10),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.upload_file,
|
||||
size: 16,
|
||||
color: Colors.blueAccent),
|
||||
MySpacing.width(6),
|
||||
MyText.bodySmall('Upload',
|
||||
color: Colors.blueAccent),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
MySpacing.height(24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
@ -233,7 +461,9 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
.getController('comment')
|
||||
?.text ??
|
||||
'',
|
||||
images: controller.selectedImages,
|
||||
);
|
||||
|
||||
if (widget.onCommentSuccess != null) {
|
||||
widget.onCommentSuccess!();
|
||||
}
|
||||
@ -262,12 +492,13 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
}),
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
MySpacing.height(10),
|
||||
if ((widget.taskData['taskComments'] as List<dynamic>?)
|
||||
?.isNotEmpty ==
|
||||
true) ...[
|
||||
Row(
|
||||
children: [
|
||||
MySpacing.width(10),
|
||||
Icon(Icons.chat_bubble_outline,
|
||||
size: 18, color: Colors.grey[700]),
|
||||
MySpacing.width(8),
|
||||
@ -277,6 +508,7 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(),
|
||||
MySpacing.height(12),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
@ -298,6 +530,9 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
return SizedBox(
|
||||
height: 300,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical:
|
||||
8), // Added padding around the list
|
||||
itemCount: comments.length,
|
||||
itemBuilder: (context, index) {
|
||||
final comment = comments[index];
|
||||
@ -306,11 +541,13 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
comment['commentedBy'] ?? 'Unknown';
|
||||
final relativeTime =
|
||||
timeAgo(comment['date'] ?? '');
|
||||
|
||||
// Dummy image URLs (simulate as if coming from backend)
|
||||
final imageUrls = List<String>.from(
|
||||
comment['preSignedUrls'] ?? []);
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(
|
||||
vertical: 6, horizontal: 8),
|
||||
padding: EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8), // Spacing between items
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@ -319,55 +556,198 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Avatar for commenter
|
||||
Avatar(
|
||||
firstName:
|
||||
commentedBy.split(' ').first,
|
||||
lastName: commentedBy
|
||||
.split(' ')
|
||||
.length >
|
||||
1
|
||||
? commentedBy.split(' ').last
|
||||
: '',
|
||||
size: 32,
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
// Comment text and meta
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🔹 Top Row: Avatar + Name + Time
|
||||
Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: commentedBy
|
||||
.split(' ')
|
||||
.first,
|
||||
lastName: commentedBy
|
||||
.split(' ')
|
||||
.length >
|
||||
1
|
||||
? commentedBy
|
||||
.split(' ')
|
||||
.last
|
||||
: '',
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
commentedBy,
|
||||
fontWeight: 700,
|
||||
color:
|
||||
Colors.black87,
|
||||
),
|
||||
MyText.bodySmall(
|
||||
relativeTime,
|
||||
fontSize: 12,
|
||||
color:
|
||||
Colors.black54,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 🔹 Comment text below attachments
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
commentedBy,
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
MyText.bodyMedium(
|
||||
commentText,
|
||||
fontWeight: 500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
Text(
|
||||
relativeTime,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black54,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Text(
|
||||
commentText,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
const SizedBox(height: 12),
|
||||
// 🔹 Attachments row: full width below top row
|
||||
if (imageUrls.isNotEmpty) ...[
|
||||
Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons
|
||||
.attach_file_outlined,
|
||||
size: 18,
|
||||
color:
|
||||
Colors.grey[700]),
|
||||
MyText.bodyMedium(
|
||||
'Attachments',
|
||||
fontWeight: 600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 60,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets
|
||||
.symmetric(
|
||||
horizontal: 0),
|
||||
scrollDirection:
|
||||
Axis.horizontal,
|
||||
itemCount:
|
||||
imageUrls.length,
|
||||
itemBuilder: (context,
|
||||
imageIndex) {
|
||||
final imageUrl =
|
||||
imageUrls[
|
||||
imageIndex];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor:
|
||||
Colors
|
||||
.black54,
|
||||
builder: (_) =>
|
||||
ImageViewerDialog(
|
||||
imageSources:
|
||||
imageUrls,
|
||||
initialIndex:
|
||||
imageIndex,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration:
|
||||
BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius
|
||||
.circular(
|
||||
12),
|
||||
color: Colors
|
||||
.grey[
|
||||
100],
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors
|
||||
.black26,
|
||||
blurRadius:
|
||||
6,
|
||||
offset:
|
||||
Offset(
|
||||
2,
|
||||
2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child:
|
||||
ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius
|
||||
.circular(
|
||||
12),
|
||||
child: Image
|
||||
.network(
|
||||
imageUrl,
|
||||
fit: BoxFit
|
||||
.cover,
|
||||
errorBuilder: (context,
|
||||
error,
|
||||
stackTrace) =>
|
||||
Container(
|
||||
color: Colors
|
||||
.grey[
|
||||
300],
|
||||
child: Icon(
|
||||
Icons
|
||||
.broken_image,
|
||||
color:
|
||||
Colors.grey[700]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
child: Icon(
|
||||
Icons
|
||||
.zoom_in,
|
||||
color: Colors
|
||||
.white70,
|
||||
size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder:
|
||||
(_, __) =>
|
||||
const SizedBox(
|
||||
width: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -20,13 +20,9 @@ class DailyProgressReportFilter extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DailyProgressReportFilterState extends State<DailyProgressReportFilter> {
|
||||
late String? tempSelectedProjectId;
|
||||
bool showProjectList = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
tempSelectedProjectId = widget.controller.selectedProjectId;
|
||||
}
|
||||
|
||||
String getLabelText() {
|
||||
@ -42,116 +38,6 @@ class _DailyProgressReportFilterState extends State<DailyProgressReportFilter> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accessibleProjects = widget.controller.projects
|
||||
.where((project) => widget.permissionController
|
||||
.isUserAssignedToProject(project.id.toString()))
|
||||
.toList();
|
||||
|
||||
List<Widget> filterWidgets;
|
||||
|
||||
if (showProjectList) {
|
||||
filterWidgets = accessibleProjects.isEmpty
|
||||
? [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Center(child: Text('No Projects Assigned')),
|
||||
),
|
||||
]
|
||||
: accessibleProjects.map((project) {
|
||||
final isSelected = tempSelectedProjectId == project.id.toString();
|
||||
return ListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
title: Text(project.name),
|
||||
trailing: isSelected ? const Icon(Icons.check) : null,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
tempSelectedProjectId = project.id.toString();
|
||||
showProjectList = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
} else {
|
||||
final selectedProject = accessibleProjects.isNotEmpty
|
||||
? accessibleProjects.firstWhere(
|
||||
(p) => p.id.toString() == tempSelectedProjectId,
|
||||
orElse: () => accessibleProjects[0],
|
||||
)
|
||||
: null;
|
||||
|
||||
final selectedProjectName = selectedProject?.name ?? "Select Project";
|
||||
|
||||
filterWidgets = [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: MyText.titleSmall(
|
||||
'Select Project',
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
title: Text(selectedProjectName),
|
||||
trailing: const Icon(Icons.arrow_drop_down),
|
||||
onTap: () => setState(() => showProjectList = true),
|
||||
),
|
||||
];
|
||||
|
||||
filterWidgets.addAll([
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: MyText.titleSmall(
|
||||
"Select Date Range",
|
||||
fontWeight: 600,
|
||||
)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () => widget.controller.selectDateRangeForTaskData(
|
||||
context,
|
||||
widget.controller,
|
||||
),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.date_range, color: Colors.blue.shade600),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
getLabelText(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
@ -174,7 +60,55 @@ class _DailyProgressReportFilterState extends State<DailyProgressReportFilter> {
|
||||
),
|
||||
),
|
||||
),
|
||||
...filterWidgets,
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: MyText.titleSmall(
|
||||
"Select Date Range",
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () => widget.controller.selectDateRangeForTaskData(
|
||||
context,
|
||||
widget.controller,
|
||||
),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.date_range, color: Colors.blue.shade600),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
getLabelText(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding:
|
||||
@ -190,8 +124,11 @@ class _DailyProgressReportFilterState extends State<DailyProgressReportFilter> {
|
||||
),
|
||||
child: const Text('Apply Filter'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, {
|
||||
'projectId': tempSelectedProjectId,
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Navigator.pop(context, {
|
||||
'startDate': widget.controller.startDateTask,
|
||||
'endDate': widget.controller.endDateTask,
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
@ -15,7 +15,7 @@ class DailyTaskPlaningFilter extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String? tempSelectedProjectId = controller.selectedProjectId;
|
||||
String? tempSelectedProjectId = '654563563645';
|
||||
bool showProjectList = false;
|
||||
|
||||
final accessibleProjects = controller.projects
|
||||
|
||||
@ -10,8 +10,12 @@ import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||
|
||||
class ReportTaskBottomSheet extends StatefulWidget {
|
||||
final Map<String, dynamic> taskData;
|
||||
final VoidCallback? onReportSuccess;
|
||||
const ReportTaskBottomSheet({super.key, required this.taskData,this.onReportSuccess,});
|
||||
final VoidCallback? onReportSuccess;
|
||||
const ReportTaskBottomSheet({
|
||||
super.key,
|
||||
required this.taskData,
|
||||
this.onReportSuccess,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState();
|
||||
@ -201,6 +205,147 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
||||
),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.camera_alt_outlined,
|
||||
size: 18, color: Colors.grey[700]),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall("Attach Photos:",
|
||||
fontWeight: 600),
|
||||
MySpacing.height(12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Obx(() {
|
||||
final images = controller.selectedImages;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (images.isEmpty)
|
||||
Container(
|
||||
height: 70,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300, width: 2),
|
||||
color: Colors.grey.shade100,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(Icons.photo_camera_outlined,
|
||||
size: 48, color: Colors.grey.shade400),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: 70,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: images.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
MySpacing.width(12),
|
||||
itemBuilder: (context, index) {
|
||||
final file = images[index];
|
||||
return Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
child: InteractiveViewer(
|
||||
child: Image.file(file),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
child: Image.file(
|
||||
file,
|
||||
height: 70,
|
||||
width: 70,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller
|
||||
.removeImageAt(index),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.close,
|
||||
size: 20,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
MySpacing.height(16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyButton.outlined(
|
||||
onPressed: () => controller.pickImages(
|
||||
fromCamera: true),
|
||||
padding: MySpacing.xy(12, 10),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.camera_alt,
|
||||
size: 16,
|
||||
color: Colors.blueAccent),
|
||||
MySpacing.width(6),
|
||||
MyText.bodySmall('Capture',
|
||||
color: Colors.blueAccent),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: MyButton.outlined(
|
||||
onPressed: () => controller.pickImages(
|
||||
fromCamera: false),
|
||||
padding: MySpacing.xy(12, 10),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.upload_file,
|
||||
size: 16,
|
||||
color: Colors.blueAccent),
|
||||
MySpacing.width(6),
|
||||
MyText.bodySmall('Upload',
|
||||
color: Colors.blueAccent),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -211,60 +356,49 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
||||
child: MyText.bodySmall('Cancel'),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Obx(() {
|
||||
return MyButton(
|
||||
onPressed: controller.reportStatus.value ==
|
||||
ApiStatus.loading
|
||||
? null
|
||||
: () async {
|
||||
if (controller.basicValidator
|
||||
.validateForm()) {
|
||||
await controller.reportTask(
|
||||
projectId: controller.basicValidator
|
||||
.getController('task_id')
|
||||
?.text ??
|
||||
'',
|
||||
comment: controller.basicValidator
|
||||
.getController('comment')
|
||||
?.text ??
|
||||
'',
|
||||
completedTask: int.tryParse(
|
||||
controller.basicValidator
|
||||
.getController(
|
||||
'completed_work')
|
||||
?.text ??
|
||||
'') ??
|
||||
0,
|
||||
checklist: [],
|
||||
reportedDate: DateTime.now(),
|
||||
);
|
||||
if (widget.onReportSuccess != null) {
|
||||
widget.onReportSuccess!();
|
||||
}
|
||||
}
|
||||
},
|
||||
elevation: 0,
|
||||
padding: MySpacing.xy(20, 16),
|
||||
backgroundColor: Colors.blueAccent,
|
||||
borderRadiusAll: AppStyle.buttonRadius.medium,
|
||||
child: controller.reportStatus.value ==
|
||||
ApiStatus.loading
|
||||
? SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
contentTheme.onPrimary),
|
||||
),
|
||||
)
|
||||
: MyText.bodySmall(
|
||||
'Report',
|
||||
color: contentTheme.onPrimary,
|
||||
),
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final isLoading = controller.reportStatus.value == ApiStatus.loading;
|
||||
|
||||
return MyButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () async {
|
||||
if (controller.basicValidator.validateForm()) {
|
||||
final success = await controller.reportTask(
|
||||
projectId: controller.basicValidator.getController('task_id')?.text ?? '',
|
||||
comment: controller.basicValidator.getController('comment')?.text ?? '',
|
||||
completedTask: int.tryParse(
|
||||
controller.basicValidator.getController('completed_work')?.text ?? '') ??
|
||||
0,
|
||||
checklist: [],
|
||||
reportedDate: DateTime.now(),
|
||||
images: controller.selectedImages,
|
||||
);
|
||||
if (success && widget.onReportSuccess != null) {
|
||||
widget.onReportSuccess!();
|
||||
}
|
||||
}
|
||||
},
|
||||
elevation: 0,
|
||||
padding: MySpacing.xy(20, 16),
|
||||
backgroundColor: Colors.blueAccent,
|
||||
borderRadiusAll: AppStyle.buttonRadius.medium,
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: MyText.bodySmall(
|
||||
'Report',
|
||||
color: contentTheme.onPrimary,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@ -9,10 +9,11 @@ class TaskModel {
|
||||
final AssignedBy assignedBy;
|
||||
final List<TeamMember> teamMembers;
|
||||
final List<Comment> comments;
|
||||
final List<String> reportedPreSignedUrls;
|
||||
|
||||
TaskModel({
|
||||
required this.assignmentDate,
|
||||
this.reportedDate,
|
||||
this.reportedDate,
|
||||
required this.id,
|
||||
required this.workItem,
|
||||
required this.workItemId,
|
||||
@ -21,13 +22,14 @@ class TaskModel {
|
||||
required this.assignedBy,
|
||||
required this.teamMembers,
|
||||
required this.comments,
|
||||
required this.reportedPreSignedUrls,
|
||||
});
|
||||
|
||||
factory TaskModel.fromJson(Map<String, dynamic> json) {
|
||||
return TaskModel(
|
||||
assignmentDate: DateTime.parse(json['assignmentDate']),
|
||||
reportedDate: json['reportedDate'] != null
|
||||
? DateTime.tryParse(json['reportedDate'])
|
||||
? DateTime.tryParse(json['reportedDate'])
|
||||
: null,
|
||||
id: json['id'],
|
||||
workItem:
|
||||
@ -41,6 +43,10 @@ class TaskModel {
|
||||
.toList(),
|
||||
comments:
|
||||
(json['comments'] as List).map((e) => Comment.fromJson(e)).toList(),
|
||||
reportedPreSignedUrls: (json['reportedPreSignedUrls'] as List<dynamic>?)
|
||||
?.map((e) => e.toString())
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -51,6 +57,7 @@ class WorkItem {
|
||||
final WorkArea? workArea;
|
||||
final int? plannedWork;
|
||||
final int? completedWork;
|
||||
final List<String> preSignedUrls;
|
||||
|
||||
WorkItem({
|
||||
this.id,
|
||||
@ -58,6 +65,7 @@ class WorkItem {
|
||||
this.workArea,
|
||||
this.plannedWork,
|
||||
this.completedWork,
|
||||
this.preSignedUrls = const [],
|
||||
});
|
||||
|
||||
factory WorkItem.fromJson(Map<String, dynamic> json) {
|
||||
@ -70,6 +78,10 @@ class WorkItem {
|
||||
json['workArea'] != null ? WorkArea.fromJson(json['workArea']) : null,
|
||||
plannedWork: json['plannedWork'],
|
||||
completedWork: json['completedWork'],
|
||||
preSignedUrls: (json['preSignedUrls'] as List<dynamic>?)
|
||||
?.map((e) => e.toString())
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -167,11 +179,13 @@ class Comment {
|
||||
final String comment;
|
||||
final TeamMember commentedBy;
|
||||
final DateTime timestamp;
|
||||
final List<String> preSignedUrls;
|
||||
|
||||
Comment({
|
||||
required this.comment,
|
||||
required this.commentedBy,
|
||||
required this.timestamp,
|
||||
required this.preSignedUrls,
|
||||
});
|
||||
|
||||
factory Comment.fromJson(Map<String, dynamic> json) {
|
||||
@ -181,6 +195,10 @@ class Comment {
|
||||
? TeamMember.fromJson(json['employee'])
|
||||
: TeamMember(id: '', firstName: '', lastName: null),
|
||||
timestamp: DateTime.parse(json['commentDate'] ?? ''),
|
||||
preSignedUrls: (json['preSignedUrls'] as List<dynamic>?)
|
||||
?.map((e) => e.toString())
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,10 +9,6 @@ import 'package:marco/view/error_pages/coming_soon_screen.dart';
|
||||
import 'package:marco/view/error_pages/error_404_screen.dart';
|
||||
import 'package:marco/view/error_pages/error_500_screen.dart';
|
||||
import 'package:marco/view/dashboard/dashboard_screen.dart';
|
||||
import 'package:marco/view/dashboard/add_employee_screen.dart';
|
||||
import 'package:marco/view/dashboard/daily_task_screen.dart';
|
||||
import 'package:marco/view/taskPlaning/report_task_screen.dart';
|
||||
import 'package:marco/view/taskPlaning/comment_task_screen.dart';
|
||||
import 'package:marco/view/dashboard/Attendence/attendance_screen.dart';
|
||||
import 'package:marco/view/taskPlaning/daily_task_planing.dart';
|
||||
import 'package:marco/view/taskPlaning/daily_progress.dart';
|
||||
@ -36,6 +32,12 @@ getPageRoute() {
|
||||
name: '/',
|
||||
page: () => DashboardScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
GetPage(
|
||||
name: '/home',
|
||||
page: () => DashboardScreen(), // or your actual home screen
|
||||
middlewares: [AuthMiddleware()],
|
||||
),
|
||||
|
||||
// Dashboard
|
||||
GetPage(
|
||||
name: '/dashboard/attendance',
|
||||
@ -49,16 +51,7 @@ getPageRoute() {
|
||||
name: '/dashboard/employees',
|
||||
page: () => EmployeesScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
// Employees Creation
|
||||
GetPage(
|
||||
name: '/employees/addEmployee',
|
||||
page: () => AddEmployeeScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
// Daily Task Planning
|
||||
GetPage(
|
||||
name: '/dashboard/daily-task',
|
||||
page: () => DailyTaskScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
GetPage(
|
||||
name: '/dashboard/daily-task-planing',
|
||||
page: () => DailyTaskPlaningScreen(),
|
||||
@ -67,14 +60,6 @@ getPageRoute() {
|
||||
name: '/dashboard/daily-task-progress',
|
||||
page: () => DailyProgressReportScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
GetPage(
|
||||
name: '/daily-task/report-task',
|
||||
page: () => ReportTaskScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
GetPage(
|
||||
name: '/daily-task/comment-task',
|
||||
page: () => CommentTaskScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
// Authentication
|
||||
GetPage(name: '/auth/login', page: () => LoginScreen()),
|
||||
GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()),
|
||||
|
||||
@ -236,8 +236,17 @@ class _MPINAuthScreenState extends State<MPINAuthScreen> with UIMixin {
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
onChanged: (value) =>
|
||||
controller.onDigitChanged(value, index, isRetype: isRetype),
|
||||
onChanged: (value) {
|
||||
controller.onDigitChanged(value, index, isRetype: isRetype);
|
||||
|
||||
if (!isRetype) {
|
||||
final isComplete =
|
||||
controller.digitControllers.every((c) => c.text.isNotEmpty);
|
||||
if (isComplete && !controller.isLoading.value) {
|
||||
controller.onSubmitMPIN();
|
||||
}
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
filled: true,
|
||||
|
||||
@ -3,15 +3,12 @@ import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@ -20,6 +17,7 @@ import 'package:marco/model/attendance/log_details_view.dart';
|
||||
import 'package:marco/model/attendance/attendence_action_button.dart';
|
||||
import 'package:marco/model/attendance/regualrize_action_button.dart';
|
||||
import 'package:marco/model/attendance/attendence_filter_sheet.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
class AttendanceScreen extends StatefulWidget {
|
||||
AttendanceScreen({super.key});
|
||||
@ -35,167 +33,241 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
Get.put(PermissionController());
|
||||
|
||||
String selectedTab = 'todaysAttendance';
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final projectController = Get.find<ProjectController>();
|
||||
final attendanceController = Get.find<AttendanceController>();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
// Listen for future changes in selected project
|
||||
ever<String?>(projectController.selectedProjectId!, (projectId) async {
|
||||
if (projectId != null && projectId.isNotEmpty) {
|
||||
try {
|
||||
await attendanceController.loadAttendanceData(projectId);
|
||||
attendanceController.update(['attendance_dashboard_controller']);
|
||||
} catch (e) {
|
||||
debugPrint("Error updating data on project change: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load data initially if project is already selected
|
||||
final initialProjectId = projectController.selectedProjectId?.value;
|
||||
if (initialProjectId != null && initialProjectId.isNotEmpty) {
|
||||
try {
|
||||
await attendanceController.loadAttendanceData(initialProjectId);
|
||||
attendanceController.update(['attendance_dashboard_controller']);
|
||||
} catch (e) {
|
||||
debugPrint("Error loading initial data: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder<AttendanceController>(
|
||||
init: attendanceController,
|
||||
tag: 'attendance_dashboard_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Attendance",
|
||||
fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Attendance', active: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(80),
|
||||
child: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
foregroundColor: Colors.black,
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(top: 15.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () {
|
||||
Get.offNamed('/dashboard');
|
||||
},
|
||||
),
|
||||
),
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(top: 15.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Attendance',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
const SizedBox(height: 2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: MySpacing.x(0),
|
||||
child: GetBuilder<AttendanceController>(
|
||||
init: attendanceController,
|
||||
tag: 'attendance_dashboard_controller',
|
||||
builder: (controller) {
|
||||
final selectedProjectId =
|
||||
Get.find<ProjectController>().selectedProjectId?.value;
|
||||
final bool noProjectSelected =
|
||||
selectedProjectId == null || selectedProjectId.isEmpty;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
"Filter",
|
||||
fontWeight: 600,
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Filter Project',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: () async {
|
||||
final result =
|
||||
await showModalBottomSheet<Map<String, dynamic>>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
builder: (context) => AttendanceFilterBottomSheet(
|
||||
controller: attendanceController,
|
||||
permissionController: permissionController,
|
||||
selectedTab: selectedTab,
|
||||
),
|
||||
);
|
||||
MySpacing.height(flexSpacing),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
MyText.bodyMedium("Filter", fontWeight: 600),
|
||||
Tooltip(
|
||||
message: 'Filter Project',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: () async {
|
||||
final result = await showModalBottomSheet<
|
||||
Map<String, dynamic>>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12)),
|
||||
),
|
||||
builder: (context) => AttendanceFilterBottomSheet(
|
||||
controller: attendanceController,
|
||||
permissionController: permissionController,
|
||||
selectedTab: selectedTab,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
final selectedProjectId =
|
||||
result['projectId'] as String?;
|
||||
final selectedView = result['selectedTab'] as String?;
|
||||
if (result != null) {
|
||||
final selectedProjectId =
|
||||
Get.find<ProjectController>()
|
||||
.selectedProjectId
|
||||
?.value;
|
||||
|
||||
if (selectedProjectId != null &&
|
||||
selectedProjectId !=
|
||||
attendanceController.selectedProjectId) {
|
||||
attendanceController.selectedProjectId =
|
||||
selectedProjectId;
|
||||
try {
|
||||
await attendanceController
|
||||
.fetchEmployeesByProject(selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchAttendanceLogs(selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchRegularizationLogs(selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchProjectData(selectedProjectId);
|
||||
} catch (_) {}
|
||||
attendanceController
|
||||
.update(['attendance_dashboard_controller']);
|
||||
}
|
||||
final selectedView =
|
||||
result['selectedTab'] as String?;
|
||||
|
||||
if (selectedView != null &&
|
||||
selectedView != selectedTab) {
|
||||
setState(() {
|
||||
selectedTab = selectedView;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.filter_list_alt,
|
||||
color: Colors.blueAccent,
|
||||
size: 28,
|
||||
if (selectedProjectId != null) {
|
||||
try {
|
||||
await attendanceController
|
||||
.fetchEmployeesByProject(
|
||||
selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchAttendanceLogs(selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchRegularizationLogs(
|
||||
selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchProjectData(selectedProjectId);
|
||||
} catch (_) {}
|
||||
attendanceController.update(
|
||||
['attendance_dashboard_controller']);
|
||||
}
|
||||
|
||||
if (selectedView != null &&
|
||||
selectedView != selectedTab) {
|
||||
setState(() {
|
||||
selectedTab = selectedView;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.filter_list_alt,
|
||||
color: Colors.blueAccent,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
MyText.bodyMedium(
|
||||
"Refresh",
|
||||
fontWeight: 600,
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Refresh Data',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: () async {
|
||||
final projectId =
|
||||
attendanceController.selectedProjectId;
|
||||
if (projectId != null && projectId.isNotEmpty) {
|
||||
try {
|
||||
await attendanceController
|
||||
.fetchEmployeesByProject(projectId);
|
||||
await attendanceController
|
||||
.fetchAttendanceLogs(projectId);
|
||||
await attendanceController
|
||||
.fetchRegularizationLogs(projectId);
|
||||
await attendanceController
|
||||
.fetchProjectData(projectId);
|
||||
attendanceController
|
||||
.update(['attendance_dashboard_controller']);
|
||||
} catch (e) {
|
||||
debugPrint("Error refreshing data: $e");
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.green,
|
||||
size: 28,
|
||||
const SizedBox(width: 4),
|
||||
MyText.bodyMedium("Refresh", fontWeight: 600),
|
||||
Tooltip(
|
||||
message: 'Refresh Data',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: () async {
|
||||
final projectId = Get.find<ProjectController>()
|
||||
.selectedProjectId
|
||||
?.value;
|
||||
if (projectId != null && projectId.isNotEmpty) {
|
||||
try {
|
||||
await attendanceController
|
||||
.loadAttendanceData(projectId);
|
||||
attendanceController.update(
|
||||
['attendance_dashboard_controller']);
|
||||
} catch (e) {
|
||||
debugPrint("Error refreshing data: $e");
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.green,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
MyFlex(children: [
|
||||
MyFlexItem(
|
||||
sizes: 'lg-12 md-12 sm-12',
|
||||
child: noProjectSelected
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: MyText.titleMedium(
|
||||
'No Records Found',
|
||||
fontWeight: 600,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
)
|
||||
: selectedTab == 'todaysAttendance'
|
||||
? employeeListTab()
|
||||
: selectedTab == 'attendanceLogs'
|
||||
? employeeLog()
|
||||
: regularizationScreen(),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(children: [
|
||||
MyFlexItem(
|
||||
sizes: 'lg-12 md-12 sm-12',
|
||||
child: selectedTab == 'todaysAttendance'
|
||||
? employeeListTab()
|
||||
: selectedTab == 'attendanceLogs'
|
||||
? employeeLog()
|
||||
: regularizationScreen(),
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,261 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/dashboard/add_employee_controller.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_button.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
|
||||
class AddEmployeeScreen extends StatefulWidget {
|
||||
const AddEmployeeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AddEmployeeScreen> createState() => _AddEmployeeScreenState();
|
||||
}
|
||||
|
||||
class _AddEmployeeScreenState extends State<AddEmployeeScreen> with UIMixin {
|
||||
final AddEmployeeController controller = Get.put(AddEmployeeController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder<AddEmployeeController>(
|
||||
init: controller,
|
||||
tag: 'add_employee_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium(
|
||||
"Add Employee",
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Employee'),
|
||||
MyBreadcrumbItem(name: 'Add Employee'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(
|
||||
children: [
|
||||
MyFlexItem(sizes: "lg-8 md-12", child: detail()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget detail() {
|
||||
return Form(
|
||||
key: controller.basicValidator.formKey,
|
||||
child: MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(LucideIcons.server, size: 16),
|
||||
MySpacing.width(12),
|
||||
MyText.titleMedium("General", fontWeight: 600),
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium("First Name"),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
validator: controller.basicValidator.getValidation('first_name'),
|
||||
controller: controller.basicValidator.getController('first_name'),
|
||||
keyboardType: TextInputType.name,
|
||||
decoration: InputDecoration(
|
||||
hintText: "eg: Jhon",
|
||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||
border: outlineInputBorder,
|
||||
enabledBorder: outlineInputBorder,
|
||||
focusedBorder: focusedInputBorder,
|
||||
contentPadding: MySpacing.all(16),
|
||||
isCollapsed: true,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
MyText.labelMedium("Last Name"),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
validator: controller.basicValidator.getValidation('last_name'),
|
||||
controller: controller.basicValidator.getController('last_name'),
|
||||
keyboardType: TextInputType.name,
|
||||
decoration: InputDecoration(
|
||||
hintText: "eg: Doe",
|
||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||
border: outlineInputBorder,
|
||||
enabledBorder: outlineInputBorder,
|
||||
focusedBorder: focusedInputBorder,
|
||||
contentPadding: MySpacing.all(16),
|
||||
isCollapsed: true,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
MyText.labelMedium("Phone Number"),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
validator: controller.basicValidator.getValidation('phone_number'),
|
||||
controller: controller.basicValidator.getController('phone_number'),
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
hintText: "eg: +91 9876543210",
|
||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||
border: outlineInputBorder,
|
||||
enabledBorder: outlineInputBorder,
|
||||
focusedBorder: focusedInputBorder,
|
||||
contentPadding: MySpacing.all(16),
|
||||
isCollapsed: true,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
MyFlex(contentPadding: false, children: [
|
||||
MyFlexItem(
|
||||
sizes: 'lg-6 md-12',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium("Select Gender"),
|
||||
MySpacing.height(8),
|
||||
DropdownButtonFormField<Gender>(
|
||||
value: controller.selectedGender,
|
||||
dropdownColor: contentTheme.background,
|
||||
menuMaxHeight: 200,
|
||||
isDense: true,
|
||||
items: Gender.values.map((gender) {
|
||||
return DropdownMenuItem<Gender>(
|
||||
value: gender,
|
||||
child: MyText.labelMedium(
|
||||
gender.name[0].toUpperCase() + gender.name.substring(1),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
icon: const Icon(Icons.expand_more, size: 20),
|
||||
decoration: InputDecoration(
|
||||
hintText: "Select Gender",
|
||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||
border: outlineInputBorder,
|
||||
enabledBorder: outlineInputBorder,
|
||||
focusedBorder: focusedInputBorder,
|
||||
contentPadding: MySpacing.all(14),
|
||||
isCollapsed: true,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
),
|
||||
onChanged: controller.onGenderSelected,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]),
|
||||
MySpacing.height(24),
|
||||
MyFlex(contentPadding: false, children: [
|
||||
MyFlexItem(
|
||||
sizes: 'lg-6 md-12',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium("Select Role"),
|
||||
MySpacing.height(8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: controller.selectedRoleId,
|
||||
dropdownColor: contentTheme.background,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Select Role",
|
||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||
border: outlineInputBorder,
|
||||
enabledBorder: outlineInputBorder,
|
||||
focusedBorder: focusedInputBorder,
|
||||
contentPadding: MySpacing.all(14),
|
||||
isCollapsed: true,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
),
|
||||
icon: const Icon(Icons.expand_more, size: 20),
|
||||
isDense: true,
|
||||
items: controller.roles.map((role) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: role['id'],
|
||||
child: Text(role['name']),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: controller.onRoleSelected,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]),
|
||||
MySpacing.height(24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
MyButton.text(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
padding: MySpacing.xy(20, 16),
|
||||
splashColor: contentTheme.secondary.withValues(alpha: 0.1),
|
||||
child: MyText.bodySmall('Cancel'),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
MyButton(
|
||||
onPressed: () async {
|
||||
if (controller.basicValidator.validateForm()) {
|
||||
await controller.createEmployees();
|
||||
}
|
||||
},
|
||||
elevation: 0,
|
||||
padding: MySpacing.xy(20, 16),
|
||||
backgroundColor: contentTheme.primary,
|
||||
borderRadiusAll: AppStyle.buttonRadius.medium,
|
||||
child: MyText.bodySmall(
|
||||
'Save',
|
||||
color: contentTheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,546 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/dashboard/analytics_controller.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/utils/utils.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_list_extension.dart';
|
||||
import 'package:marco/helpers/widgets/my_progress_bar.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/images.dart';
|
||||
import 'package:marco/model/chart_model.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class AnalyticsScreen extends StatefulWidget {
|
||||
const AnalyticsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AnalyticsScreen> createState() => _AnalyticsScreenState();
|
||||
}
|
||||
|
||||
class _AnalyticsScreenState extends State<AnalyticsScreen> with UIMixin {
|
||||
AnalyticsController controller = Get.put(AnalyticsController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder(
|
||||
init: controller,
|
||||
tag: 'analytics_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Analytics", fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Analytics', active: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(children: [
|
||||
MyFlexItem(sizes: 'lg-2.4 md-6 sm-6', child: stats("Pending", "1.245", "5.12%", LucideIcons.clock)),
|
||||
MyFlexItem(sizes: 'lg-2.4 md-6 sm-6', child: stats("Paid", "92.342", "67.89%", LucideIcons.circle_check)),
|
||||
MyFlexItem(sizes: 'lg-2.4 md-4 sm-4', child: stats("Rejected", "12.367", "3.56%", LucideIcons.circle_x)),
|
||||
MyFlexItem(sizes: 'lg-2.4 md-4 sm-4', child: stats("In Progress", "5.125", "10.78%", LucideIcons.hourglass)),
|
||||
MyFlexItem(sizes: 'lg-2.4 md-4 sm-4', child: stats("Canceled", "7.489", "4.45%", LucideIcons.trash)),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: activityOnThePage()),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: audienceOverview()),
|
||||
MyFlexItem(sizes: 'lg-4 md-12', child: buildTrafficSources()),
|
||||
MyFlexItem(sizes: 'lg-4 md-6 sm-6', child: buildMostActiveUser()),
|
||||
MyFlexItem(sizes: 'lg-4 md-6 sm-6', child: buildVisitorsByCountry()),
|
||||
MyFlexItem(child: buildVisitorByChannel()),
|
||||
]),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget stats(String title, String subTitle, String percentage, IconData icon) {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 0,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.all(24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodySmall(title, maxLines: 1),
|
||||
MySpacing.height(4),
|
||||
MyText.titleLarge(subTitle, maxLines: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyContainer(
|
||||
color: contentTheme.secondary.withValues(alpha:0.2),
|
||||
paddingAll: 12,
|
||||
child: Icon(icon, size: 16, color: contentTheme.onBackground),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
MyContainer(
|
||||
color: contentTheme.background,
|
||||
borderRadiusAll: 0,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.arrow_up_right, size: 16),
|
||||
MySpacing.width(8),
|
||||
MyText.labelMedium(percentage),
|
||||
MySpacing.width(8),
|
||||
Expanded(child: MyText.labelMedium("Last Month", muted: true, maxLines: 1)),
|
||||
Expanded(child: InkWell(onTap: () {}, child: MyText.labelMedium("View More", fontWeight: 600, maxLines: 1))),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget activityOnThePage() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.bodyMedium("Activity on the pages", fontWeight: 600, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
PopupMenuButton(
|
||||
onSelected: controller.onSelectedActivity,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
itemBuilder: (BuildContext context) {
|
||||
return ["Year", "Month", "Week", "Day", "Hours"].map((behavior) {
|
||||
return PopupMenuItem(
|
||||
value: behavior,
|
||||
height: 32,
|
||||
child: MyText.bodySmall(
|
||||
behavior.toString(),
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontWeight: 600,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
color: theme.cardTheme.color,
|
||||
child: MyContainer.bordered(
|
||||
padding: MySpacing.xy(8, 4),
|
||||
borderRadiusAll: 8,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
MyText.bodySmall(
|
||||
controller.selectActivity.toString(),
|
||||
fontWeight: 600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
MySpacing.width(4),
|
||||
Icon(
|
||||
LucideIcons.chevron_down,
|
||||
size: 20,
|
||||
color: theme.colorScheme.onSurface,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
SizedBox(
|
||||
height: 305,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)),
|
||||
tooltipBehavior: controller.columnChartToolTip,
|
||||
legend: Legend(isVisible: true, position: LegendPosition.bottom),
|
||||
series: [
|
||||
ColumnSeries<ChartSampleData, int>(
|
||||
opacity: 0.9,
|
||||
width: 0.6,
|
||||
color: contentTheme.title,
|
||||
dataSource: controller.columnChart,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
|
||||
xValueMapper: (ChartSampleData data, _) => data.x,
|
||||
yValueMapper: (ChartSampleData data, _) => data.y,
|
||||
dataLabelSettings: DataLabelSettings(isVisible: true)),
|
||||
ColumnSeries<ChartSampleData, int>(
|
||||
color: contentTheme.success,
|
||||
dataSource: controller.columnChart,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
|
||||
xValueMapper: (ChartSampleData data, _) => data.x,
|
||||
yValueMapper: (ChartSampleData data, _) => data.yValue,
|
||||
dataLabelSettings: DataLabelSettings(isVisible: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget audienceOverview() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Audience Overview", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
SizedBox(
|
||||
height: 318,
|
||||
child: SfCartesianChart(tooltipBehavior: controller.audienceOverview, series: <CartesianSeries>[
|
||||
BarSeries<ChartSampleData, dynamic>(
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.horizontal(right: Radius.circular(8)),
|
||||
dataSource: controller.audienceOverviewChart,
|
||||
xValueMapper: (ChartSampleData data, _) => data.x,
|
||||
yValueMapper: (ChartSampleData data, _) => data.y,
|
||||
width: 0.6,
|
||||
spacing: 0.3),
|
||||
BarSeries<ChartSampleData, dynamic>(
|
||||
borderRadius: BorderRadius.horizontal(right: Radius.circular(8)),
|
||||
dataSource: controller.audienceOverviewChart,
|
||||
color: Colors.teal,
|
||||
xValueMapper: (ChartSampleData data, _) => data.x,
|
||||
yValueMapper: (ChartSampleData data, _) => data.yValue,
|
||||
width: 0.6,
|
||||
spacing: 0.3)
|
||||
]))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTrafficSources() {
|
||||
Widget buildData(String browser, session, double process) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: MyText.bodyMedium(browser, fontWeight: 600, overflow: TextOverflow.ellipsis)),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
if (session >= 5000) Icon(LucideIcons.trending_up, size: 20, color: contentTheme.success),
|
||||
if (session < 5000) Icon(LucideIcons.trending_down, size: 20, color: contentTheme.danger),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: MyText.bodyMedium("$session", fontWeight: 600, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: MyProgressBar(
|
||||
progress: process,
|
||||
height: 4,
|
||||
radius: 4,
|
||||
inactiveColor: theme.dividerColor,
|
||||
activeColor: contentTheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 0,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.only(left: 23, top: 19),
|
||||
child: MyText.titleMedium("Traffic Sources", fontWeight: 600),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
Divider(height: 0),
|
||||
MySpacing.height(24),
|
||||
Padding(
|
||||
padding: MySpacing.only(left: 23),
|
||||
child: Row(children: [
|
||||
Expanded(child: MyText.bodyMedium("Browser", fontWeight: 600)),
|
||||
Expanded(child: MyText.bodyMedium("Sessions", fontWeight: 600)),
|
||||
Expanded(child: MyText.bodyMedium("Traffic", fontWeight: 600)),
|
||||
]),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
Divider(height: 0),
|
||||
Padding(
|
||||
padding: MySpacing.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildData("Google Chrome", 12000, .30),
|
||||
MySpacing.height(24),
|
||||
buildData("Apple Safari", 7000, .25),
|
||||
MySpacing.height(24),
|
||||
buildData("Microsoft Edge", 4500, .15),
|
||||
MySpacing.height(24),
|
||||
buildData("Mozilla Firefox", 8000, .22),
|
||||
MySpacing.height(24),
|
||||
buildData("Opera Browser", 3000, .18),
|
||||
MySpacing.height(24),
|
||||
buildData("Brave Browser", 2000, .12),
|
||||
MySpacing.height(24),
|
||||
buildData("Vivaldi Browser", 1500, .08),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildMostActiveUser() {
|
||||
Widget buildData(String image, name, emailID) {
|
||||
return MyContainer.bordered(
|
||||
child: Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
paddingAll: 0,
|
||||
height: 44,
|
||||
width: 44,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
child: Image.asset(image, fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(name, fontWeight: 600),
|
||||
MySpacing.height(4),
|
||||
MyText.labelMedium(
|
||||
emailID,
|
||||
fontWeight: 600,
|
||||
xMuted: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium("Most Active user", fontWeight: 600),
|
||||
MySpacing.height(20),
|
||||
SizedBox(
|
||||
height: 372,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
buildData(Images.avatars[0], "John Doe", "john.doe@example.com"),
|
||||
MySpacing.height(24),
|
||||
buildData(Images.avatars[1], "Emily Smith", "emily.smith@example.com"),
|
||||
MySpacing.height(24),
|
||||
buildData(Images.avatars[2], "Michael Johnson", "michael.johnson@example.com"),
|
||||
MySpacing.height(24),
|
||||
buildData(Images.avatars[3], "Olivia Williams", "olivia.williams@example.com"),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget buildVisitorsByCountry() {
|
||||
Widget buildData(String image, name, count) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
paddingAll: 0,
|
||||
height: 41,
|
||||
width: 41,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
child: Image.asset(
|
||||
image,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(name, fontWeight: 600, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
MyContainer(
|
||||
borderRadiusAll: 8,
|
||||
padding: MySpacing.xy(8, 8),
|
||||
color: Colors.brown.withAlpha(36),
|
||||
child: MyText.bodySmall(
|
||||
numberFormatter(count),
|
||||
fontWeight: 600,
|
||||
color: Colors.brown,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium("Visitor by country's", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
buildData('assets/country/united_states.png', "United State", "41560"),
|
||||
MySpacing.height(24),
|
||||
buildData('assets/country/argentina.png', "Argentina", "18400"),
|
||||
MySpacing.height(24),
|
||||
buildData('assets/country/germany.png', "Germany", "9000"),
|
||||
MySpacing.height(24),
|
||||
buildData('assets/country/mexico.png', "Mexico", "15325"),
|
||||
MySpacing.height(24),
|
||||
buildData('assets/country/russia.png', "Russia", "12222"),
|
||||
MySpacing.height(24),
|
||||
buildData('assets/country/canada.png', "Canada", "2040"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildVisitorByChannel() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Visitors By Channel", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
if (controller.visitorByChannel.isNotEmpty)
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
sortAscending: true,
|
||||
columnSpacing: 105,
|
||||
onSelectAll: (_) => {},
|
||||
headingRowColor: WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)),
|
||||
dataRowMaxHeight: 60,
|
||||
showBottomBorder: true,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
border: TableBorder.all(borderRadius: BorderRadius.circular(4), style: BorderStyle.solid, width: .4, color: Colors.grey),
|
||||
columns: [
|
||||
DataColumn(label: MyText.labelLarge('S.No', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Channel', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Session', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Bounce Rate', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Session Duration', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Target Reached', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Page Per Session', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Action', color: contentTheme.primary)),
|
||||
],
|
||||
rows: controller.visitorByChannel
|
||||
.mapIndexed((index, data) => DataRow(cells: [
|
||||
DataCell(MyText.bodyMedium('${data.id}')),
|
||||
DataCell(MyText.labelLarge(data.channel, overflow: TextOverflow.ellipsis, maxLines: 1)),
|
||||
DataCell(MyText.bodySmall('${data.session}', fontWeight: 600)),
|
||||
DataCell(MyText.bodySmall('${data.bounceRate}%', fontWeight: 600)),
|
||||
DataCell(MyText.bodySmall('${Utils.getDateTimeStringFromDateTime(data.sessionDuration)}', fontWeight: 600)),
|
||||
DataCell(
|
||||
MyContainer(
|
||||
borderRadiusAll: 4,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
padding: MySpacing.xy(8, 8),
|
||||
color: contentTheme.primary.withAlpha(32),
|
||||
child: MyText.bodySmall('${data.targetReached}', fontWeight: 600, color: contentTheme.primary),
|
||||
),
|
||||
),
|
||||
DataCell(MyText.bodyMedium('${data.pagePerSession}')),
|
||||
DataCell(SizedBox(
|
||||
width: 130,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyContainer(
|
||||
onTap: () => {},
|
||||
padding: MySpacing.xy(8, 8),
|
||||
color: contentTheme.primary.withAlpha(36),
|
||||
child: Icon(LucideIcons.pencil, size: 14, color: contentTheme.primary),
|
||||
),
|
||||
MyContainer(
|
||||
onTap: () => {},
|
||||
padding: MySpacing.xy(8, 8),
|
||||
color: contentTheme.success.withAlpha(36),
|
||||
child: Icon(LucideIcons.pencil, size: 14, color: contentTheme.success),
|
||||
),
|
||||
MyContainer(
|
||||
onTap: () => controller.removeData(index),
|
||||
padding: MySpacing.xy(8, 8),
|
||||
color: contentTheme.danger.withAlpha(36),
|
||||
child: Icon(LucideIcons.trash_2, size: 14, color: contentTheme.danger),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
]))
|
||||
.toList()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,523 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/dashboard/crm_controller.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/utils/utils.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_list_extension.dart';
|
||||
import 'package:marco/helpers/widgets/my_progress_bar.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/images.dart';
|
||||
import 'package:marco/model/chart_model.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class CrmScreen extends StatefulWidget {
|
||||
const CrmScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CrmScreen> createState() => _CrmScreenState();
|
||||
}
|
||||
|
||||
class _CrmScreenState extends State<CrmScreen> with UIMixin {
|
||||
CrmController controller = Get.put(CrmController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder(
|
||||
init: controller,
|
||||
tag: 'crm_dashboard_controller',
|
||||
builder: (controller) {
|
||||
return Layout(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Crm", fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Crm', active: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(
|
||||
children: [
|
||||
MyFlexItem(
|
||||
sizes: "lg-3 md-6 sm-6",
|
||||
child:
|
||||
stats(LucideIcons.circle_dollar_sign, "Nominal Balance", "\$5,780", "Nominal Balance last month", "\$6,290", contentTheme.primary)),
|
||||
MyFlexItem(
|
||||
sizes: "lg-3 md-6 sm-6",
|
||||
child: stats(LucideIcons.package, "Total Stock Product", "5.264", "Total Stock product last month", "2.546", contentTheme.secondary)),
|
||||
MyFlexItem(
|
||||
sizes: "lg-3 md-6 sm-6",
|
||||
child: stats(LucideIcons.file_down, "Nominal Revenue", "5.264", "Total revenue last month", "2.546", contentTheme.success)),
|
||||
MyFlexItem(
|
||||
sizes: "lg-3 md-6 sm-6",
|
||||
child: stats(LucideIcons.file_up, "Nominal Expenses", "\$19,644", "Total expenses last month", "\$18,946", contentTheme.warning)),
|
||||
MyFlexItem(sizes: 'lg-9', child: revenueForecast()),
|
||||
MyFlexItem(sizes: 'lg-3', child: topDeals()),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: dealSource()),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: leadResponse()),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: openDeals()),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: leadSource()),
|
||||
MyFlexItem(child: leadReport()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget stats(IconData icon, String title, String subTitle, String statsMonthName, String statsMonthRevenue, Color color) {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
MyContainer.bordered(paddingAll: 8, borderColor: color, child: Icon(icon, size: 16, color: color)),
|
||||
MySpacing.width(12),
|
||||
MyText.bodyMedium(title, fontWeight: 600),
|
||||
],
|
||||
),
|
||||
MyText.titleMedium(subTitle, fontWeight: 600),
|
||||
Row(
|
||||
children: [
|
||||
MyText.labelMedium(statsMonthName, fontWeight: 600, xMuted: true),
|
||||
MySpacing.width(8),
|
||||
Expanded(child: MyText.labelMedium(statsMonthRevenue, fontWeight: 600, maxLines: 1)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget revenueForecast() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium("Revenue Forecast", fontWeight: 600),
|
||||
PopupMenuButton(
|
||||
offset: Offset(0, 20),
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
shape: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none),
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(padding: MySpacing.xy(16, 8), height: 10, child: MyText.bodySmall("Download", fontWeight: 600)),
|
||||
PopupMenuItem(padding: MySpacing.xy(16, 8), height: 10, child: MyText.bodySmall("Import", fontWeight: 600)),
|
||||
PopupMenuItem(padding: MySpacing.xy(16, 8), height: 10, child: MyText.bodySmall("Export", fontWeight: 600)),
|
||||
],
|
||||
child: Icon(LucideIcons.ellipsis_vertical, size: 20),
|
||||
)
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: const CategoryAxis(
|
||||
majorGridLines: MajorGridLines(width: 0),
|
||||
),
|
||||
margin: MySpacing.zero,
|
||||
primaryYAxis: const NumericAxis(maximum: 20, minimum: 0, interval: 4, axisLine: AxisLine(width: 0), majorTickLines: MajorTickLines(size: 0)),
|
||||
series: [
|
||||
ColumnSeries<ChartSampleData, String>(
|
||||
width: 0.8,
|
||||
spacing: 0.2,
|
||||
dataSource: controller.chartData,
|
||||
color: contentTheme.primary,
|
||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
||||
yValueMapper: (ChartSampleData sales, _) => sales.y,
|
||||
name: 'Sales Revenue'),
|
||||
ColumnSeries<ChartSampleData, String>(
|
||||
dataSource: controller.chartData,
|
||||
width: 0.8,
|
||||
spacing: 0.2,
|
||||
color: contentTheme.secondary,
|
||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
||||
yValueMapper: (ChartSampleData sales, _) => sales.secondSeriesYValue,
|
||||
name: 'Product Cost'),
|
||||
],
|
||||
legend: Legend(isVisible: true, position: LegendPosition.bottom),
|
||||
tooltipBehavior: controller.tooltipBehavior,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget topDeals() {
|
||||
Widget topDealsWidget(String image, String name, String email, String price) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
paddingAll: 0,
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Image.asset(image, fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(name, fontWeight: 600),
|
||||
MyText.labelMedium(email, fontWeight: 600, muted: true, maxLines: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyText.labelMedium(price, fontWeight: 600),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium("Top Deals", fontWeight: 600),
|
||||
PopupMenuButton(
|
||||
offset: Offset(0, 20),
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
shape: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none),
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(padding: MySpacing.xy(16, 8), height: 10, child: MyText.bodySmall("Week", fontWeight: 600)),
|
||||
PopupMenuItem(padding: MySpacing.xy(16, 8), height: 10, child: MyText.bodySmall("Month", fontWeight: 600)),
|
||||
PopupMenuItem(padding: MySpacing.xy(16, 8), height: 10, child: MyText.bodySmall("Year", fontWeight: 600)),
|
||||
],
|
||||
child: Icon(LucideIcons.ellipsis_vertical, size: 20),
|
||||
)
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
topDealsWidget(Images.avatars[0], "Christopher", "christopher123@gmail.com", "\$14,541"),
|
||||
MySpacing.height(24),
|
||||
topDealsWidget(Images.avatars[1], "Edward", "edward15@gmail.com", "\$21,548"),
|
||||
MySpacing.height(24),
|
||||
topDealsWidget(Images.avatars[2], "Michael", "michael@gmail.com", "\$13,645"),
|
||||
MySpacing.height(24),
|
||||
topDealsWidget(Images.avatars[3], "Sebastian", "sebastian@gmail.com", "\$51,254"),
|
||||
MySpacing.height(24),
|
||||
topDealsWidget(Images.avatars[4], "Nicholas", "nicholas@gmail.com", "\$15,487"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget dealSource() {
|
||||
Widget dealSourceWidget(String image, String title, String subtitle, String totalLeads) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
height: 32,
|
||||
width: 32,
|
||||
paddingAll: 0,
|
||||
child: Image.asset(image, fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(title, fontWeight: 600),
|
||||
MyText.bodySmall(subtitle),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyText.labelSmall('$totalLeads Leads', fontWeight: 600),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Deal Source", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
dealSourceWidget("assets/social/uxerflow_logo.png", "Website", "userflow.com", "50"),
|
||||
MySpacing.height(24),
|
||||
dealSourceWidget("assets/social/dribbble-logo.png", "Dribbble", "dribbble.com", "50"),
|
||||
MySpacing.height(24),
|
||||
dealSourceWidget("assets/social/facebook-logo.png", "Facebook", "facebook.com", "50"),
|
||||
MySpacing.height(24),
|
||||
dealSourceWidget("assets/social/instagram-logo.png", "Instagram", "instagram.com", "50"),
|
||||
MySpacing.height(24),
|
||||
dealSourceWidget("assets/social/LinkedIn-logo.png", "Linkedin", "linkedin.com", "50"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget leadResponse() {
|
||||
Widget leadData(String image, name, processRate, double progress) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
paddingAll: 0,
|
||||
height: 32,
|
||||
width: 32,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
child: Image.asset(image, fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(24),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.labelLarge(name, fontWeight: 600, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
MyText.labelSmall(processRate, fontWeight: 600, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
MySpacing.height(8),
|
||||
MyProgressBar(
|
||||
width: 300,
|
||||
height: 7,
|
||||
progress: progress,
|
||||
radius: 8,
|
||||
activeColor: contentTheme.primary,
|
||||
inactiveColor: contentTheme.primary.withAlpha(32),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Lead Response", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
leadData(Images.avatars[0], "Amelia Thomson", "1.3", .3),
|
||||
MySpacing.height(24),
|
||||
leadData(Images.avatars[1], "Ian Ferguson", "1.4", .4),
|
||||
MySpacing.height(24),
|
||||
leadData(Images.avatars[2], "Simon Ross", "2", .8),
|
||||
MySpacing.height(24),
|
||||
leadData(Images.avatars[3], "Heather", "1.5", .5),
|
||||
MySpacing.height(24),
|
||||
leadData(Images.avatars[4], "Madeleine Simpson", "1.9", .7),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget openDeals() {
|
||||
Widget dealsData(String image, dealsType, dealsDate, price) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
paddingAll: 0,
|
||||
height: 46,
|
||||
width: 46,
|
||||
borderRadiusAll: 8,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
child: Image.asset(image, fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(24),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 32,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium(dealsType, fontWeight: 600, overflow: TextOverflow.ellipsis),
|
||||
MyText.labelSmall("Closing deal date ${dealsDate}", fontWeight: 600, overflow: TextOverflow.ellipsis)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
MyText.labelMedium("\$${numberFormatter(price)}", fontWeight: 600, color: contentTheme.primary),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Open Deals", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
dealsData(Images.avatars[1], "SASS app workflow", "26 Jan", "15478"),
|
||||
MySpacing.height(24),
|
||||
dealsData(Images.avatars[0], "Create new component", "8 Fab", "54791"),
|
||||
MySpacing.height(24),
|
||||
dealsData(Images.avatars[3], "New Email Design Template", "16 March", "54876"),
|
||||
MySpacing.height(24),
|
||||
dealsData(Images.avatars[4], "React Developer", "12 Fab", "1564"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget leadSource() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Lead Source", fontWeight: 600),
|
||||
SizedBox(
|
||||
height: 280,
|
||||
child: SfCircularChart(
|
||||
series: [
|
||||
PieSeries<ChartSampleData, String>(
|
||||
explode: true,
|
||||
explodeIndex: 0,
|
||||
dataSource: <ChartSampleData>[
|
||||
ChartSampleData(x: 'Prospecting', y: 13, text: 'Prospecting \n 13%'),
|
||||
ChartSampleData(x: 'Negotiation', y: 24, text: 'Negotiation \n 24%'),
|
||||
ChartSampleData(x: 'Proposal', y: 25, text: 'Proposal \n 25%'),
|
||||
ChartSampleData(x: 'Qualification', y: 38, text: 'Qualification \n 38%'),
|
||||
],
|
||||
xValueMapper: (ChartSampleData data, _) => data.x as String,
|
||||
yValueMapper: (ChartSampleData data, _) => data.y,
|
||||
dataLabelMapper: (ChartSampleData data, _) => data.text,
|
||||
dataLabelSettings: DataLabelSettings(isVisible: true)),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget leadReport() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Lead Report", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
if (controller.leadReport.isNotEmpty)
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
sortAscending: true,
|
||||
columnSpacing: 80,
|
||||
onSelectAll: (_) => {},
|
||||
headingRowColor: WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)),
|
||||
dataRowMaxHeight: 60,
|
||||
showBottomBorder: true,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
border: TableBorder.all(borderRadius: BorderRadius.circular(4), style: BorderStyle.solid, width: .4, color: Colors.grey),
|
||||
columns: [
|
||||
DataColumn(label: MyText.labelLarge('S.No', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Lead', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Company Name', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Phone Number', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Status', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Location', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Date', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Amount', color: contentTheme.primary)),
|
||||
],
|
||||
rows: controller.leadReport
|
||||
.mapIndexed((index, data) => DataRow(cells: [
|
||||
DataCell(MyText.bodyMedium("#${data.id}", fontWeight: 600)),
|
||||
DataCell(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
MyContainer(
|
||||
height: 44,
|
||||
width: 44,
|
||||
paddingAll: 0,
|
||||
child: Image.asset(Images.avatars[index % Images.avatars.length], fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(24),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(data.firstName, fontWeight: 600),
|
||||
MyText.labelMedium(data.email),
|
||||
],
|
||||
)
|
||||
],
|
||||
)),
|
||||
DataCell(MyText.bodyMedium(data.companyName, fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium(data.phoneNumber, fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium(data.status, fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium(data.location, fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium("${Utils.getDateStringFromDateTime(data.date)}", fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium("\$${data.amount}", fontWeight: 600)),
|
||||
]))
|
||||
.toList()),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -1,533 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/controller/dashboard/crypto_controller.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/utils/utils.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_list_extension.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/model/chart_model.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class CryptoScreen extends StatefulWidget {
|
||||
const CryptoScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CryptoScreen> createState() => _CryptoScreenState();
|
||||
}
|
||||
|
||||
class _CryptoScreenState extends State<CryptoScreen> with UIMixin {
|
||||
CryptoController controller = Get.put(CryptoController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder(
|
||||
init: controller,
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Crypto", fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Crypto', active: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(
|
||||
children: [
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: stats('assets/coin/ethereum.png', 'ETH', 'Ethereum', '3.754120', '4.2')),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: stats('assets/coin/bitcoin.png', 'BTC', 'Bitcoin', '12.125620', '-1.3')),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: stats('assets/coin/chainlink.png', 'LINK', 'Chainlink', '15.874562', '0.8')),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: stats('assets/coin/dogecoin.png', 'DOGE', 'Dogecoin', '8.674930', '5.0')),
|
||||
MyFlexItem(sizes: 'lg-6 md-6', child: marketOverview()),
|
||||
MyFlexItem(sizes: 'lg-6 md-6', child: cryptoStatistics()),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: accountStats("Total Balance", "\$12000.50", LucideIcons.credit_card, contentTheme.success)),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: accountStats("Total Investment", "\$8000.75", LucideIcons.trending_up, contentTheme.info)),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: accountStats("Total Change", "\$500.25", LucideIcons.refresh_cw, contentTheme.warning)),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: accountStats("Day Change", "\$20.30", LucideIcons.circle_arrow_up, contentTheme.danger)),
|
||||
MyFlexItem(sizes: 'lg-4', child: recentActivity()),
|
||||
MyFlexItem(sizes: 'lg-4', child: topPerformers()),
|
||||
MyFlexItem(sizes: 'lg-4', child: transactionHistory()),
|
||||
MyFlexItem(child: activeOverallGrowth()),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget stats(String coinImage, String coinShortName, String coinName, String count, String change) {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: .2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
height: 170,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
height: 44,
|
||||
width: 44,
|
||||
paddingAll: 0,
|
||||
child: Image.asset(coinImage, fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(20),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodySmall(coinShortName, xMuted: true),
|
||||
MyText.bodySmall(coinName),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
MyText.titleLarge('$count $coinShortName'),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(change.startsWith("-") ? LucideIcons.trending_down : LucideIcons.trending_up,
|
||||
size: 16, color: change.startsWith("-") ? theme.colorScheme.error : theme.colorScheme.primary),
|
||||
MySpacing.width(8),
|
||||
MyText.bodySmall("${change.startsWith("-") ? '' : '+'}${change}%",
|
||||
color: change.startsWith("-") ? theme.colorScheme.error : theme.colorScheme.primary),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget marketOverview() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: .2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium("Market Overview", fontWeight: 600),
|
||||
PopupMenuButton(
|
||||
onSelected: controller.onSelectIntervalType,
|
||||
itemBuilder: (BuildContext context) {
|
||||
return DateTimeIntervalType.values.map((behavior) {
|
||||
return PopupMenuItem(
|
||||
value: behavior,
|
||||
height: 32,
|
||||
child: MyText.bodySmall(
|
||||
behavior.toString().split('.').last.capitalize.toString(),
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontWeight: 600,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
color: theme.cardTheme.color,
|
||||
child: MyContainer.bordered(
|
||||
padding: MySpacing.xy(8, 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
MyText.labelSmall(controller.intervalType.toString().split('.').last.capitalize.toString(), color: theme.colorScheme.onSurface),
|
||||
Icon(LucideIcons.chevron_down, size: 16, color: theme.colorScheme.onSurface)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: DateTimeAxis(
|
||||
autoScrollingMode: AutoScrollingMode.start,
|
||||
dateFormat: DateFormat.MMM(),
|
||||
intervalType: controller.intervalType,
|
||||
minimum: DateTime(2016),
|
||||
maximum: DateTime(2016, 10),
|
||||
majorGridLines: const MajorGridLines(width: 0)),
|
||||
primaryYAxis: const NumericAxis(minimum: 80, maximum: 120, labelFormat: r'${value}', axisLine: AxisLine(width: 0)),
|
||||
series: _getCandleSeries(),
|
||||
trackballBehavior: controller.trackballBehavior,
|
||||
tooltipBehavior: TooltipBehavior(),
|
||||
zoomPanBehavior: ZoomPanBehavior(enableMouseWheelZooming: true, enablePinching: true, enablePanning: true, enableDoubleTapZooming: true)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<CandleSeries<ChartSampleData, DateTime>> _getCandleSeries() {
|
||||
return <CandleSeries<ChartSampleData, DateTime>>[
|
||||
CandleSeries<ChartSampleData, DateTime>(
|
||||
enableSolidCandles: controller.enableSolidCandle,
|
||||
dataSource: <ChartSampleData>[
|
||||
ChartSampleData(x: DateTime(2016, 01, 11), open: 98.97, high: 101.19, low: 95.36, close: 97.13),
|
||||
ChartSampleData(x: DateTime(2016, 01, 18), open: 98.41, high: 101.46, low: 93.42, close: 101.42),
|
||||
ChartSampleData(x: DateTime(2016, 01, 25), open: 101.52, high: 101.53, low: 92.39, close: 97.34),
|
||||
ChartSampleData(x: DateTime(2016, 02), open: 96.47, high: 97.33, low: 93.69, close: 94.02),
|
||||
ChartSampleData(x: DateTime(2016, 02, 08), open: 93.13, high: 96.35, low: 92.59, close: 93.99),
|
||||
ChartSampleData(x: DateTime(2016, 02, 15), open: 95.02, high: 98.89, low: 94.61, close: 96.04),
|
||||
ChartSampleData(x: DateTime(2016, 02, 22), open: 96.31, high: 98.0237, low: 93.32, close: 96.91),
|
||||
ChartSampleData(x: DateTime(2016, 02, 29), open: 96.86, high: 103.75, low: 96.65, close: 103.01),
|
||||
ChartSampleData(x: DateTime(2016, 03, 07), open: 102.39, high: 102.83, low: 100.15, close: 102.26),
|
||||
ChartSampleData(x: DateTime(2016, 03, 14), open: 106.5, high: 106.5, low: 106.5, close: 106.5),
|
||||
ChartSampleData(x: DateTime(2016, 03, 21), open: 105.93, high: 107.65, low: 104.89, close: 105.67),
|
||||
ChartSampleData(x: DateTime(2016, 03, 28), open: 106, high: 110.42, low: 104.88, close: 109.99),
|
||||
ChartSampleData(x: DateTime(2016, 04, 04), open: 110.42, high: 112.19, low: 108.121, close: 108.66),
|
||||
ChartSampleData(x: DateTime(2016, 04, 11), open: 108.97, high: 112.39, low: 108.66, close: 109.85),
|
||||
ChartSampleData(x: DateTime(2016, 04, 18), open: 108.89, high: 108.95, low: 104.62, close: 105.68),
|
||||
ChartSampleData(x: DateTime(2016, 04, 25), open: 105, high: 105.65, low: 92.51, close: 93.74),
|
||||
ChartSampleData(x: DateTime(2016, 05, 02), open: 93.965, high: 95.9, low: 91.85, close: 92.72),
|
||||
ChartSampleData(x: DateTime(2016, 05, 09), open: 93, high: 93.77, low: 89.47, close: 90.52),
|
||||
ChartSampleData(x: DateTime(2016, 05, 16), open: 92.39, high: 95.43, low: 91.65, close: 95.22),
|
||||
ChartSampleData(x: DateTime(2016, 05, 23), open: 95.87, high: 100.73, low: 95.67, close: 100.35),
|
||||
ChartSampleData(x: DateTime(2016, 05, 30), open: 99.6, high: 100.4, low: 96.63, close: 97.92),
|
||||
ChartSampleData(x: DateTime(2016, 06, 06), open: 97.99, high: 101.89, low: 97.55, close: 98.83),
|
||||
ChartSampleData(x: DateTime(2016, 06, 13), open: 98.69, high: 99.12, low: 95.3, close: 95.33),
|
||||
ChartSampleData(x: DateTime(2016, 06, 20), open: 96, high: 96.89, low: 92.65, close: 93.4),
|
||||
ChartSampleData(x: DateTime(2016, 06, 27), open: 93, high: 96.465, low: 91.5, close: 95.89),
|
||||
ChartSampleData(x: DateTime(2016, 07, 04), open: 95.39, high: 96.89, low: 94.37, close: 96.68),
|
||||
ChartSampleData(x: DateTime(2016, 07, 11), open: 96.75, high: 99.3, low: 96.73, close: 98.78),
|
||||
ChartSampleData(x: DateTime(2016, 07, 18), open: 98.7, high: 101, low: 98.31, close: 98.66),
|
||||
ChartSampleData(x: DateTime(2016, 07, 25), open: 98.25, high: 104.55, low: 96.42, close: 104.21),
|
||||
ChartSampleData(x: DateTime(2016, 08), open: 104.41, high: 107.65, low: 104, close: 107.48),
|
||||
ChartSampleData(x: DateTime(2016, 08, 08), open: 107.52, high: 108.94, low: 107.16, close: 108.18),
|
||||
ChartSampleData(x: DateTime(2016, 08, 15), open: 108.14, high: 110.23, low: 108.08, close: 109.36),
|
||||
ChartSampleData(x: DateTime(2016, 08, 22), open: 108.86, high: 109.32, low: 106.31, close: 106.94),
|
||||
ChartSampleData(x: DateTime(2016, 08, 29), open: 109.74, high: 109.74, low: 109.74, close: 109.74),
|
||||
ChartSampleData(x: DateTime(2016, 09, 05), open: 107.9, high: 108.76, low: 103.13, close: 103.13),
|
||||
ChartSampleData(x: DateTime(2016, 09, 12), open: 102.65, high: 116.13, low: 102.53, close: 114.92),
|
||||
ChartSampleData(x: DateTime(2016, 09, 19), open: 115.19, high: 116.18, low: 111.55, close: 112.71),
|
||||
ChartSampleData(x: DateTime(2016, 09, 26), open: 111.64, high: 114.64, low: 111.55, close: 113.05),
|
||||
],
|
||||
showIndicationForSameValues: true,
|
||||
xValueMapper: (ChartSampleData sales, _) => sales.x as DateTime,
|
||||
lowValueMapper: (ChartSampleData sales, _) => sales.low,
|
||||
highValueMapper: (ChartSampleData sales, _) => sales.high,
|
||||
openValueMapper: (ChartSampleData sales, _) => sales.open,
|
||||
closeValueMapper: (ChartSampleData sales, _) => sales.close,
|
||||
spacing: 0.2,
|
||||
width: 0.8,
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
Widget cryptoStatistics() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: .2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Crypto Statistics", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
SizedBox(
|
||||
height: 329,
|
||||
child: SfCartesianChart(
|
||||
margin: MySpacing.zero,
|
||||
plotAreaBorderWidth: 0,
|
||||
legend: Legend(isVisible: false, position: LegendPosition.bottom),
|
||||
primaryXAxis: const CategoryAxis(majorGridLines: MajorGridLines(width: 0), labelPlacement: LabelPlacement.onTicks),
|
||||
primaryYAxis: const NumericAxis(
|
||||
axisLine: AxisLine(width: 0), edgeLabelPlacement: EdgeLabelPlacement.shift, labelFormat: '{value}', majorTickLines: MajorTickLines(size: 0)),
|
||||
series: [
|
||||
SplineSeries<ChartSampleData, String>(
|
||||
dataSource: controller.chartData,
|
||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
||||
yValueMapper: (ChartSampleData sales, _) => sales.y,
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
color: contentTheme.success,
|
||||
name: 'High'),
|
||||
SplineSeries<ChartSampleData, String>(
|
||||
dataSource: controller.chartData,
|
||||
name: 'Low',
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
||||
yValueMapper: (ChartSampleData sales, _) => sales.secondSeriesYValue,
|
||||
)
|
||||
],
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget accountStats(String title, String data, IconData icon, Color color) {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: .2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
color: color,
|
||||
child: Icon(icon, color: contentTheme.light),
|
||||
),
|
||||
MySpacing.width(20),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(title, fontWeight: 600, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
MySpacing.height(4),
|
||||
MyText.titleLarge(data, fontWeight: 600, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget recentActivity() {
|
||||
Widget recentActivityWidget(String coinName, String transactionType, String price, String transactionUpDown, IconData icon) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyContainer.roundBordered(
|
||||
height: 44,
|
||||
width: 44,
|
||||
paddingAll: 0,
|
||||
child: Icon(icon, size: 20),
|
||||
),
|
||||
MySpacing.width(20),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(coinName, fontWeight: 600),
|
||||
MySpacing.height(4),
|
||||
MyText.bodySmall(transactionType, fontWeight: 600, muted: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
MyText.bodyMedium(price, fontWeight: 600),
|
||||
MySpacing.height(4),
|
||||
MyText.bodySmall("${transactionUpDown.startsWith('-') ? '' : '+'}${transactionUpDown}",
|
||||
fontWeight: 600, muted: true, color: transactionUpDown.startsWith('-') ? contentTheme.danger : contentTheme.success),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: .2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Recent Activity", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget("Bought Ethereum", "MasterCard ***8", "+0.215 BTC", "4320.22 USD", LucideIcons.circle_plus),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget("Sold Bitcoin", "PayPal Account", "-0.012 BTC", "-231.56 USD", LucideIcons.circle_minus),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget("Transferred Litecoin", "Visa Debit Card ***5", "-2.23 LTC", "-150.99 USD", LucideIcons.arrow_right),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget("Bought Cardano", "Bitcoin Wallet", "+500 ADA", "2,650.32 USD", LucideIcons.circle_plus),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget("Sold Dogecoin", "Bank Transfer", "-10,000 DOGE", "-756.11 USD", LucideIcons.circle_minus),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget topPerformers() {
|
||||
Widget topPerformersWidget(String coinName, String shortName, String price, String image) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
height: 44,
|
||||
width: 44,
|
||||
paddingAll: 0,
|
||||
child: Image.asset(image),
|
||||
),
|
||||
MySpacing.width(24),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(coinName, fontWeight: 600),
|
||||
MySpacing.height(4),
|
||||
MyText.bodySmall(shortName, fontWeight: 600, muted: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyText.bodyMedium(price, fontWeight: 600),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: .2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Top Performance", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
topPerformersWidget("Bitcoin", "BTC", "\$45,000", "assets/coin/bitcoin.png"),
|
||||
MySpacing.height(24),
|
||||
topPerformersWidget("Chainlink", "LINK", "\$25.50", "assets/coin/chainlink.png"),
|
||||
MySpacing.height(24),
|
||||
topPerformersWidget("Dogecoin", "DOGE", "\$0.25", "assets/coin/dogecoin.png"),
|
||||
MySpacing.height(24),
|
||||
topPerformersWidget("Ethereum", "ETH", "\$3,200", "assets/coin/ethereum.png"),
|
||||
MySpacing.height(24),
|
||||
topPerformersWidget("Polkadot", "DOT", "\$12.75", "assets/coin/polkadot.png"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget transactionHistory() {
|
||||
Widget transactionHistoryWidget(String coinName, String date, String buyPrice, IconData icon) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
height: 44,
|
||||
width: 44,
|
||||
paddingAll: 0,
|
||||
color: contentTheme.primary.withValues(alpha: 0.2),
|
||||
child: Icon(icon, color: contentTheme.primary),
|
||||
),
|
||||
MySpacing.width(24),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(coinName, fontWeight: 600),
|
||||
MySpacing.height(4),
|
||||
MyText.bodySmall(date, fontWeight: 600, muted: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyText.bodyMedium(buyPrice, fontWeight: 600),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: .2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Transaction History", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
transactionHistoryWidget("Sent BTC", "12 November 2024 2:15 PM", "0.045 BTC", LucideIcons.arrow_up),
|
||||
MySpacing.height(24),
|
||||
transactionHistoryWidget("Received ETH", "11 November 2024 9:30 AM", "2.5 ETH", LucideIcons.arrow_down),
|
||||
MySpacing.height(24),
|
||||
transactionHistoryWidget("Sent LTC", "10 November 2024 5:20 PM", "1.2 LTC", LucideIcons.arrow_up),
|
||||
MySpacing.height(24),
|
||||
transactionHistoryWidget("Received ADA", "9 November 2024 11:50 PM", "500 ADA", LucideIcons.arrow_down),
|
||||
MySpacing.height(24),
|
||||
transactionHistoryWidget("Sent DOGE", "8 November 2024 1:10 PM", "10,000 DOGE", LucideIcons.arrow_up),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget activeOverallGrowth() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: .2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Active Overall Growth", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
sortAscending: true,
|
||||
columnSpacing: 170,
|
||||
onSelectAll: (_) => {},
|
||||
headingRowColor: WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)),
|
||||
dataRowMaxHeight: 60,
|
||||
showBottomBorder: true,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
border: TableBorder.all(borderRadius: BorderRadius.circular(4), style: BorderStyle.solid, width: .4, color: Colors.grey),
|
||||
columns: [
|
||||
DataColumn(label: MyText.labelLarge('Type', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Assets', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Date', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('IP Address', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Status', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Amount', color: contentTheme.primary)),
|
||||
],
|
||||
rows: controller.coinGrowth
|
||||
.mapIndexed((index, data) => DataRow(cells: [
|
||||
DataCell(MyText.labelMedium('Exchange')),
|
||||
DataCell(MyText.labelMedium('${data.asset}')),
|
||||
DataCell(MyText.labelMedium('${Utils.getDateTimeStringFromDateTime(data.date)}')),
|
||||
DataCell(MyText.labelMedium('${data.ipAddress}')),
|
||||
DataCell(MyContainer(
|
||||
paddingAll: 4,
|
||||
color: data.status == 'Success' ? contentTheme.success : contentTheme.danger,
|
||||
child: MyText.labelMedium('${data.status}', color: data.status == 'Success' ? contentTheme.onSuccess : contentTheme.onDanger))),
|
||||
DataCell(MyText.labelMedium('\$${data.amount}')),
|
||||
]))
|
||||
.toList()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,437 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_loading_component.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_wrapper.dart';
|
||||
import 'package:marco/model/my_paginated_table.dart';
|
||||
import 'package:marco/controller/dashboard/daily_task_controller.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
|
||||
class DailyTaskScreen extends StatefulWidget {
|
||||
const DailyTaskScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DailyTaskScreen> createState() => _DailyTaskScreenState();
|
||||
}
|
||||
|
||||
class _DailyTaskScreenState extends State<DailyTaskScreen> with UIMixin {
|
||||
final DailyTaskController dailyTaskController =
|
||||
Get.put(DailyTaskController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: Obx(() {
|
||||
return LoadingComponent(
|
||||
isLoading: dailyTaskController.isLoading.value,
|
||||
loadingText: 'Loading Tasks...',
|
||||
child: GetBuilder<DailyTaskController>(
|
||||
init: dailyTaskController,
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
MySpacing.height(flexSpacing),
|
||||
_buildBreadcrumb(),
|
||||
MySpacing.height(flexSpacing),
|
||||
_buildFilterSection(),
|
||||
MySpacing.height(flexSpacing),
|
||||
_buildTaskList(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: MyText.titleMedium(
|
||||
"Daily Progress Report",
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBreadcrumb() {
|
||||
return Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Daily Progress Report', active: true),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterSection() {
|
||||
return Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildProjectFilter(),
|
||||
const SizedBox(width: 10),
|
||||
_buildDateRangeButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProjectFilter() {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: PopupMenuButton<String>(
|
||||
onSelected: (String value) async {
|
||||
if (value.isNotEmpty) {
|
||||
dailyTaskController.selectedProjectId = value;
|
||||
await dailyTaskController.fetchTaskData(value);
|
||||
}
|
||||
dailyTaskController.update();
|
||||
},
|
||||
itemBuilder: (BuildContext context) {
|
||||
return dailyTaskController.projects
|
||||
.map<PopupMenuItem<String>>((project) {
|
||||
return PopupMenuItem<String>(
|
||||
value: project.id,
|
||||
child: MyText.bodySmall(project.name),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
offset: const Offset(0, 40),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
dailyTaskController.selectedProjectId == null
|
||||
? dailyTaskController.projects.isNotEmpty
|
||||
? dailyTaskController.projects.first.name
|
||||
: 'No Tasks'
|
||||
: dailyTaskController.projects
|
||||
.firstWhere((project) =>
|
||||
project.id ==
|
||||
dailyTaskController.selectedProjectId)
|
||||
.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildDateRangeButton() {
|
||||
String dateRangeText;
|
||||
if (dailyTaskController.startDateTask != null &&
|
||||
dailyTaskController.endDateTask != null) {
|
||||
dateRangeText =
|
||||
'${DateFormat('dd-MM-yyyy').format(dailyTaskController.startDateTask!)}'
|
||||
' to '
|
||||
'${DateFormat('dd-MM-yyyy').format(dailyTaskController.endDateTask!)}';
|
||||
} else {
|
||||
dateRangeText = "Select Date Range";
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.date_range),
|
||||
label: Text(dateRangeText),
|
||||
onPressed: () => dailyTaskController.selectDateRangeForTaskData(
|
||||
context,
|
||||
dailyTaskController,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTaskList() {
|
||||
return Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(
|
||||
children: [
|
||||
MyFlexItem(sizes: 'lg-6', child: employeeListTab()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget employeeListTab() {
|
||||
if (dailyTaskController.dailyTasks.isEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodySmall("No Tasks Assigned to This Project",
|
||||
fontWeight: 600),
|
||||
);
|
||||
}
|
||||
Map<String, List<dynamic>> groupedTasks = {};
|
||||
for (var task in dailyTaskController.dailyTasks) {
|
||||
String dateKey =
|
||||
DateFormat('dd-MM-yyyy').format(task.assignmentDate);
|
||||
groupedTasks.putIfAbsent(dateKey, () => []).add(task);
|
||||
}
|
||||
|
||||
// Sort dates descending (latest first)
|
||||
final sortedEntries = groupedTasks.entries.toList()
|
||||
..sort((a, b) => DateFormat('dd-MM-yyyy')
|
||||
.parse(b.key)
|
||||
.compareTo(DateFormat('dd-MM-yyyy').parse(a.key)));
|
||||
|
||||
// Flatten grouped data into one list with optional visual separators
|
||||
List<DataRow> allRows = [];
|
||||
|
||||
for (var entry in sortedEntries) {
|
||||
allRows.add(
|
||||
DataRow(
|
||||
color: WidgetStateProperty.all(Colors.grey.shade200),
|
||||
cells: [
|
||||
DataCell(MyText.titleSmall('Date: ${entry.key}')),
|
||||
DataCell(MyText.titleSmall('')),
|
||||
DataCell(MyText.titleSmall('')),
|
||||
DataCell(MyText.titleSmall('')),
|
||||
DataCell(MyText.titleSmall('')),
|
||||
DataCell(MyText.titleSmall('')),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
allRows.addAll(entry.value.map((task) => _buildRow(task)));
|
||||
}
|
||||
|
||||
return MyRefreshableContent(
|
||||
onRefresh: () async {
|
||||
if (dailyTaskController.selectedProjectId != null) {
|
||||
await dailyTaskController
|
||||
.fetchTaskData(dailyTaskController.selectedProjectId!);
|
||||
}
|
||||
},
|
||||
child: MyPaginatedTable(
|
||||
columns: _buildColumns(),
|
||||
rows: allRows,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<DataColumn> _buildColumns() {
|
||||
return [
|
||||
DataColumn(
|
||||
label: MyText.labelLarge('Activity', color: contentTheme.primary)),
|
||||
DataColumn(
|
||||
label: MyText.labelLarge('Assigned', color: contentTheme.primary)),
|
||||
DataColumn(
|
||||
label: MyText.labelLarge('Completed', color: contentTheme.primary)),
|
||||
DataColumn(
|
||||
label: MyText.labelLarge('Assigned On', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Team', color: contentTheme.primary)),
|
||||
DataColumn(
|
||||
label: MyText.labelLarge('Actions', color: contentTheme.primary)),
|
||||
];
|
||||
}
|
||||
|
||||
DataRow _buildRow(dynamic task) {
|
||||
final workItem = task.workItem;
|
||||
final location = [
|
||||
workItem?.workArea?.floor?.building?.name,
|
||||
workItem?.workArea?.floor?.floorName,
|
||||
workItem?.workArea?.areaName
|
||||
].where((e) => e != null && e.isNotEmpty).join(' > ');
|
||||
|
||||
return DataRow(cells: [
|
||||
DataCell(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MyText.bodyMedium(workItem?.activityMaster?.activityName ?? 'N/A',
|
||||
fontWeight: 600),
|
||||
SizedBox(height: 2),
|
||||
MyText.bodySmall(location, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
MyText.bodyMedium(
|
||||
'${task.plannedTask ?? "NA"} / '
|
||||
'${(workItem?.plannedWork != null && workItem?.completedWork != null) ? (workItem!.plannedWork! - workItem.completedWork!) : "NA"}',
|
||||
),
|
||||
),
|
||||
DataCell(MyText.bodyMedium(task.completedTask.toString())),
|
||||
DataCell(MyText.bodyMedium(DateFormat('dd-MM-yyyy')
|
||||
.format(DateTime.parse(task.assignmentDate)))),
|
||||
DataCell(_buildTeamCell(task)),
|
||||
DataCell(Row(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final activityName =
|
||||
task.workItem?.activityMaster?.activityName ?? 'N/A';
|
||||
final assigned = '${task.plannedTask ?? "NA"} / '
|
||||
'${(task.workItem?.plannedWork != null && task.workItem?.completedWork != null) ? (task.workItem!.plannedWork! - task.workItem.completedWork!) : "NA"}';
|
||||
final assignedBy =
|
||||
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
|
||||
final completed = task.completedTask.toString();
|
||||
final assignedOn = DateFormat('dd-MM-yyyy')
|
||||
.format(DateTime.parse(task.assignmentDate));
|
||||
final taskId = task.id;
|
||||
final location = [
|
||||
task.workItem?.workArea?.floor?.building?.name,
|
||||
task.workItem?.workArea?.floor?.floorName,
|
||||
task.workItem?.workArea?.areaName
|
||||
].where((e) => e != null && e.isNotEmpty).join(' > ');
|
||||
|
||||
final teamMembers =
|
||||
task.teamMembers.map((member) => member.firstName).toList();
|
||||
|
||||
// Navigate with detailed values
|
||||
Get.toNamed(
|
||||
'/daily-task/report-task',
|
||||
arguments: {
|
||||
'activity': activityName,
|
||||
'assigned': assigned,
|
||||
'taskId': taskId,
|
||||
'assignedBy': assignedBy,
|
||||
'completed': completed,
|
||||
'assignedOn': assignedOn,
|
||||
'location': location,
|
||||
'teamSize': task.teamMembers.length,
|
||||
'teamMembers': teamMembers,
|
||||
},
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
|
||||
minimumSize: const Size(60, 20),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
child: const Text("Report"),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final activityName =
|
||||
task.workItem?.activityMaster?.activityName ?? 'N/A';
|
||||
final assigned = '${task.plannedTask ?? "NA"} / '
|
||||
'${(task.workItem?.plannedWork != null && task.workItem?.completedWork != null) ? (task.workItem!.plannedWork! - task.workItem.completedWork!) : "NA"}';
|
||||
final plannedWork = '${(task.plannedTask.toString())}';
|
||||
final assignedBy =
|
||||
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
|
||||
final completedWork = '${(task.completedTask.toString())}';
|
||||
final assignedOn = DateFormat('dd-MM-yyyy')
|
||||
.format(DateTime.parse(task.assignmentDate));
|
||||
final taskId = task.id;
|
||||
final location = [
|
||||
task.workItem?.workArea?.floor?.building?.name,
|
||||
task.workItem?.workArea?.floor?.floorName,
|
||||
task.workItem?.workArea?.areaName
|
||||
].where((e) => e != null && e.isNotEmpty).join(' > ');
|
||||
|
||||
final teamMembers =
|
||||
task.teamMembers.map((member) => member.firstName).toList();
|
||||
final taskComments = task.comments
|
||||
.map((comment) => comment.comment ?? 'No Content')
|
||||
.toList();
|
||||
Get.toNamed(
|
||||
'/daily-task/comment-task',
|
||||
arguments: {
|
||||
'activity': activityName,
|
||||
'assigned': assigned,
|
||||
'taskId': taskId,
|
||||
'assignedBy': assignedBy,
|
||||
'completedWork': completedWork,
|
||||
'plannedWork': plannedWork,
|
||||
'assignedOn': assignedOn,
|
||||
'location': location,
|
||||
'teamSize': task.teamMembers.length,
|
||||
'teamMembers': teamMembers,
|
||||
'taskComments': taskComments
|
||||
},
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
|
||||
minimumSize: const Size(60, 20),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
child: const Text("Comment"),
|
||||
),
|
||||
],
|
||||
)),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildTeamCell(dynamic task) {
|
||||
return GestureDetector(
|
||||
onTap: () => TeamBottomSheet.show(
|
||||
context: context,
|
||||
teamMembers: task.teamMembers,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 32,
|
||||
width: 100,
|
||||
child: Stack(
|
||||
children: [
|
||||
for (int i = 0; i < task.teamMembers.length.clamp(0, 3); i++)
|
||||
_buildAvatar(task.teamMembers[i], i * 24.0),
|
||||
if (task.teamMembers.length > 3)
|
||||
_buildExtraMembersIndicator(task.teamMembers.length - 3, 48.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatar(dynamic member, double leftPosition) {
|
||||
return Positioned(
|
||||
left: leftPosition,
|
||||
child: Tooltip(
|
||||
message: member.firstName,
|
||||
child: Avatar(firstName: member.firstName, lastName: '', size: 32),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExtraMembersIndicator(int extraMembers, double leftPosition) {
|
||||
return Positioned(
|
||||
left: leftPosition,
|
||||
child: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
child: MyText.bodyMedium('+$extraMembers',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.black87)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/widgets/my_button.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
@ -50,10 +51,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium("Dashboard", fontWeight: 600),
|
||||
MySpacing.height(12),
|
||||
_buildDashboardStats(),
|
||||
MySpacing.height(350),
|
||||
MySpacing.height(300),
|
||||
if (!hasMpin) ...[
|
||||
MyCard(
|
||||
borderRadiusAll: 12,
|
||||
@ -130,46 +130,103 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
DashboardScreen.dailyTasksProgressRoute),
|
||||
];
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double maxWidth = constraints.maxWidth;
|
||||
int crossAxisCount = (maxWidth / 100).floor().clamp(2, 4);
|
||||
double cardWidth =
|
||||
(maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount;
|
||||
return GetBuilder<ProjectController>(
|
||||
id: 'dashboard_controller',
|
||||
builder: (controller) {
|
||||
final bool isProjectSelected = controller.selectedProject != null;
|
||||
|
||||
return Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children:
|
||||
stats.map((stat) => _buildStatCard(stat, cardWidth)).toList(),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isProjectSelected)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: MyCard(
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
paddingAll: 12,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.info_outline, color: Colors.orange),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
"No projects assigned yet. Please contact your manager to get started.",
|
||||
color: Colors.orange.shade800,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double maxWidth = constraints.maxWidth;
|
||||
int crossAxisCount = (maxWidth / 100).floor().clamp(2, 4);
|
||||
double cardWidth =
|
||||
(maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount;
|
||||
|
||||
return Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: stats
|
||||
.map((stat) =>
|
||||
_buildStatCard(stat, cardWidth, isProjectSelected))
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard(_StatItem statItem, double width) {
|
||||
return InkWell(
|
||||
onTap: () => Get.toNamed(statItem.route),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: MyCard.bordered(
|
||||
width: width,
|
||||
height: 100,
|
||||
paddingAll: 5,
|
||||
borderRadiusAll: 10,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||
shadow: MyShadow(elevation: 1.5, position: MyShadowPosition.bottom),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildStatCardIcon(statItem),
|
||||
MySpacing.height(8),
|
||||
MyText.labelSmall(
|
||||
statItem.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.visible,
|
||||
textAlign: TextAlign.center,
|
||||
Widget _buildStatCard(_StatItem statItem, double width, bool isEnabled) {
|
||||
return Opacity(
|
||||
opacity: isEnabled ? 1.0 : 0.4,
|
||||
child: IgnorePointer(
|
||||
ignoring: !isEnabled,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (!isEnabled) {
|
||||
Get.defaultDialog(
|
||||
title: "No Project Selected",
|
||||
middleText:
|
||||
"You need to select a project before accessing this section.",
|
||||
confirm: ElevatedButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text("OK"),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Get.toNamed(statItem.route);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: MyCard.bordered(
|
||||
width: width,
|
||||
height: 100,
|
||||
paddingAll: 5,
|
||||
borderRadiusAll: 10,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||
shadow: MyShadow(elevation: 1.5, position: MyShadowPosition.bottom),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildStatCardIcon(statItem),
|
||||
MySpacing.height(8),
|
||||
MyText.labelSmall(
|
||||
statItem.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.visible,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -1,528 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/controller/dashboard/ecommerce_controller.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/utils/utils.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_list_extension.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/images.dart';
|
||||
import 'package:marco/model/chart_model.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class EcommerceScreen extends StatefulWidget {
|
||||
const EcommerceScreen({super.key});
|
||||
|
||||
@override
|
||||
State<EcommerceScreen> createState() => _EcommerceScreenState();
|
||||
}
|
||||
|
||||
class _EcommerceScreenState extends State<EcommerceScreen> with UIMixin {
|
||||
EcommerceController controller = Get.put(EcommerceController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder(
|
||||
init: controller,
|
||||
tag: 'ecommerce_dashboard_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Ecommerce", fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Ecommerce', active: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(
|
||||
children: [
|
||||
MyFlexItem(sizes: 'lg-6 ', child: stats()),
|
||||
MyFlexItem(sizes: "lg-6", child: responseTimeByLocation()),
|
||||
MyFlexItem(sizes: 'lg-3 md-6', child: topCustomer()),
|
||||
MyFlexItem(sizes: "lg-4 md-6", child: costBreakDown()),
|
||||
MyFlexItem(sizes: 'lg-5', child: salesAnalytics()),
|
||||
MyFlexItem(child: productOrder()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget stats() {
|
||||
Widget statsWidget(IconData icon, String title, String count, String change, Color color) {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
height: 140,
|
||||
child: Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
paddingAll: 24,
|
||||
color: color.withValues(alpha:0.2),
|
||||
child: MyContainer(paddingAll: 8, color: color, child: Icon(icon, size: 16, color: contentTheme.light)),
|
||||
),
|
||||
MySpacing.width(24),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 80,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelSmall(title, maxLines: 1),
|
||||
MyText.bodyMedium(count, fontWeight: 600, maxLines: 1),
|
||||
Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
padding: MySpacing.xy(6, 4),
|
||||
color: change[0] == '+' ? Colors.green.withValues(alpha:.2) : theme.colorScheme.error.withValues(alpha:.2),
|
||||
child: MyText.labelSmall(change, color: change[0] == '+' ? Colors.green : theme.colorScheme.error),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(child: MyText.labelSmall("This Month", overflow: TextOverflow.ellipsis))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return MyFlex(contentPadding: false, children: [
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget(LucideIcons.shopping_bag, "Total Sales", "12,254", "+4.2%", contentTheme.primary)),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget(LucideIcons.receipt_text, "Total Expenses", "\$28,346.00", "-4.2%", contentTheme.secondary)),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget(LucideIcons.user, "Total Visitors", "1,29,368", "-3.54%", contentTheme.success)),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget(LucideIcons.shopping_basket, "Total Orders", "35,367", "+5.18%", contentTheme.info)),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget(LucideIcons.chart_column, "Average Order Value", "\$120", "+4.48%", contentTheme.dark)),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget(LucideIcons.users, "Total Customer", "36,835", "-1.15%", contentTheme.warning)),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget salesAnalytics() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Sales Analytics", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
SizedBox(height: 384, child: salesAnalyticsChart()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SfCartesianChart salesAnalyticsChart() {
|
||||
return SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: MySpacing.zero,
|
||||
primaryXAxis: const CategoryAxis(majorGridLines: MajorGridLines(width: 0), labelPlacement: LabelPlacement.onTicks),
|
||||
primaryYAxis: const NumericAxis(
|
||||
minimum: 30,
|
||||
maximum: 80,
|
||||
axisLine: AxisLine(width: 0),
|
||||
edgeLabelPlacement: EdgeLabelPlacement.shift,
|
||||
labelFormat: '{value}',
|
||||
majorTickLines: MajorTickLines(size: 0)),
|
||||
series: [
|
||||
SplineSeries<ChartSampleData, String>(
|
||||
dataSource: controller.salesAnalyticsData,
|
||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
||||
yValueMapper: (ChartSampleData sales, _) => sales.y,
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
name: 'Pending',
|
||||
),
|
||||
SplineSeries<ChartSampleData, String>(
|
||||
dataSource: controller.salesAnalyticsData,
|
||||
name: 'Complete',
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
||||
yValueMapper: (ChartSampleData sales, _) => sales.secondSeriesYValue,
|
||||
)
|
||||
],
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
);
|
||||
}
|
||||
|
||||
Widget topCustomer() {
|
||||
Widget topCustomerData(String name, String cardNumber, int orders, String image) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
height: 44,
|
||||
width: 44,
|
||||
paddingAll: 0,
|
||||
borderRadiusAll: 4,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
child: Image.asset(image),
|
||||
),
|
||||
MySpacing.width(20),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(name, fontWeight: 600, maxLines: 1),
|
||||
MyText.labelSmall(cardNumber, maxLines: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyText.labelSmall("Orders : $orders"),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Top Customer", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
topCustomerData("John Doe", "1234 5678 9012 3456", 21, Images.avatars[0]),
|
||||
MySpacing.height(24),
|
||||
topCustomerData("Jane Smith", "2345 6789 0123 4567", 22, Images.avatars[1]),
|
||||
MySpacing.height(24),
|
||||
topCustomerData("Michael Johnson", "3456 7890 1234 5678", 23, Images.avatars[2]),
|
||||
MySpacing.height(24),
|
||||
topCustomerData("Emily Davis", "4567 8901 2345 6789", 24, Images.avatars[3]),
|
||||
MySpacing.height(24),
|
||||
topCustomerData("Chris Brown", "5678 9012 3456 7890", 25, Images.avatars[4]),
|
||||
MySpacing.height(24),
|
||||
topCustomerData("Olivia Wilson", "6789 0123 4567 8901", 26, Images.avatars[5]),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget costBreakDown() {
|
||||
Widget buildCircleChartData(Color color, String name, String price) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
paddingAll: 4,
|
||||
color: color,
|
||||
),
|
||||
MySpacing.width(8),
|
||||
MyText.labelMedium(name)
|
||||
],
|
||||
),
|
||||
MyText.labelSmall(price),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium("Cost BreakDown", overflow: TextOverflow.ellipsis, fontWeight: 600),
|
||||
InkWell(onTap: () {}, child: Icon(LucideIcons.move_right, size: 16)),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 293,
|
||||
child: SfCircularChart(
|
||||
margin: MySpacing.zero,
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CircularSeries>[
|
||||
DoughnutSeries<ChartSampleData, String>(
|
||||
radius: '80%',
|
||||
explode: true,
|
||||
explodeOffset: '10%',
|
||||
dataSource: controller.circleChart,
|
||||
pointColorMapper: (ChartSampleData data, _) => data.pointColor,
|
||||
xValueMapper: (ChartSampleData data, _) => data.x,
|
||||
yValueMapper: (ChartSampleData data, _) => data.y,
|
||||
dataLabelSettings: const DataLabelSettings(isVisible: true)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [MyText.titleMedium("Top Channel"), MyText.titleMedium("Value")],
|
||||
),
|
||||
MySpacing.height(12),
|
||||
buildCircleChartData(const Color.fromRGBO(9, 0, 136, 1), "Salary", "\$54,847"),
|
||||
MySpacing.height(8),
|
||||
buildCircleChartData(const Color.fromRGBO(147, 0, 119, 1), "Bill", "\$58,188"),
|
||||
MySpacing.height(8),
|
||||
buildCircleChartData(const Color.fromRGBO(228, 0, 124, 1), "Marketing", "\$24,618"),
|
||||
MySpacing.height(8),
|
||||
buildCircleChartData(const Color.fromRGBO(255, 189, 57, 1), "Other", "\$15,651")
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget responseTimeByLocation() {
|
||||
Widget buildResponseTimeByLocationData(String currentTime, String price, IconData icon, Color iconColor) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(LucideIcons.circle_dot_dashed, size: 16),
|
||||
MySpacing.width(8),
|
||||
MyText.bodyMedium(currentTime),
|
||||
],
|
||||
),
|
||||
MySpacing.height(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MyText.bodyLarge(price, fontSize: 20, fontWeight: 600, muted: true),
|
||||
MySpacing.width(8),
|
||||
Icon(icon, size: 16, color: iconColor),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 0,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.titleMedium(
|
||||
"Response time by location",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
PopupMenuButton(
|
||||
onSelected: controller.onSelectedTimeByLocation,
|
||||
itemBuilder: (BuildContext context) {
|
||||
return ["Year", "Month", "Week", "Day", "Hours"].map((behavior) {
|
||||
return PopupMenuItem(
|
||||
value: behavior,
|
||||
height: 32,
|
||||
child: MyText.bodySmall(
|
||||
behavior.toString(),
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontWeight: 600,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
color: theme.cardTheme.color,
|
||||
child: MyContainer.bordered(
|
||||
padding: MySpacing.xy(8, 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
MyText.labelSmall(controller.selectedTimeByLocation.toString(), color: theme.colorScheme.onSurface),
|
||||
Icon(LucideIcons.chevron_down, size: 16, color: theme.colorScheme.onSurface)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
MySpacing.height(12),
|
||||
MyFlex(
|
||||
wrapCrossAlignment: WrapCrossAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
wrapAlignment: WrapAlignment.center,
|
||||
children: [
|
||||
MyFlexItem(
|
||||
sizes: "lg-4 md-4 sm-6",
|
||||
child: buildResponseTimeByLocationData("Current Week", "\$1886.52", LucideIcons.corner_right_up, contentTheme.success),
|
||||
),
|
||||
MyFlexItem(
|
||||
sizes: "lg-4 md-4 sm-6",
|
||||
child: buildResponseTimeByLocationData("Conversation", "5.68%", LucideIcons.corner_right_up, contentTheme.success),
|
||||
),
|
||||
MyFlexItem(
|
||||
sizes: "lg-4 md-4 sm-6",
|
||||
child: buildResponseTimeByLocationData("Customers", "59K", LucideIcons.corner_right_up, contentTheme.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(12),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: MySpacing.all(16),
|
||||
child: SizedBox(
|
||||
height: 267,
|
||||
child: SfCartesianChart(
|
||||
primaryXAxis: CategoryAxis(),
|
||||
tooltipBehavior: controller.chart,
|
||||
axes: <ChartAxis>[
|
||||
NumericAxis(
|
||||
numberFormat: NumberFormat.compact(),
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
opposedPosition: true,
|
||||
name: 'yAxis1',
|
||||
interval: 1000,
|
||||
minimum: 0,
|
||||
maximum: 7000)
|
||||
],
|
||||
series: [
|
||||
ColumnSeries<ChartSampleData, String>(
|
||||
animationDuration: 2000,
|
||||
width: 0.5,
|
||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(4)),
|
||||
color: contentTheme.primary,
|
||||
dataSource: controller.chartData,
|
||||
xValueMapper: (ChartSampleData data, _) => data.x,
|
||||
yValueMapper: (ChartSampleData data, _) => data.y,
|
||||
name: 'Unit Sold'),
|
||||
LineSeries<ChartSampleData, String>(
|
||||
animationDuration: 4500,
|
||||
animationDelay: 2000,
|
||||
dataSource: controller.chartData,
|
||||
xValueMapper: (ChartSampleData data, _) => data.x,
|
||||
yValueMapper: (ChartSampleData data, _) => data.yValue,
|
||||
yAxisName: 'yAxis1',
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
name: 'Total Transaction')
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget productOrder() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium("Product Order", fontWeight: 600),
|
||||
MySpacing.height(20),
|
||||
if (controller.order.isNotEmpty)
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
sortAscending: true,
|
||||
columnSpacing: 98,
|
||||
onSelectAll: (_) => {},
|
||||
headingRowColor: WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)),
|
||||
dataRowMaxHeight: 60,
|
||||
showBottomBorder: true,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
border: TableBorder.all(borderRadius: BorderRadius.circular(4), style: BorderStyle.solid, width: .4, color: Colors.grey),
|
||||
columns: [
|
||||
DataColumn(label: MyText.labelLarge('Order Id', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Customer Name', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Location', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Order Date', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Payments', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Quantity', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Price', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Total Amount', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Status', color: contentTheme.primary)),
|
||||
],
|
||||
rows: controller.order
|
||||
.mapIndexed((index, data) => DataRow(cells: [
|
||||
DataCell(MyText.bodyMedium("#${data.orderId}", fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium(data.customerName, fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium(data.location, fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium("${Utils.getDateStringFromDateTime(data.orderDate)}", fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium(data.payment, fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium("${data.quantity}", fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium("\$${data.price}", fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium("\$${data.quantity * data.price}", fontWeight: 600)),
|
||||
DataCell(MyContainer(
|
||||
padding: MySpacing.xy(8, 4),
|
||||
color: getStatusColor(data.status)?.withAlpha(32),
|
||||
child: MyText.bodySmall(
|
||||
data.status,
|
||||
fontWeight: 600,
|
||||
color: getStatusColor(data.status),
|
||||
),
|
||||
)),
|
||||
]))
|
||||
.toList()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color? getStatusColor(String? status) {
|
||||
switch (status) {
|
||||
case "Delivered":
|
||||
return contentTheme.primary;
|
||||
case "Shopping":
|
||||
return contentTheme.success;
|
||||
case "New":
|
||||
return contentTheme.warning;
|
||||
case "Pending":
|
||||
return contentTheme.danger;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,436 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/dashboard/job_controller.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/utils/utils.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_list_extension.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/images.dart';
|
||||
import 'package:marco/model/chart_model.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class JobScreen extends StatefulWidget {
|
||||
const JobScreen({super.key});
|
||||
|
||||
@override
|
||||
State<JobScreen> createState() => _JobScreenState();
|
||||
}
|
||||
|
||||
class _JobScreenState extends State<JobScreen> with UIMixin {
|
||||
JobController controller = Get.put(JobController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder(
|
||||
init: controller,
|
||||
tag: 'job_dashboard_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Job", fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Job', active: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(
|
||||
children: [
|
||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.briefcase, '245', 'EMPLOYEES IN SYSTEM', contentTheme.primary)),
|
||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.file_text, '3201', 'CANDIDATES IN DATA', contentTheme.secondary)),
|
||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.map_pin, '56', 'LOCATIONS SERVED', contentTheme.success)),
|
||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.user_plus, '312', 'RECRUITER NETWORK', contentTheme.info)),
|
||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.credit_card, '689', 'ACTIVE SUBSCRIPTIONS', contentTheme.purple)),
|
||||
MyFlexItem(sizes: 'xxl-2 xl-4 lg-4 md-4 sm-6', child: stats(LucideIcons.cloud_upload, '82%', 'RESUME UPLOAD RATE', contentTheme.pink)),
|
||||
MyFlexItem(sizes: 'lg-4', child: workingFormat()),
|
||||
MyFlexItem(sizes: 'lg-8 md-6', child: listingPerformance()),
|
||||
MyFlexItem(sizes: 'lg-4 md-6', child: recentCandidate()),
|
||||
MyFlexItem(sizes: 'lg-4 md-6', child: mostViewedCVs()),
|
||||
MyFlexItem(sizes: 'lg-4 md-6', child: recentChat()),
|
||||
MyFlexItem(child: recentApplication()),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget stats(IconData? icon, String title, String subTitle, Color color) {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
paddingAll: 12,
|
||||
color: color,
|
||||
child: Icon(icon, color: contentTheme.light, size: 16),
|
||||
),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium(title, fontWeight: 600),
|
||||
MySpacing.height(4),
|
||||
MyText.labelSmall(subTitle, xMuted: true, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget workingFormat() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
height: 408,
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
MyText.bodyMedium("Working Format", height: .8, fontWeight: 600),
|
||||
SfCircularChart(legend: Legend(isVisible: true, position: LegendPosition.bottom, overflowMode: LegendItemOverflowMode.wrap), series: [
|
||||
DoughnutSeries<ChartSampleData, String>(
|
||||
explode: true,
|
||||
dataSource: <ChartSampleData>[
|
||||
ChartSampleData(x: 'OnSite', y: 55, text: '55%'),
|
||||
ChartSampleData(x: 'Remote', y: 31, text: '31%'),
|
||||
ChartSampleData(x: 'Hybrid', y: 7.7, text: '7.7%'),
|
||||
],
|
||||
xValueMapper: (ChartSampleData data, _) => data.x as String,
|
||||
yValueMapper: (ChartSampleData data, _) => data.y,
|
||||
dataLabelMapper: (ChartSampleData data, _) => data.text,
|
||||
dataLabelSettings: DataLabelSettings(isVisible: true))
|
||||
])
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget listingPerformance() {
|
||||
Widget isSelectTime(String title, int index) {
|
||||
bool isSelect = controller.isSelectedListingPerformanceTime == index;
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 0, position: MyShadowPosition.bottom),
|
||||
paddingAll: 4,
|
||||
color: isSelect ? contentTheme.secondary.withValues(alpha:0.15) : null,
|
||||
onTap: () => controller.onSelectListingPerformanceTimeToggle(index),
|
||||
child: MyText.labelSmall(title, fontWeight: 600, color: isSelect ? contentTheme.secondary : null),
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.bodyMedium("Listing Performance", fontWeight: 600, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
isSelectTime("Day", 0),
|
||||
MySpacing.width(12),
|
||||
isSelectTime("Week", 1),
|
||||
MySpacing.width(12),
|
||||
isSelectTime("Month", 2),
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
SizedBox(
|
||||
height: 310,
|
||||
child: SfCartesianChart(
|
||||
margin: MySpacing.zero,
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)),
|
||||
primaryYAxis: NumericAxis(
|
||||
maximum: 20,
|
||||
minimum: 0,
|
||||
interval: 4,
|
||||
axisLine: AxisLine(width: 0),
|
||||
majorTickLines: MajorTickLines(size: 0),
|
||||
),
|
||||
series: [
|
||||
ColumnSeries<ChartSampleData, String>(
|
||||
width: .7,
|
||||
spacing: .2,
|
||||
dataSource: controller.chartData,
|
||||
color: theme.colorScheme.primary,
|
||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
||||
yValueMapper: (ChartSampleData sales, _) => sales.y,
|
||||
name: 'Views'),
|
||||
ColumnSeries<ChartSampleData, String>(
|
||||
dataSource: controller.chartData,
|
||||
width: .7,
|
||||
spacing: .2,
|
||||
color: theme.colorScheme.secondary,
|
||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
||||
yValueMapper: (ChartSampleData sales, _) => sales.secondSeriesYValue,
|
||||
name: 'Application')
|
||||
],
|
||||
legend: Legend(isVisible: true, position: LegendPosition.bottom),
|
||||
tooltipBehavior: controller.columnToolTip),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget recentCandidate() {
|
||||
Widget candidatesData(String image, title, subtitle) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
paddingAll: 0,
|
||||
height: 44,
|
||||
width: 44,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
child: Image.asset(image, fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(title, fontWeight: 600),
|
||||
MyText.bodySmall(subtitle, fontWeight: 600, xMuted: true, maxLines: 1, overflow: TextOverflow.visible)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Recent Candidate", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
candidatesData(Images.avatars[3], "Sophia Williams", controller.dummyTexts[0]),
|
||||
MySpacing.height(24),
|
||||
candidatesData(Images.avatars[4], "Ethan Johnson", controller.dummyTexts[1]),
|
||||
MySpacing.height(24),
|
||||
candidatesData(Images.avatars[5], "Olivia Martinez", controller.dummyTexts[2]),
|
||||
MySpacing.height(24),
|
||||
candidatesData(Images.avatars[6], "Liam Brown", controller.dummyTexts[3]),
|
||||
MySpacing.height(24),
|
||||
candidatesData(Images.avatars[7], "Ava Davis", controller.dummyTexts[4]),
|
||||
MySpacing.height(24),
|
||||
candidatesData(Images.avatars[8], "Mason Lee", controller.dummyTexts[5]),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget mostViewedCVs() {
|
||||
Widget cv(String title) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
paddingAll: 0,
|
||||
height: 44,
|
||||
width: 44,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
color: contentTheme.primary.withAlpha(40),
|
||||
child: Icon(LucideIcons.file_text, color: contentTheme.primary),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(child: MyText.bodyMedium(title, fontWeight: 600, overflow: TextOverflow.ellipsis)),
|
||||
InkWell(onTap: () {}, child: Icon(LucideIcons.download))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Most Viewed CV's", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
cv("Isabella Green"),
|
||||
MySpacing.height(24),
|
||||
cv("James Turner"),
|
||||
MySpacing.height(24),
|
||||
cv("Charlotte Scott"),
|
||||
MySpacing.height(24),
|
||||
cv("Oliver King"),
|
||||
MySpacing.height(24),
|
||||
cv("Lucas Carter"),
|
||||
MySpacing.height(24),
|
||||
cv("Mia Brooks"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget recentChat() {
|
||||
Widget chat(String image, name, message) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
paddingAll: 0,
|
||||
height: 44,
|
||||
width: 44,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
child: Image.asset(image, fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(name, fontWeight: 600),
|
||||
MyText.labelSmall(message, fontWeight: 600, maxLines: 1, muted: true, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
)),
|
||||
MySpacing.width(28),
|
||||
Icon(LucideIcons.message_square, size: 20)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
MyText.bodyMedium("Recent Chat", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
chat(Images.avatars[0], "Sophia", controller.dummyTexts[6]),
|
||||
MySpacing.height(24),
|
||||
chat(Images.avatars[1], "Liam", controller.dummyTexts[5]),
|
||||
MySpacing.height(24),
|
||||
chat(Images.avatars[2], "Charlotte", controller.dummyTexts[4]),
|
||||
MySpacing.height(24),
|
||||
chat(Images.avatars[3], "Oliver", controller.dummyTexts[3]),
|
||||
MySpacing.height(24),
|
||||
chat(Images.avatars[4], "Amelia", controller.dummyTexts[2]),
|
||||
MySpacing.height(24),
|
||||
chat(Images.avatars[5], "James", controller.dummyTexts[1])
|
||||
]));
|
||||
}
|
||||
|
||||
Widget recentApplication() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Recent Application", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
sortAscending: true,
|
||||
columnSpacing: 88,
|
||||
onSelectAll: (_) => {},
|
||||
headingRowColor: WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)),
|
||||
dataRowMaxHeight: 60,
|
||||
showBottomBorder: true,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
border: TableBorder.all(borderRadius: BorderRadius.circular(4), style: BorderStyle.solid, width: .4, color: Colors.grey),
|
||||
columns: [
|
||||
DataColumn(label: MyText.labelLarge('S.No', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Candidate', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Category', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Designation', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Mail', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Location', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Date', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Type', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Action', color: contentTheme.primary)),
|
||||
],
|
||||
rows: controller.recentApplication
|
||||
.mapIndexed((index, data) => DataRow(cells: [
|
||||
DataCell(MyText.bodyMedium("#${data.id}", fontWeight: 600)),
|
||||
DataCell(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
MyContainer(
|
||||
height: 40,
|
||||
width: 40,
|
||||
paddingAll: 0,
|
||||
child: Image.asset(Images.avatars[index % Images.avatars.length], fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(24),
|
||||
MyText.labelMedium(data.candidate, fontWeight: 600)
|
||||
],
|
||||
)),
|
||||
DataCell(MyText.labelMedium(data.category, fontWeight: 600)),
|
||||
DataCell(MyText.labelMedium(data.designation, fontWeight: 600)),
|
||||
DataCell(MyText.labelMedium(data.mail, fontWeight: 600)),
|
||||
DataCell(MyText.labelMedium(data.location, fontWeight: 600)),
|
||||
DataCell(MyText.labelMedium("${Utils.getDateStringFromDateTime(data.date)}", fontWeight: 600)),
|
||||
DataCell(MyText.labelMedium(data.type, fontWeight: 600)),
|
||||
DataCell(Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
onTap: () {},
|
||||
color: contentTheme.primary,
|
||||
paddingAll: 8,
|
||||
child: Icon(LucideIcons.download, size: 16, color: contentTheme.onPrimary),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
MyContainer(
|
||||
onTap: () {},
|
||||
color: contentTheme.secondary,
|
||||
paddingAll: 8,
|
||||
child: Icon(LucideIcons.pencil, size: 16, color: contentTheme.onPrimary),
|
||||
),
|
||||
],
|
||||
))
|
||||
]))
|
||||
.toList()),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,435 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/dashboard/project_controller.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/utils/utils.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_list_extension.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/images.dart';
|
||||
import 'package:marco/model/chart_model.dart';
|
||||
import 'package:marco/model/task_list_model.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class ProjectScreen extends StatefulWidget {
|
||||
const ProjectScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ProjectScreen> createState() => _ProjectScreenState();
|
||||
}
|
||||
|
||||
class _ProjectScreenState extends State<ProjectScreen> with UIMixin {
|
||||
ProjectController controller = Get.put(ProjectController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder(
|
||||
init: controller,
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Project", fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Project', active: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(children: [
|
||||
MyFlexItem(sizes: 'lg-6', child: stats()),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: taskPerformance()),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: incomeAnalytics()),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: taskList()),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: recentTransaction()),
|
||||
MyFlexItem(sizes: 'lg-3 md-6', child: taskSummary()),
|
||||
MyFlexItem(sizes: 'lg-9 md-6', child: projectSummary()),
|
||||
]),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget stats() {
|
||||
Widget statsWidget(String title, String subTitle, IconData icon, Color color) {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium(title, maxLines: 1),
|
||||
MyText.titleMedium(subTitle, fontWeight: 600),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
MyContainer(
|
||||
color: color,
|
||||
child: Icon(icon, color: contentTheme.light),
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
return MyFlex(
|
||||
contentPadding: false,
|
||||
children: [
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget("Projects Completed", "120", LucideIcons.briefcase, contentTheme.primary)),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget("Tasks In Progress", "75", LucideIcons.circle_check, contentTheme.secondary)),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget("Total Hours Worked", "540", LucideIcons.clock, contentTheme.info)),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget("Current Budgets", "\$12,500", LucideIcons.dollar_sign, contentTheme.success)),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget("Completed Tasks", "58", LucideIcons.check, contentTheme.warning)),
|
||||
MyFlexItem(sizes: 'lg-6 md-6 sm-6', child: statsWidget("Team Members", "15", LucideIcons.user, contentTheme.danger)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget taskPerformance() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Task Performance", fontWeight: 600),
|
||||
SizedBox(
|
||||
height: 299,
|
||||
child: Theme(
|
||||
data: ThemeData(),
|
||||
child: SfCircularChart(
|
||||
margin: MySpacing.zero,
|
||||
series: [
|
||||
RadialBarSeries<ChartSampleData, String>(
|
||||
dataLabelSettings: const DataLabelSettings(isVisible: true, textStyle: TextStyle(fontSize: 10.0)),
|
||||
dataSource: <ChartSampleData>[
|
||||
ChartSampleData(x: 'Complete', y: 7, text: '100%', pointColor: contentTheme.primary),
|
||||
ChartSampleData(x: 'Active', y: 5, text: '100%', pointColor: contentTheme.success),
|
||||
ChartSampleData(x: 'Assigned', y: 8, text: '100%', pointColor: contentTheme.info),
|
||||
],
|
||||
trackColor: contentTheme.background,
|
||||
cornerStyle: CornerStyle.bothCurve,
|
||||
gap: '10%',
|
||||
radius: '90%',
|
||||
xValueMapper: (ChartSampleData data, _) => data.x as String,
|
||||
yValueMapper: (ChartSampleData data, _) => data.y,
|
||||
pointRadiusMapper: (ChartSampleData data, _) => data.text,
|
||||
pointColorMapper: (ChartSampleData data, _) => data.pointColor,
|
||||
dataLabelMapper: (ChartSampleData data, _) => data.x as String)
|
||||
],
|
||||
tooltipBehavior: controller.tooltipBehavior,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget incomeAnalytics() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Income Analytics", fontWeight: 600),
|
||||
SfCircularChart(
|
||||
margin: MySpacing.zero,
|
||||
legend: Legend(isVisible: true, overflowMode: LegendItemOverflowMode.wrap),
|
||||
series: [
|
||||
PieSeries<ChartSampleData, String>(
|
||||
dataSource: <ChartSampleData>[
|
||||
ChartSampleData(x: 'USA', y: 700000, text: '60%'),
|
||||
ChartSampleData(x: 'Germany', y: 450000, text: '50%'),
|
||||
ChartSampleData(x: 'China', y: 600000, text: '65%'),
|
||||
ChartSampleData(x: 'India', y: 400000, text: '55%'),
|
||||
ChartSampleData(x: 'Brazil', y: 350000, text: '40%'),
|
||||
ChartSampleData(x: 'Russia', y: 300000, text: '35%'),
|
||||
ChartSampleData(x: 'South Africa', y: 250000, text: '30%')
|
||||
],
|
||||
xValueMapper: (ChartSampleData data, _) => data.x as String,
|
||||
yValueMapper: (ChartSampleData data, _) => data.y,
|
||||
dataLabelMapper: (ChartSampleData data, _) => data.x as String,
|
||||
startAngle: 100,
|
||||
endAngle: 100,
|
||||
pointRadiusMapper: (ChartSampleData data, _) => data.text,
|
||||
dataLabelSettings: const DataLabelSettings(isVisible: true, labelPosition: ChartDataLabelPosition.outside),
|
||||
),
|
||||
],
|
||||
tooltipBehavior: TooltipBehavior(enable: true, tooltipPosition: TooltipPosition.auto, duration: 2 * 1000),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget taskList() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 0,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.all(24),
|
||||
child: MyText.bodyMedium("Task List", fontWeight: 600),
|
||||
),
|
||||
SizedBox(
|
||||
height: 300,
|
||||
child: ListView.separated(
|
||||
itemCount: controller.task.length,
|
||||
shrinkWrap: true,
|
||||
padding: MySpacing.x(24),
|
||||
itemBuilder: (context, index) {
|
||||
TaskListModel task = controller.task[index];
|
||||
return Row(
|
||||
children: [
|
||||
Theme(
|
||||
data: ThemeData(visualDensity: getCompactDensity),
|
||||
child: Checkbox(
|
||||
value: task.isSelectTask,
|
||||
onChanged: (value) => controller.onSelectTask(task),
|
||||
visualDensity: getCompactDensity,
|
||||
),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
MyContainer.rounded(
|
||||
height: 32,
|
||||
width: 32,
|
||||
paddingAll: 0,
|
||||
child: Image.asset(Images.avatars[index % Images.avatars.length], fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(child: MyText.bodyMedium(task.title, maxLines: 1)),
|
||||
MyText.labelMedium(task.status,
|
||||
color: task.status == 'Pending'
|
||||
? contentTheme.primary
|
||||
: task.status == 'Completed'
|
||||
? contentTheme.success
|
||||
: null),
|
||||
],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return MySpacing.height(24);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget recentTransaction() {
|
||||
Widget recentTransaction(String title, String subTitle, String price) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
MyContainer.roundBordered(
|
||||
paddingAll: 12,
|
||||
child: MyText(title[0].capitalize.toString()),
|
||||
),
|
||||
MySpacing.width(24),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(title),
|
||||
MySpacing.height(4),
|
||||
MyText.bodySmall(subTitle),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyText.bodySmall(price),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 0,
|
||||
height: 367,
|
||||
child: ListView(
|
||||
padding: MySpacing.all(24),
|
||||
children: [
|
||||
MyText.bodyMedium("Recent Transaction", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
recentTransaction("Charles", "Feb 28,2023 - 12:54PM", "price"),
|
||||
MySpacing.height(24),
|
||||
recentTransaction("David", "Feb 28,2023 - 12:54PM", "price"),
|
||||
MySpacing.height(24),
|
||||
recentTransaction("Leonard", "Feb 28,2023 - 12:54PM", "price"),
|
||||
MySpacing.height(24),
|
||||
recentTransaction("Steven", "Feb 28,2023 - 12:54PM", "price"),
|
||||
MySpacing.height(24),
|
||||
recentTransaction("Steven", "Feb 28,2023 - 12:54PM", "price"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget taskSummary() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Task Summary", fontWeight: 600),
|
||||
MyContainer(
|
||||
onTap: () {},
|
||||
paddingAll: 8,
|
||||
color: contentTheme.light,
|
||||
child: MyText.labelSmall("View All", fontWeight: 600),
|
||||
)
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
SizedBox(
|
||||
height: 344,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: MySpacing.zero,
|
||||
legend: Legend(isVisible: true, position: LegendPosition.bottom),
|
||||
primaryXAxis: const CategoryAxis(majorGridLines: MajorGridLines(width: 0), labelPlacement: LabelPlacement.onTicks),
|
||||
primaryYAxis: const NumericAxis(
|
||||
axisLine: AxisLine(width: 0), edgeLabelPlacement: EdgeLabelPlacement.shift, labelFormat: '{value}', majorTickLines: MajorTickLines(size: 0)),
|
||||
series: [
|
||||
SplineSeries<ChartSampleData, String>(
|
||||
dataSource: controller.chartData,
|
||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
||||
yValueMapper: (ChartSampleData sales, _) => sales.y,
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
name: 'This Week'),
|
||||
SplineSeries<ChartSampleData, String>(
|
||||
dataSource: controller.chartData,
|
||||
name: 'Last Week',
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
xValueMapper: (ChartSampleData sales, _) => sales.x as String,
|
||||
yValueMapper: (ChartSampleData sales, _) => sales.secondSeriesYValue,
|
||||
)
|
||||
],
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget projectSummary() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Project Summary", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
sortAscending: true,
|
||||
columnSpacing: 84,
|
||||
onSelectAll: (_) => {},
|
||||
headingRowColor: WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)),
|
||||
dataRowMaxHeight: 60,
|
||||
showBottomBorder: true,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
border: TableBorder.all(borderRadius: BorderRadius.circular(4), style: BorderStyle.solid, width: .4, color: Colors.grey),
|
||||
columns: [
|
||||
DataColumn(label: MyText.labelLarge('S.No', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Title', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Assign to', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Due Date', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Priority', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Status', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Action', color: contentTheme.primary)),
|
||||
],
|
||||
rows: controller.projectSummary
|
||||
.mapIndexed((index, data) => DataRow(cells: [
|
||||
DataCell(MyText.bodyMedium("#${data.id}", fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium(data.title, fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium(data.assignTo, fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium(Utils.getDateStringFromDateTime(data.date), fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium(data.priority, fontWeight: 600)),
|
||||
DataCell(MyText.bodyMedium(data.status, fontWeight: 600)),
|
||||
DataCell(Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
onTap: () {},
|
||||
color: contentTheme.primary,
|
||||
paddingAll: 8,
|
||||
child: Icon(LucideIcons.download, size: 16, color: contentTheme.onPrimary),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
MyContainer(
|
||||
onTap: () {},
|
||||
color: contentTheme.secondary,
|
||||
paddingAll: 8,
|
||||
child: Icon(LucideIcons.pencil, size: 16, color: contentTheme.onPrimary),
|
||||
),
|
||||
],
|
||||
))
|
||||
]))
|
||||
.toList()),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,506 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/controller/dashboard/sales_controller.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/utils/utils.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_list_extension.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/images.dart';
|
||||
import 'package:marco/model/chart_model.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class SalesScreen extends StatefulWidget {
|
||||
const SalesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SalesScreen> createState() => _SalesScreenState();
|
||||
}
|
||||
|
||||
class _SalesScreenState extends State<SalesScreen> with UIMixin {
|
||||
SalesController controller = Get.put(SalesController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder(
|
||||
init: controller,
|
||||
tag: 'sales_dashboard_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Crypto", fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Crypto', active: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(
|
||||
children: [
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: stats("Total Income", "\$3,50,000", "15.00%")),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: stats("Profit", "\$1,20,000", "10.00%")),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: stats("Total Views", "15000", "5.00%")),
|
||||
MyFlexItem(sizes: 'lg-3 md-6 sm-6', child: stats("Conversion Rate", "18.75%", "3.50%")),
|
||||
MyFlexItem(sizes: 'lg-6 md-6', child: visitorsReport()),
|
||||
MyFlexItem(sizes: 'lg-6 md-6', child: otherStatistics()),
|
||||
MyFlexItem(sizes: 'lg-3.5 md-6', child: recentTransaction()),
|
||||
MyFlexItem(sizes: 'lg-3.5 md-6', child: recentActivity()),
|
||||
MyFlexItem(sizes: 'lg-2.5 md-6', child: countryWiseSale()),
|
||||
MyFlexItem(sizes: 'lg-2.5 md-6', child: dealSource()),
|
||||
MyFlexItem(child: recentOrder()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget stats(String title, String changes, String percentage) {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
height: 144,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(title),
|
||||
MyText.titleLarge(changes, fontWeight: 600),
|
||||
Row(
|
||||
children: [
|
||||
MyContainer(
|
||||
paddingAll: 4,
|
||||
color: contentTheme.success.withValues(alpha:0.2),
|
||||
child: MyText.labelSmall(percentage, color: contentTheme.success),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(child: MyText.labelSmall("Compare to last month")),
|
||||
],
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget visitorsReport() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Visitors Report", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
SfCartesianChart(
|
||||
margin: MySpacing.zero,
|
||||
primaryXAxis: CategoryAxis(),
|
||||
tooltipBehavior: controller.visitorChart,
|
||||
axes: <ChartAxis>[
|
||||
NumericAxis(
|
||||
numberFormat: NumberFormat.compact(),
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
opposedPosition: true,
|
||||
name: 'yAxis1',
|
||||
interval: 1000,
|
||||
minimum: 0,
|
||||
maximum: 7000)
|
||||
],
|
||||
series: [
|
||||
ColumnSeries<ChartSampleData, String>(
|
||||
animationDuration: 2000,
|
||||
width: 0.5,
|
||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(4)),
|
||||
color: contentTheme.success,
|
||||
dataSource: controller.visitorChartData,
|
||||
xValueMapper: (ChartSampleData data, _) => data.x,
|
||||
yValueMapper: (ChartSampleData data, _) => data.y,
|
||||
name: 'Unit Sold'),
|
||||
LineSeries<ChartSampleData, String>(
|
||||
animationDuration: 4500,
|
||||
animationDelay: 2000,
|
||||
dataSource: controller.visitorChartData,
|
||||
xValueMapper: (ChartSampleData data, _) => data.x,
|
||||
yValueMapper: (ChartSampleData data, _) => data.yValue,
|
||||
yAxisName: 'yAxis1',
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
name: 'Total Transaction')
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget otherStatistics() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Other Statistics", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: MySpacing.zero,
|
||||
legend: Legend(isVisible: true, overflowMode: LegendItemOverflowMode.wrap, position: LegendPosition.bottom),
|
||||
primaryXAxis: const NumericAxis(edgeLabelPlacement: EdgeLabelPlacement.shift, majorGridLines: MajorGridLines(width: 0)),
|
||||
primaryYAxis: const NumericAxis(labelFormat: '{value}', axisLine: AxisLine(width: 0), majorTickLines: MajorTickLines(color: Colors.transparent)),
|
||||
series: [
|
||||
LineSeries<ChartData, num>(
|
||||
dataSource: controller.statisticsData,
|
||||
xValueMapper: (ChartData sales, _) => sales.x,
|
||||
yValueMapper: (ChartData sales, _) => sales.y,
|
||||
name: 'Pending',
|
||||
color: contentTheme.secondary,
|
||||
markerSettings: const MarkerSettings(isVisible: true)),
|
||||
LineSeries<ChartData, num>(
|
||||
dataSource: controller.statisticsData,
|
||||
name: 'Delivered',
|
||||
color: contentTheme.primary,
|
||||
xValueMapper: (ChartData sales, _) => sales.x,
|
||||
yValueMapper: (ChartData sales, _) => sales.y2,
|
||||
markerSettings: const MarkerSettings(isVisible: true))
|
||||
],
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget recentTransaction() {
|
||||
Widget recentTransactionWidget(String transactionMethod, String transactionType, String price, String date, IconData? icon, Color color) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
height: 44,
|
||||
width: 44,
|
||||
paddingAll: 0,
|
||||
color: color.withValues(alpha:0.2),
|
||||
child: Icon(icon, color: color),
|
||||
),
|
||||
MySpacing.width(24),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(transactionMethod, fontWeight: 600, maxLines: 1),
|
||||
MySpacing.height(4),
|
||||
MyText.bodySmall(transactionType, fontWeight: 600, muted: true, maxLines: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
MyText.bodyMedium(price, fontWeight: 600),
|
||||
MyText.bodySmall(date, fontWeight: 600, muted: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
height: 475,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: MyText.bodyMedium("Recent Transaction", fontWeight: 600)),
|
||||
InkWell(onTap: () {}, child: MyText.labelSmall("View All", fontWeight: 600)),
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
recentTransactionWidget("Digital Wallet", "Online Transaction", "\$350.00", "Nov 23, 2023", LucideIcons.gift, contentTheme.primary),
|
||||
MySpacing.height(24),
|
||||
recentTransactionWidget("Bank Account", "Purchase", "\$150.00", "Nov 23, 2023", LucideIcons.coins, contentTheme.success),
|
||||
MySpacing.height(24),
|
||||
recentTransactionWidget("PayPal", "Transfer", "\$500.00", "Nov 22, 2023", LucideIcons.shopping_cart, contentTheme.purple),
|
||||
MySpacing.height(24),
|
||||
recentTransactionWidget("Digital Wallet", "Bill Payment", "\$120.00", "Nov 21, 2023", LucideIcons.wallet, contentTheme.warning),
|
||||
MySpacing.height(24),
|
||||
recentTransactionWidget("Credit Card", "Subscription", "\$20.00", "Nov 20, 2023", LucideIcons.id_card, contentTheme.danger),
|
||||
MySpacing.height(24),
|
||||
recentTransactionWidget("Digital Wallet", "Refund", "\$100.00", "Nov 19, 2023", LucideIcons.circle_arrow_up, contentTheme.info),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget countryWiseSale() {
|
||||
Widget countryWiseSaleWidget(String image, name, count) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
paddingAll: 0,
|
||||
height: 44,
|
||||
width: 44,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
child: Image.asset(image, fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(child: MyText.bodyMedium(name, fontWeight: 600, overflow: TextOverflow.ellipsis)),
|
||||
MyContainer(
|
||||
borderRadiusAll: 8,
|
||||
padding: MySpacing.xy(8, 8),
|
||||
color: contentTheme.success.withAlpha(36),
|
||||
child: MyText.bodySmall(numberFormatter(count), fontWeight: 600, color: contentTheme.success),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
height: 475,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Country wise sale", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
countryWiseSaleWidget('assets/country/united_states.png', "France", "25000"),
|
||||
MySpacing.height(24),
|
||||
countryWiseSaleWidget('assets/country/argentina.png', "Brazil", "17500"),
|
||||
MySpacing.height(24),
|
||||
countryWiseSaleWidget('assets/country/germany.png', "India", "12500"),
|
||||
MySpacing.height(24),
|
||||
countryWiseSaleWidget('assets/country/mexico.png', "Japan", "22000"),
|
||||
MySpacing.height(24),
|
||||
countryWiseSaleWidget('assets/country/russia.png', "United Kingdom", "30000"),
|
||||
MySpacing.height(24),
|
||||
countryWiseSaleWidget('assets/country/canada.png', "Australia", "18000"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget recentActivity() {
|
||||
Widget recentActivityWidget(String title) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyContainer.rounded(paddingAll: 4, child: MyContainer.rounded(paddingAll: 4, color: contentTheme.primary, child: MyContainer.rounded(paddingAll: 4))),
|
||||
MySpacing.width(12),
|
||||
Expanded(child: MyText.bodyMedium(title, fontWeight: 600, maxLines: 2))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
height: 475,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Recent Activity", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget(
|
||||
"Ava Thompson placed an order for 3 units of Wireless Earbuds, 2 units of Bluetooth Speakers, and 1 unit of Smart Home Thermostat"),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget(
|
||||
"Noah Jackson upgraded his subscription to the Gold service plan, gaining access to premium features, priority support, and 20GB cloud storage for his team of 15 employees."),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget(
|
||||
"Emma Harris added 5 new items to the shopping cart, including a designer handbag, a set of luxury skincare products, a Bluetooth speaker, a fitness tracker, and a pair of leather boots"),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget(
|
||||
"Liam Davis purchased 1 unit of Electric Scooter, along with an additional helmet and charging station for enhanced convenience. Delivery expected in 5-7 business days."),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget(
|
||||
"Mason Martinez returned 2 units of Smartphone, citing dissatisfaction with the camera quality. The items will be processed for a full refund once inspected."),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget(
|
||||
"Isabella Robinson upgraded to the Platinum Plan with 5 users, unlocking advanced analytics tools, exclusive discounts, and premium customer support for her growing e-commerce team."),
|
||||
MySpacing.height(24),
|
||||
recentActivityWidget(
|
||||
"Elijah Walker completed a purchase of 6 ergonomic office chairs, 3 sit-stand desks, and a set of new monitor arms for the team, improving the comfort and productivity of the workspace."),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget dealSource() {
|
||||
Widget dealSourceWidget(String image, String title, String subtitle, String totalLeads) {
|
||||
return Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
height: 44,
|
||||
width: 44,
|
||||
paddingAll: 0,
|
||||
child: Image.asset(image, fit: BoxFit.cover),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(title, fontWeight: 600, maxLines: 1),
|
||||
MyText.bodySmall(subtitle, maxLines: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyText.bodyMedium('\$$totalLeads', fontWeight: 600),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
height: 475,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Deal Source", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
dealSourceWidget("assets/social/uxerflow_logo.png", "Website", "userflow.com", "50"),
|
||||
MySpacing.height(24),
|
||||
dealSourceWidget("assets/social/dribbble-logo.png", "Dribbble", "dribbble.com", "50"),
|
||||
MySpacing.height(24),
|
||||
dealSourceWidget("assets/social/facebook-logo.png", "Facebook", "facebook.com", "50"),
|
||||
MySpacing.height(24),
|
||||
dealSourceWidget("assets/social/instagram-logo.png", "Instagram", "instagram.com", "50"),
|
||||
MySpacing.height(24),
|
||||
dealSourceWidget("assets/social/LinkedIn-logo.png", "Linkedin", "linkedin.com", "50"),
|
||||
MySpacing.height(24),
|
||||
dealSourceWidget("assets/social/twitter-logo.png", "Twitter", "twitter.com", "50"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget recentOrder() {
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withValues(alpha:0.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("Recent Order", fontWeight: 600),
|
||||
MySpacing.height(24),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
sortAscending: true,
|
||||
columnSpacing: 150,
|
||||
onSelectAll: (_) => {},
|
||||
headingRowColor: WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)),
|
||||
dataRowMaxHeight: 60,
|
||||
showBottomBorder: true,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
border: TableBorder.all(borderRadius: BorderRadius.circular(4), style: BorderStyle.solid, width: .4, color: Colors.grey),
|
||||
columns: [
|
||||
DataColumn(label: MyText.labelLarge('Product', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Quantity', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Customer', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Status', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Price', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Date', color: contentTheme.primary)),
|
||||
DataColumn(label: MyText.labelLarge('Action', color: contentTheme.primary)),
|
||||
],
|
||||
rows: controller.recentOrder
|
||||
.mapIndexed((index, data) => DataRow(cells: [
|
||||
DataCell(MyText.labelMedium(data.productName)),
|
||||
DataCell(MyText.labelMedium('${data.quantity}')),
|
||||
DataCell(Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
height: 32,
|
||||
width: 32,
|
||||
paddingAll: 0,
|
||||
child: Image.asset(Images.avatars[index % Images.avatars.length]),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
MyText.labelMedium('${data.customer}'),
|
||||
],
|
||||
)),
|
||||
DataCell(
|
||||
MyContainer(
|
||||
paddingAll: 4,
|
||||
color: data.status == 'Shipped'
|
||||
? contentTheme.success
|
||||
: data.status == 'Delivery'
|
||||
? contentTheme.info
|
||||
: contentTheme.danger,
|
||||
child: MyText.labelMedium(
|
||||
'${data.status}',
|
||||
color: data.status == 'Success' ? contentTheme.onSuccess : contentTheme.onDanger,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(MyText.labelMedium('\$${data.price}')),
|
||||
DataCell(MyText.labelMedium('${Utils.getDateTimeStringFromDateTime(data.orderDate)}')),
|
||||
DataCell(Row(
|
||||
children: [
|
||||
MyContainer.rounded(
|
||||
onTap: () {},
|
||||
paddingAll: 8,
|
||||
color: contentTheme.primary,
|
||||
child: Icon(LucideIcons.eye, size: 16, color: contentTheme.onPrimary),
|
||||
),
|
||||
MySpacing.width(20),
|
||||
MyContainer.rounded(
|
||||
onTap: () {},
|
||||
paddingAll: 8,
|
||||
color: contentTheme.secondary,
|
||||
child: Icon(LucideIcons.pencil, size: 16, color: contentTheme.onSecondary),
|
||||
),
|
||||
],
|
||||
)),
|
||||
]))
|
||||
.toList()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -8,13 +8,12 @@ import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/model/employees/employees_screen_filter_sheet.dart';
|
||||
import 'package:marco/model/employees/add_employee_bottom_sheet.dart';
|
||||
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:marco/model/employees/employee_detail_bottom_sheet.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
class EmployeesScreen extends StatefulWidget {
|
||||
const EmployeesScreen({super.key});
|
||||
@ -28,58 +27,101 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
Get.put(EmployeesScreenController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
Future<void> _refreshEmployees() async {
|
||||
try {
|
||||
final selectedProjectId =
|
||||
Get.find<ProjectController>().selectedProject?.id;
|
||||
final isAllSelected =
|
||||
employeeScreenController.isAllEmployeeSelected.value;
|
||||
|
||||
Future<void> _openFilterSheet() async {
|
||||
final result = await showModalBottomSheet<Map<String, dynamic>>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
builder: (context) => EmployeesScreenFilterSheet(
|
||||
controller: employeeScreenController,
|
||||
permissionController: permissionController,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
final selectedProjectId = result['projectId'] as String?;
|
||||
if (selectedProjectId != employeeScreenController.selectedProjectId) {
|
||||
if (isAllSelected) {
|
||||
employeeScreenController.selectedProjectId = null;
|
||||
await employeeScreenController.fetchAllEmployees();
|
||||
} else if (selectedProjectId != null) {
|
||||
employeeScreenController.selectedProjectId = selectedProjectId;
|
||||
|
||||
try {
|
||||
if (selectedProjectId == null) {
|
||||
await employeeScreenController.fetchAllEmployees();
|
||||
} else {
|
||||
await employeeScreenController
|
||||
.fetchEmployeesByProject(selectedProjectId);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching employees: ${e.toString()}');
|
||||
}
|
||||
|
||||
employeeScreenController.update(['employee_screen_controller']);
|
||||
await employeeScreenController
|
||||
.fetchEmployeesByProject(selectedProjectId);
|
||||
} else {
|
||||
// ❗ Clear employees if neither selected
|
||||
employeeScreenController.clearEmployees();
|
||||
}
|
||||
|
||||
employeeScreenController.update(['employee_screen_controller']);
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Error refreshing employee data: ${e.toString()}');
|
||||
debugPrintStack(stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshEmployees() async {
|
||||
try {
|
||||
final projectId = employeeScreenController.selectedProjectId;
|
||||
if (projectId == null) {
|
||||
await employeeScreenController.fetchAllEmployees();
|
||||
} else {
|
||||
await employeeScreenController.fetchEmployeesByProject(projectId);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error refreshing employee data: ${e.toString()}');
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final selectedProjectId = Get.find<ProjectController>().selectedProject?.id;
|
||||
|
||||
if (selectedProjectId != null) {
|
||||
employeeScreenController.selectedProjectId = selectedProjectId;
|
||||
employeeScreenController.fetchEmployeesByProject(selectedProjectId);
|
||||
} else if (employeeScreenController.isAllEmployeeSelected.value) {
|
||||
employeeScreenController.selectedProjectId = null;
|
||||
employeeScreenController.fetchAllEmployees();
|
||||
} else {
|
||||
employeeScreenController.clearEmployees();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(80),
|
||||
child: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
foregroundColor: Colors.black,
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(top: 15.0), // Aligns with title
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () {
|
||||
Get.offNamed('/dashboard');
|
||||
},
|
||||
),
|
||||
),
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(top: 15.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Employees',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: InkWell(
|
||||
onTap: () async {
|
||||
final result = await showModalBottomSheet<bool>(
|
||||
@ -120,13 +162,14 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
),
|
||||
),
|
||||
),
|
||||
child: GetBuilder<EmployeesScreenController>(
|
||||
init: employeeScreenController,
|
||||
tag: 'employee_screen_controller',
|
||||
builder: (controller) {
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
body: SafeArea(
|
||||
child: GetBuilder<EmployeesScreenController>(
|
||||
init: employeeScreenController,
|
||||
tag: 'employee_screen_controller',
|
||||
builder: (controller) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 80),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
@ -134,11 +177,6 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium(
|
||||
"Employees",
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
@ -154,33 +192,67 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
"Filter",
|
||||
fontWeight: 600,
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Project',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: _openFilterSheet,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Icon(
|
||||
Icons.filter_list_alt,
|
||||
color: Colors.blueAccent,
|
||||
size: 28,
|
||||
Obx(() {
|
||||
return Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: employeeScreenController
|
||||
.isAllEmployeeSelected.value,
|
||||
activeColor: Colors.blueAccent,
|
||||
fillColor:
|
||||
MaterialStateProperty.resolveWith<Color?>(
|
||||
(states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return Colors.blueAccent;
|
||||
}
|
||||
return Colors.transparent;
|
||||
}),
|
||||
checkColor: Colors.white,
|
||||
side: BorderSide(
|
||||
color: Colors.black,
|
||||
width: 2,
|
||||
),
|
||||
onChanged: (value) async {
|
||||
employeeScreenController
|
||||
.isAllEmployeeSelected.value = value!;
|
||||
|
||||
if (value) {
|
||||
employeeScreenController.selectedProjectId =
|
||||
null;
|
||||
await employeeScreenController
|
||||
.fetchAllEmployees();
|
||||
} else {
|
||||
final selectedProjectId =
|
||||
Get.find<ProjectController>()
|
||||
.selectedProject
|
||||
?.id;
|
||||
|
||||
if (selectedProjectId != null) {
|
||||
employeeScreenController
|
||||
.selectedProjectId =
|
||||
selectedProjectId;
|
||||
await employeeScreenController
|
||||
.fetchEmployeesByProject(
|
||||
selectedProjectId);
|
||||
} else {
|
||||
// ✅ THIS is your critical path
|
||||
employeeScreenController.clearEmployees();
|
||||
}
|
||||
}
|
||||
|
||||
employeeScreenController
|
||||
.update(['employee_screen_controller']);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
MyText.bodyMedium(
|
||||
"Refresh",
|
||||
fontWeight: 600,
|
||||
),
|
||||
MyText.bodyMedium(
|
||||
"All Employees",
|
||||
fontWeight: 600,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
const SizedBox(width: 16),
|
||||
MyText.bodyMedium("Refresh", fontWeight: 600),
|
||||
Tooltip(
|
||||
message: 'Refresh Data',
|
||||
child: InkWell(
|
||||
@ -208,9 +280,9 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -219,16 +291,17 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
return Obx(() {
|
||||
final isLoading = employeeScreenController.isLoading.value;
|
||||
final employees = employeeScreenController.employees;
|
||||
|
||||
if (isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (employees.isEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodySmall(
|
||||
"No Assigned Employees Found",
|
||||
fontWeight: 600,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 50),
|
||||
child: Center(
|
||||
child: MyText.bodySmall(
|
||||
"No Assigned Employees Found",
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,337 +1,285 @@
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:marco/controller/layout/layout_controller.dart';
|
||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/widgets/my_button.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_dashed_divider.dart';
|
||||
import 'package:marco/helpers/widgets/my_responsive.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/view/layouts/left_bar.dart';
|
||||
import 'package:marco/view/layouts/right_bar.dart';
|
||||
import 'package:marco/view/layouts/top_bar.dart';
|
||||
import 'package:marco/widgets/custom_pop_menu.dart';
|
||||
import 'package:marco/controller/layout/layout_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_responsive.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/model/employee_info.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||
import 'package:marco/images.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:marco/view/layouts/user_profile_right_bar.dart';
|
||||
|
||||
class Layout extends StatelessWidget {
|
||||
class Layout extends StatefulWidget {
|
||||
final Widget? child;
|
||||
final Widget? floatingActionButton;
|
||||
|
||||
const Layout({super.key, this.child, this.floatingActionButton});
|
||||
|
||||
@override
|
||||
State<Layout> createState() => _LayoutState();
|
||||
}
|
||||
|
||||
class _LayoutState extends State<Layout> {
|
||||
final LayoutController controller = LayoutController();
|
||||
final topBarTheme = AdminTheme.theme.topBarTheme;
|
||||
final contentTheme = AdminTheme.theme.contentTheme;
|
||||
final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo();
|
||||
final bool isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage");
|
||||
final projectController = Get.find<ProjectController>();
|
||||
|
||||
Layout({super.key, this.child, this.floatingActionButton});
|
||||
|
||||
bool get isBetaEnvironment => ApiEndpoints.baseUrl.contains("stage");
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyResponsive(builder: (BuildContext context, _, screenMT) {
|
||||
return MyResponsive(builder: (context, _, screenMT) {
|
||||
return GetBuilder(
|
||||
init: controller,
|
||||
builder: (controller) {
|
||||
if (screenMT.isMobile || screenMT.isTablet) {
|
||||
return mobileScreen();
|
||||
} else {
|
||||
return largeScreen();
|
||||
}
|
||||
});
|
||||
init: controller,
|
||||
builder: (_) {
|
||||
return (screenMT.isMobile || screenMT.isTablet)
|
||||
? _buildScaffold(context, isMobile: true)
|
||||
: _buildScaffold(context);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget mobileScreen() {
|
||||
Widget _buildScaffold(BuildContext context, {bool isMobile = false}) {
|
||||
return Scaffold(
|
||||
key: controller.scaffoldKey,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
actions: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.width(6),
|
||||
if (isBetaEnvironment)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 17.0, horizontal: 8.0),
|
||||
endDrawer: UserProfileBar(),
|
||||
floatingActionButton: widget.floatingActionButton,
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
_buildHeader(context, isMobile),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
key: controller.scrollKey,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 0, vertical: isMobile ? 16 : 32),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Overlay project list below header
|
||||
Obx(() {
|
||||
if (!projectController.isProjectSelectionExpanded.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Positioned(
|
||||
top: 95, // Adjust based on header card height
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueAccent,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'BETA',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: _buildProjectList(context, isMobile),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context, bool isMobile) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
child: Obx(() {
|
||||
final isExpanded = projectController.isProjectSelectionExpanded.value;
|
||||
final selectedProjectId = projectController.selectedProjectId?.value;
|
||||
final selectedProject = projectController.projects.firstWhereOrNull(
|
||||
(p) => p.id == selectedProjectId,
|
||||
);
|
||||
|
||||
final hasProjects = projectController.projects.isNotEmpty;
|
||||
|
||||
if (!hasProjects) {
|
||||
projectController.selectedProjectId?.value = '';
|
||||
} else if (selectedProject == null) {
|
||||
projectController
|
||||
.updateSelectedProject(projectController.projects.first.id);
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
Images.logoDark,
|
||||
height: 50,
|
||||
width: 50,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: hasProjects
|
||||
? GestureDetector(
|
||||
onTap: () => projectController
|
||||
.isProjectSelectionExpanded
|
||||
.toggle(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.bodyLarge(
|
||||
selectedProject?.name ??
|
||||
"Select Project",
|
||||
fontWeight: 700,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isExpanded
|
||||
? Icons.arrow_drop_up_outlined
|
||||
: Icons
|
||||
.arrow_drop_down_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MyText.bodyMedium(
|
||||
"Hi, ${employeeInfo?.firstName ?? ''}",
|
||||
color: Colors.black54,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyLarge(
|
||||
"No Project Assigned",
|
||||
fontWeight: 700,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
MyText.bodyMedium(
|
||||
"Hi, ${employeeInfo?.firstName ?? ''}",
|
||||
color: Colors.black54,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isBetaEnvironment)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.deepPurple,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: MyText.bodySmall(
|
||||
'BETA',
|
||||
color: Colors.white,
|
||||
fontWeight: 700,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
onPressed: () =>
|
||||
controller.scaffoldKey.currentState?.openEndDrawer(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Expanded Project List inside card — only show if projects exist
|
||||
if (isExpanded && hasProjects)
|
||||
Positioned(
|
||||
top: 70,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
color: Colors.white,
|
||||
child: _buildProjectList(context, isMobile),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.width(6),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Get.toNamed('/dashboard');
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProjectList(BuildContext context, bool isMobile) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall("Switch Project", fontWeight: 600),
|
||||
const SizedBox(height: 4),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight:
|
||||
isMobile ? MediaQuery.of(context).size.height * 0.4 : 400,
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: projectController.projects.length,
|
||||
itemBuilder: (context, index) {
|
||||
final project = projectController.projects[index];
|
||||
final selectedId = projectController.selectedProjectId?.value;
|
||||
final isSelected = project.id == selectedId;
|
||||
|
||||
return RadioListTile<String>(
|
||||
value: project.id,
|
||||
groupValue: selectedId,
|
||||
onChanged: (value) {
|
||||
projectController.updateSelectedProject(value!);
|
||||
projectController.isProjectSelectionExpanded.value = false;
|
||||
},
|
||||
title: Text(
|
||||
project.name,
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected ? Colors.blueAccent : Colors.black87,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 0),
|
||||
activeColor: Colors.blueAccent,
|
||||
tileColor: isSelected
|
||||
? Colors.blueAccent.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
splashColor: contentTheme.primary.withAlpha(20),
|
||||
child: Padding(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
child: Icon(
|
||||
LucideIcons.layout_dashboard,
|
||||
size: 18,
|
||||
color: Colors.blueAccent,
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
CustomPopupMenu(
|
||||
backdrop: true,
|
||||
onChange: (_) {},
|
||||
offsetX: -180,
|
||||
menu: Padding(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
LucideIcons.bell,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
menuBuilder: (_) => buildNotifications(),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
CustomPopupMenu(
|
||||
backdrop: true,
|
||||
onChange: (_) {},
|
||||
offsetX: -90,
|
||||
offsetY: 0,
|
||||
menu: Padding(
|
||||
padding: MySpacing.xy(0, 8),
|
||||
child: MyContainer.rounded(
|
||||
paddingAll: 0,
|
||||
child: Avatar(
|
||||
firstName: employeeInfo?.firstName ?? 'First',
|
||||
lastName: employeeInfo?.lastName ?? 'Name',
|
||||
),
|
||||
),
|
||||
),
|
||||
menuBuilder: (_) => buildAccountMenu(),
|
||||
),
|
||||
MySpacing.width(20)
|
||||
],
|
||||
),
|
||||
drawer: LeftBar(),
|
||||
floatingActionButton: floatingActionButton,
|
||||
body: SingleChildScrollView(
|
||||
key: controller.scrollKey,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget largeScreen() {
|
||||
return Scaffold(
|
||||
key: controller.scaffoldKey,
|
||||
endDrawer: RightBar(),
|
||||
floatingActionButton: floatingActionButton,
|
||||
body: Row(
|
||||
children: [
|
||||
LeftBar(isCondensed: ThemeCustomizer.instance.leftBarCondensed),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
child: SingleChildScrollView(
|
||||
padding:
|
||||
MySpacing.fromLTRB(0, 58 + flexSpacing, 0, flexSpacing),
|
||||
key: controller.scrollKey,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
Positioned(top: 0, left: 0, right: 0, child: TopBar()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildNotifications() {
|
||||
Widget buildNotification(String title, String description) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelLarge(title),
|
||||
MySpacing.height(4),
|
||||
MyText.bodySmall(description)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return MyContainer.bordered(
|
||||
paddingAll: 0,
|
||||
width: 250,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.xy(16, 12),
|
||||
child: MyText.titleMedium("Notification", fontWeight: 600),
|
||||
),
|
||||
MyDashedDivider(
|
||||
height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6),
|
||||
Padding(
|
||||
padding: MySpacing.xy(16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildNotification("Welcome to Marco",
|
||||
"Welcome to Marco, we are glad to have you here"),
|
||||
MySpacing.height(12),
|
||||
buildNotification("New update available",
|
||||
"There is a new update available for your app"),
|
||||
],
|
||||
),
|
||||
),
|
||||
MyDashedDivider(
|
||||
height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6),
|
||||
Padding(
|
||||
padding: MySpacing.xy(16, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyButton.text(
|
||||
onPressed: () {},
|
||||
splashColor: contentTheme.primary.withAlpha(28),
|
||||
child: MyText.labelSmall(
|
||||
"View All",
|
||||
color: contentTheme.primary,
|
||||
),
|
||||
),
|
||||
MyButton.text(
|
||||
onPressed: () {},
|
||||
splashColor: contentTheme.danger.withAlpha(28),
|
||||
child: MyText.labelSmall(
|
||||
"Clear",
|
||||
color: contentTheme.danger,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAccountMenu() {
|
||||
return MyContainer.bordered(
|
||||
paddingAll: 0,
|
||||
width: 150,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyButton(
|
||||
onPressed:null,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
borderRadiusAll: AppStyle.buttonRadius.medium,
|
||||
padding: MySpacing.xy(8, 4),
|
||||
splashColor: contentTheme.onBackground.withAlpha(20),
|
||||
backgroundColor: const Color.fromARGB(0, 220, 218, 218),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.user,
|
||||
size: 14,
|
||||
color: contentTheme.onBackground,
|
||||
),
|
||||
MySpacing.width(8),
|
||||
MyText.labelMedium(
|
||||
"My Account",
|
||||
fontWeight: 600,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(4),
|
||||
MyButton(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed:null,
|
||||
borderRadiusAll: AppStyle.buttonRadius.medium,
|
||||
padding: MySpacing.xy(8, 4),
|
||||
splashColor: contentTheme.onBackground.withAlpha(20),
|
||||
backgroundColor: const Color.fromARGB(0, 220, 218, 218),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.settings,
|
||||
size: 14,
|
||||
color: contentTheme.onBackground,
|
||||
),
|
||||
MySpacing.width(8),
|
||||
MyText.labelMedium(
|
||||
"Settings",
|
||||
fontWeight: 600,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
MyButton(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () async {
|
||||
await LocalStorage.logout();
|
||||
},
|
||||
borderRadiusAll: AppStyle.buttonRadius.medium,
|
||||
padding: MySpacing.xy(8, 4),
|
||||
splashColor: contentTheme.onBackground.withAlpha(20),
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.log_out,
|
||||
size: 14,
|
||||
color: contentTheme.onBackground,
|
||||
),
|
||||
MySpacing.width(8),
|
||||
MyText.labelMedium(
|
||||
"Logout",
|
||||
fontWeight: 600,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
313
lib/view/layouts/user_profile_right_bar.dart
Normal file
@ -0,0 +1,313 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:marco/model/employee_info.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
|
||||
class UserProfileBar extends StatefulWidget {
|
||||
final bool isCondensed;
|
||||
|
||||
const UserProfileBar({super.key, this.isCondensed = false});
|
||||
|
||||
@override
|
||||
_UserProfileBarState createState() => _UserProfileBarState();
|
||||
}
|
||||
|
||||
class _UserProfileBarState extends State<UserProfileBar>
|
||||
with SingleTickerProviderStateMixin, UIMixin {
|
||||
final ThemeCustomizer customizer = ThemeCustomizer.instance;
|
||||
bool isCondensed = false;
|
||||
EmployeeInfo? employeeInfo;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadEmployeeInfo();
|
||||
}
|
||||
|
||||
void _loadEmployeeInfo() {
|
||||
setState(() {
|
||||
employeeInfo = LocalStorage.getEmployeeInfo();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
isCondensed = widget.isCondensed;
|
||||
|
||||
return MyCard(
|
||||
borderRadiusAll: 16,
|
||||
paddingAll: 0,
|
||||
shadow: MyShadow(position: MyShadowPosition.centerRight, elevation: 4),
|
||||
child: AnimatedContainer(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
leftBarTheme.background.withOpacity(0.95),
|
||||
leftBarTheme.background.withOpacity(0.85),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
width: isCondensed ? 90 : 250,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
child: Column(
|
||||
children: [
|
||||
userProfileSection(),
|
||||
MySpacing.height(8),
|
||||
supportAndSettingsMenu(),
|
||||
const Spacer(),
|
||||
logoutButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget userProfileSection() {
|
||||
if (employeeInfo == null) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: MySpacing.xy(58, 68),
|
||||
decoration: BoxDecoration(
|
||||
color: leftBarTheme.activeItemBackground,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: employeeInfo?.firstName ?? 'First',
|
||||
lastName: employeeInfo?.lastName ?? 'Name',
|
||||
size: 60,
|
||||
),
|
||||
MySpacing.height(12),
|
||||
MyText.labelLarge(
|
||||
"${employeeInfo?.firstName ?? 'First'} ${employeeInfo?.lastName ?? 'Last'}",
|
||||
fontWeight: 700,
|
||||
color: leftBarTheme.activeItemColor,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget supportAndSettingsMenu() {
|
||||
return Padding(
|
||||
padding: MySpacing.xy(16, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
menuItem(icon: LucideIcons.settings, label: "Settings"),
|
||||
MySpacing.height(12),
|
||||
menuItem(icon: LucideIcons.badge_help, label: "Support"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget menuItem({required IconData icon, required String label}) {
|
||||
return InkWell(
|
||||
onTap: () {},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.2),
|
||||
splashColor: leftBarTheme.activeItemBackground.withOpacity(0.3),
|
||||
child: Padding(
|
||||
padding: MySpacing.xy(12, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: leftBarTheme.onBackground),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(
|
||||
label,
|
||||
color: leftBarTheme.onBackground,
|
||||
fontWeight: 500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget logoutButton() {
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.log_out,
|
||||
size: 48,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Logout Confirmation",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"Are you sure you want to logout?\nYou will need to login again to continue.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.grey.shade700,
|
||||
),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await LocalStorage.logout();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.redAccent,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text("Logout"),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
// Show animated loader dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
"Logging you out...",
|
||||
style: TextStyle(color: Colors.white),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await LocalStorage.logout();
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(LucideIcons.check, color: Colors.green),
|
||||
const SizedBox(width: 12),
|
||||
const Text("You’ve been logged out successfully."),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.grey.shade900,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.2),
|
||||
splashColor: leftBarTheme.activeItemBackground.withOpacity(0.3),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeInOut,
|
||||
width: double.infinity,
|
||||
padding: MySpacing.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: leftBarTheme.activeItemBackground,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
"Logout",
|
||||
color: leftBarTheme.activeItemColor,
|
||||
fontWeight: 600,
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Icon(
|
||||
LucideIcons.log_out,
|
||||
size: 20,
|
||||
color: leftBarTheme.activeItemColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
95
lib/view/my_app.dart
Normal file
@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'package:marco/helpers/extensions/app_localization_delegate.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/services/localizations/language.dart';
|
||||
import 'package:marco/helpers/services/navigation_services.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/theme/app_notifier.dart';
|
||||
import 'package:marco/routes.dart';
|
||||
|
||||
final Logger logger = Logger();
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
Future<String> _getInitialRoute() async {
|
||||
try {
|
||||
if (!AuthService.isLoggedIn) {
|
||||
logger.i("User not logged in. Routing to /auth/login-option");
|
||||
return "/auth/login-option";
|
||||
}
|
||||
|
||||
final bool hasMpin = LocalStorage.getIsMpin();
|
||||
logger.i("MPIN enabled: $hasMpin");
|
||||
|
||||
if (hasMpin) {
|
||||
await LocalStorage.setBool("mpin_verified", false);
|
||||
logger
|
||||
.i("Routing to /auth/mpin-auth and setting mpin_verified to false");
|
||||
return "/auth/mpin-auth";
|
||||
} else {
|
||||
logger.i("MPIN not enabled. Routing to /home");
|
||||
return "/dashboard";
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
logger.e("Error determining initial route",
|
||||
error: e, stackTrace: stacktrace);
|
||||
return "/auth/login-option";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<AppNotifier>(
|
||||
builder: (_, notifier, __) {
|
||||
return FutureBuilder<String>(
|
||||
future: _getInitialRoute(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return const MaterialApp(
|
||||
home: Center(child: Text("Error determining route")),
|
||||
);
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return const MaterialApp(
|
||||
home: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return GetMaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ThemeCustomizer.instance.theme,
|
||||
navigatorKey: NavigationService.navigatorKey,
|
||||
initialRoute: snapshot.data!,
|
||||
getPages: getPageRoute(),
|
||||
builder: (context, child) {
|
||||
NavigationService.registerContext(context);
|
||||
return Directionality(
|
||||
textDirection: AppTheme.textDirection,
|
||||
child: child ?? const SizedBox(),
|
||||
);
|
||||
},
|
||||
localizationsDelegates: [
|
||||
AppLocalizationsDelegate(context),
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: Language.getLocales(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,303 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/task_planing/report_task_controller.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_button.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
|
||||
|
||||
class CommentTaskScreen extends StatefulWidget {
|
||||
const CommentTaskScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CommentTaskScreen> createState() => _CommentTaskScreenState();
|
||||
}
|
||||
class _Member {
|
||||
final String firstName;
|
||||
_Member(this.firstName);
|
||||
}
|
||||
|
||||
class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin {
|
||||
final ReportTaskController controller = Get.put(ReportTaskController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final taskData = Get.arguments as Map<String, dynamic>;
|
||||
print("Task Data: $taskData");
|
||||
controller.basicValidator.getController('assigned_date')?.text =
|
||||
taskData['assignedOn'] ?? '';
|
||||
controller.basicValidator.getController('assigned_by')?.text =
|
||||
taskData['assignedBy'] ?? '';
|
||||
controller.basicValidator.getController('work_area')?.text =
|
||||
taskData['location'] ?? '';
|
||||
controller.basicValidator.getController('activity')?.text =
|
||||
taskData['activity'] ?? '';
|
||||
controller.basicValidator.getController('planned_work')?.text =
|
||||
taskData['plannedWork'] ?? '';
|
||||
controller.basicValidator.getController('completed_work')?.text =
|
||||
taskData['completedWork'] ?? '';
|
||||
controller.basicValidator.getController('team_members')?.text =
|
||||
(taskData['teamMembers'] as List<dynamic>).join(', ');
|
||||
controller.basicValidator.getController('assigned')?.text =
|
||||
taskData['assigned'] ?? '';
|
||||
controller.basicValidator.getController('task_id')?.text =
|
||||
taskData['taskId'] ?? '';
|
||||
|
||||
return Layout(
|
||||
child: GetBuilder<ReportTaskController>(
|
||||
init: controller,
|
||||
tag: 'comment_task_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Comment Task",
|
||||
fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Daily Progress Report'),
|
||||
MyBreadcrumbItem(name: 'Comment Task'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(
|
||||
children: [
|
||||
MyFlexItem(sizes: "lg-8 md-12", child: detail()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget detail() {
|
||||
return Form(
|
||||
key: controller.basicValidator.formKey,
|
||||
child: MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(LucideIcons.server, size: 16),
|
||||
MySpacing.width(12),
|
||||
MyText.titleMedium("Activity Summary", fontWeight: 600),
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
// Static fields
|
||||
buildRow(
|
||||
"Assigned By",
|
||||
controller.basicValidator
|
||||
.getController('assigned_by')
|
||||
?.text
|
||||
.trim()),
|
||||
buildRow(
|
||||
"Work Area",
|
||||
controller.basicValidator
|
||||
.getController('work_area')
|
||||
?.text
|
||||
.trim()),
|
||||
buildRow(
|
||||
"Activity",
|
||||
controller.basicValidator
|
||||
.getController('activity')
|
||||
?.text
|
||||
.trim()),
|
||||
buildRow(
|
||||
"Planned Work",
|
||||
controller.basicValidator
|
||||
.getController('planned_work')
|
||||
?.text
|
||||
.trim()),
|
||||
buildRow(
|
||||
"Completed Work",
|
||||
controller.basicValidator
|
||||
.getController('completed_work')
|
||||
?.text
|
||||
.trim()),
|
||||
buildTeamMembers(),
|
||||
|
||||
MyText.labelMedium("Comment"),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
validator: controller.basicValidator.getValidation('comment'),
|
||||
controller: controller.basicValidator.getController('comment'),
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
hintText: "eg: Work done successfully",
|
||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||
border: outlineInputBorder,
|
||||
enabledBorder: outlineInputBorder,
|
||||
focusedBorder: focusedInputBorder,
|
||||
contentPadding: MySpacing.all(16),
|
||||
isCollapsed: true,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
|
||||
// Buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
MyButton.text(
|
||||
onPressed: () => Get.back(),
|
||||
padding: MySpacing.xy(20, 16),
|
||||
splashColor: contentTheme.secondary.withValues(alpha: 0.1),
|
||||
child: MyText.bodySmall('Cancel'),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
MyButton(
|
||||
onPressed: () async {
|
||||
if (controller.basicValidator.validateForm()) {
|
||||
await controller.reportTask(
|
||||
projectId: controller.basicValidator
|
||||
.getController('task_id')
|
||||
?.text ??
|
||||
'', // Replace with actual ID
|
||||
comment: controller.basicValidator
|
||||
.getController('comment')
|
||||
?.text ??
|
||||
'',
|
||||
completedTask: int.tryParse(controller.basicValidator
|
||||
.getController('completed_work')
|
||||
?.text ??
|
||||
'') ??
|
||||
0,
|
||||
checklist: [],
|
||||
reportedDate: DateTime.now(),
|
||||
);
|
||||
}
|
||||
},
|
||||
elevation: 0,
|
||||
padding: MySpacing.xy(20, 16),
|
||||
backgroundColor: contentTheme.primary,
|
||||
borderRadiusAll: AppStyle.buttonRadius.medium,
|
||||
child: MyText.bodySmall(
|
||||
'Comment',
|
||||
color: contentTheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Loading spinner
|
||||
Obx(() {
|
||||
return controller.isLoading.value
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget buildTeamMembers() {
|
||||
final teamMembersText =
|
||||
controller.basicValidator.getController('team_members')?.text ?? '';
|
||||
final members = teamMembersText
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
MyText.labelMedium("Team Members:"),
|
||||
MySpacing.width(12),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
TeamBottomSheet.show(
|
||||
context: context,
|
||||
teamMembers: members.map((name) => _Member(name)).toList(),
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 32,
|
||||
width: 100,
|
||||
child: Stack(
|
||||
children: [
|
||||
for (int i = 0; i < members.length.clamp(0, 3); i++)
|
||||
Positioned(
|
||||
left: i * 24.0,
|
||||
child: Tooltip(
|
||||
message: members[i],
|
||||
child: Avatar(
|
||||
firstName: members[i],
|
||||
lastName: '',
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (members.length > 3)
|
||||
Positioned(
|
||||
left: 2 * 24.0,
|
||||
child: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
child: MyText.bodyMedium(
|
||||
'+${members.length - 3}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: Colors.black87),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildRow(String label, String? value) {
|
||||
print("Label: $label, Value: $value");
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium("$label:"),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(value?.isNotEmpty == true ? value! : "-"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4,19 +4,17 @@ import 'package:intl/intl.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/controller/dashboard/daily_task_controller.dart';
|
||||
import 'package:marco/model/dailyTaskPlaning/daily_progress_report_filter.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:marco/model/dailyTaskPlaning/comment_task_bottom_sheet.dart';
|
||||
import 'package:marco/model/dailyTaskPlaning/report_task_bottom_sheet.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
class DailyProgressReportScreen extends StatefulWidget {
|
||||
const DailyProgressReportScreen({super.key});
|
||||
@ -40,46 +38,110 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
Get.put(DailyTaskController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
final ProjectController projectController = Get.find<ProjectController>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final initialProjectId = projectController.selectedProjectId?.value;
|
||||
if (initialProjectId != null) {
|
||||
dailyTaskController.selectedProjectId = initialProjectId;
|
||||
dailyTaskController.fetchTaskData(initialProjectId);
|
||||
}
|
||||
|
||||
final selectedProjectIdRx = projectController.selectedProjectId;
|
||||
if (selectedProjectIdRx != null) {
|
||||
ever<String?>(
|
||||
selectedProjectIdRx,
|
||||
(newProjectId) async {
|
||||
if (newProjectId != null &&
|
||||
newProjectId != dailyTaskController.selectedProjectId) {
|
||||
dailyTaskController.selectedProjectId = newProjectId;
|
||||
await dailyTaskController.fetchTaskData(newProjectId);
|
||||
dailyTaskController.update(['daily_progress_report_controller']);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
debugPrint(
|
||||
"Warning: selectedProjectId is null, skipping listener setup.");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder<DailyTaskController>(
|
||||
init: dailyTaskController,
|
||||
tag: 'daily_progress_report_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
MySpacing.height(flexSpacing),
|
||||
_buildActionBar(),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: _buildDailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Daily Progress Report",
|
||||
fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(name: 'Daily Progress Report', active: true),
|
||||
],
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(80),
|
||||
child: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
foregroundColor: Colors.black,
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(top: 15.0), // Aligns with title
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () {
|
||||
Get.offNamed('/dashboard');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(top: 15.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Daily Task Progress',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: MySpacing.x(0),
|
||||
child: GetBuilder<DailyTaskController>(
|
||||
init: dailyTaskController,
|
||||
tag: 'daily_progress_report_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
_buildActionBar(),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: _buildDailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -551,8 +613,12 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
'text': comment.comment,
|
||||
'date': isoDate,
|
||||
'commentedBy': commenterName,
|
||||
'preSignedUrls':
|
||||
comment.preSignedUrls,
|
||||
};
|
||||
}).toList();
|
||||
final taskLevelPreSignedUrls =
|
||||
task.reportedPreSignedUrls;
|
||||
|
||||
final taskData = {
|
||||
'activity': activityName,
|
||||
@ -566,6 +632,8 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
'teamSize': task.teamMembers.length,
|
||||
'teamMembers': teamMembers,
|
||||
'taskComments': taskComments,
|
||||
'reportedPreSignedUrls':
|
||||
taskLevelPreSignedUrls,
|
||||
};
|
||||
|
||||
showModalBottomSheet(
|
||||
|
||||
@ -3,17 +3,14 @@ import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/model/dailyTaskPlaning/daily_task_planing_filter.dart';
|
||||
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
||||
import 'package:marco/model/dailyTaskPlaning/assign_task_bottom_sheet .dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
import 'package:marco/model/dailyTaskPlaning/assign_task_bottom_sheet .dart';
|
||||
|
||||
class DailyTaskPlaningScreen extends StatefulWidget {
|
||||
DailyTaskPlaningScreen({super.key});
|
||||
@ -28,149 +25,141 @@ class _DailyTaskPlaningScreenState extends State<DailyTaskPlaningScreen>
|
||||
Get.put(DailyTaskPlaningController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
final ProjectController projectController = Get.find<ProjectController>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initial fetch if a project is already selected
|
||||
final projectId = projectController.selectedProjectId?.value;
|
||||
if (projectId != null) {
|
||||
dailyTaskPlaningController.fetchTaskData(projectId);
|
||||
}
|
||||
|
||||
// Reactive fetch on project ID change
|
||||
final selectedProject = projectController.selectedProjectId;
|
||||
if (selectedProject != null) {
|
||||
ever<String?>(
|
||||
selectedProject,
|
||||
(newProjectId) {
|
||||
if (newProjectId != null) {
|
||||
dailyTaskPlaningController.fetchTaskData(newProjectId);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: GetBuilder<DailyTaskPlaningController>(
|
||||
init: dailyTaskPlaningController,
|
||||
tag: 'daily_task_planing_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Daily Task Planning",
|
||||
fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(80),
|
||||
child: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
foregroundColor: Colors.black,
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(top: 15.0), // Aligns with title
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () {
|
||||
Get.offNamed('/dashboard');
|
||||
},
|
||||
),
|
||||
),
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(top: 15.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Daily Task Planning',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: MySpacing.x(0),
|
||||
child: GetBuilder<DailyTaskPlaningController>(
|
||||
init: dailyTaskPlaningController,
|
||||
tag: 'daily_task_planing_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Dashboard'),
|
||||
MyBreadcrumbItem(
|
||||
name: 'Daily Task Planning', active: true),
|
||||
const SizedBox(width: 8),
|
||||
MyText.bodyMedium("Refresh", fontWeight: 600),
|
||||
Tooltip(
|
||||
message: 'Refresh Data',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: () async {
|
||||
final projectId =
|
||||
projectController.selectedProjectId?.value;
|
||||
if (projectId != null) {
|
||||
try {
|
||||
await dailyTaskPlaningController
|
||||
.fetchTaskData(projectId);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Error refreshing task data: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.refresh,
|
||||
color: Colors.green, size: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
"Filter",
|
||||
fontWeight: 600,
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Project',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: () async {
|
||||
final result =
|
||||
await showModalBottomSheet<Map<String, dynamic>>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12)),
|
||||
),
|
||||
builder: (context) => DailyTaskPlaningFilter(
|
||||
controller: dailyTaskPlaningController,
|
||||
permissionController: permissionController,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
final selectedProjectId =
|
||||
result['projectId'] as String?;
|
||||
|
||||
if (selectedProjectId != null &&
|
||||
selectedProjectId !=
|
||||
dailyTaskPlaningController
|
||||
.selectedProjectId) {
|
||||
// Update the controller's selected project ID
|
||||
dailyTaskPlaningController.selectedProjectId =
|
||||
selectedProjectId;
|
||||
|
||||
try {
|
||||
// Fetch tasks for the new project
|
||||
await dailyTaskPlaningController
|
||||
.fetchTaskData(selectedProjectId);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Error fetching task data: ${e.toString()}');
|
||||
}
|
||||
|
||||
// Update the UI
|
||||
dailyTaskPlaningController
|
||||
.update(['daily_task_planing_controller']);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.filter_list_alt,
|
||||
color: Colors.blueAccent,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
MyText.bodyMedium(
|
||||
"Refresh",
|
||||
fontWeight: 600,
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Refresh Data',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: () async {
|
||||
final projectId =
|
||||
dailyTaskPlaningController.selectedProjectId;
|
||||
if (projectId != null) {
|
||||
try {
|
||||
await dailyTaskPlaningController
|
||||
.fetchTaskData(projectId);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Error refreshing task data: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.green,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: dailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: dailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,269 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/task_planing/report_task_controller.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_button.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||
import 'package:marco/view/layouts/layout.dart';
|
||||
|
||||
class ReportTaskScreen extends StatefulWidget {
|
||||
const ReportTaskScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ReportTaskScreen> createState() => _ReportTaskScreenState();
|
||||
}
|
||||
|
||||
class _ReportTaskScreenState extends State<ReportTaskScreen> with UIMixin {
|
||||
final ReportTaskController controller = Get.put(ReportTaskController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final taskData = Get.arguments as Map<String, dynamic>;
|
||||
print("Task Data: $taskData");
|
||||
controller.basicValidator.getController('assigned_date')?.text =
|
||||
taskData['assignedOn'] ?? '';
|
||||
controller.basicValidator.getController('assigned_by')?.text =
|
||||
taskData['assignedBy'] ?? '';
|
||||
controller.basicValidator.getController('work_area')?.text =
|
||||
taskData['location'] ?? '';
|
||||
controller.basicValidator.getController('activity')?.text =
|
||||
taskData['activity'] ?? '';
|
||||
controller.basicValidator.getController('team_size')?.text =
|
||||
taskData['teamSize'].toString();
|
||||
controller.basicValidator.getController('assigned')?.text =
|
||||
taskData['assigned'] ?? '';
|
||||
controller.basicValidator.getController('task_id')?.text =
|
||||
taskData['taskId'] ?? '';
|
||||
|
||||
return Layout(
|
||||
child: GetBuilder<ReportTaskController>(
|
||||
init: controller,
|
||||
tag: 'report_task_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.titleMedium("Report Task",
|
||||
fontSize: 18, fontWeight: 600),
|
||||
MyBreadcrumb(
|
||||
children: [
|
||||
MyBreadcrumbItem(name: 'Daily Progress Report'),
|
||||
MyBreadcrumbItem(name: 'Report Task'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(
|
||||
children: [
|
||||
MyFlexItem(sizes: "lg-8 md-12", child: detail()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget detail() {
|
||||
return Form(
|
||||
key: controller.basicValidator.formKey,
|
||||
child: MyCard.bordered(
|
||||
borderRadiusAll: 4,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 24,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(LucideIcons.server, size: 16),
|
||||
MySpacing.width(12),
|
||||
MyText.titleMedium("General", fontWeight: 600),
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
|
||||
// Static fields
|
||||
buildRow(
|
||||
"Assigned Date",
|
||||
controller.basicValidator
|
||||
.getController('assigned_date')
|
||||
?.text
|
||||
.trim()),
|
||||
buildRow(
|
||||
"Assigned By",
|
||||
controller.basicValidator
|
||||
.getController('assigned_by')
|
||||
?.text
|
||||
.trim()),
|
||||
buildRow(
|
||||
"Work Area",
|
||||
controller.basicValidator
|
||||
.getController('work_area')
|
||||
?.text
|
||||
.trim()),
|
||||
buildRow(
|
||||
"Activity",
|
||||
controller.basicValidator
|
||||
.getController('activity')
|
||||
?.text
|
||||
.trim()),
|
||||
buildRow(
|
||||
"Team Size",
|
||||
controller.basicValidator
|
||||
.getController('team_size')
|
||||
?.text
|
||||
.trim()),
|
||||
buildRow(
|
||||
"Assigned",
|
||||
controller.basicValidator
|
||||
.getController('assigned')
|
||||
?.text
|
||||
.trim()),
|
||||
|
||||
// Input fields
|
||||
MyText.labelMedium("Completed Work"),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
validator:
|
||||
controller.basicValidator.getValidation('completed_work'),
|
||||
controller:
|
||||
controller.basicValidator.getController('completed_work'),
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
hintText: "eg: 10",
|
||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||
border: outlineInputBorder,
|
||||
enabledBorder: outlineInputBorder,
|
||||
focusedBorder: focusedInputBorder,
|
||||
contentPadding: MySpacing.all(16),
|
||||
isCollapsed: true,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
MyText.labelMedium("Comment"),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
validator: controller.basicValidator.getValidation('comment'),
|
||||
controller: controller.basicValidator.getController('comment'),
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
hintText: "eg: Work done successfully",
|
||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||
border: outlineInputBorder,
|
||||
enabledBorder: outlineInputBorder,
|
||||
focusedBorder: focusedInputBorder,
|
||||
contentPadding: MySpacing.all(16),
|
||||
isCollapsed: true,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
|
||||
// Buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
MyButton.text(
|
||||
onPressed: () => Get.back(),
|
||||
padding: MySpacing.xy(20, 16),
|
||||
splashColor: contentTheme.secondary.withValues(alpha: 0.1),
|
||||
child: MyText.bodySmall('Cancel'),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
MyButton(
|
||||
onPressed: controller.reportStatus.value == ApiStatus.loading
|
||||
? null
|
||||
: () async {
|
||||
if (controller.basicValidator.validateForm()) {
|
||||
await controller.reportTask(
|
||||
projectId: controller.basicValidator
|
||||
.getController('task_id')
|
||||
?.text ??
|
||||
'',
|
||||
comment: controller.basicValidator
|
||||
.getController('comment')
|
||||
?.text ??
|
||||
'',
|
||||
completedTask: int.tryParse(controller
|
||||
.basicValidator
|
||||
.getController('completed_work')
|
||||
?.text ??
|
||||
'') ??
|
||||
0,
|
||||
checklist: [],
|
||||
reportedDate: DateTime.now(),
|
||||
);
|
||||
}
|
||||
},
|
||||
elevation: 0,
|
||||
padding: MySpacing.xy(20, 16),
|
||||
backgroundColor: contentTheme.primary,
|
||||
borderRadiusAll: AppStyle.buttonRadius.medium,
|
||||
child: Obx(() {
|
||||
if (controller.reportStatus.value == ApiStatus.loading) {
|
||||
return SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
contentTheme.onPrimary),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return MyText.bodySmall(
|
||||
'Save',
|
||||
color: contentTheme.onPrimary,
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildRow(String label, String? value) {
|
||||
print("Label: $label, Value: $value");
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium("$label:"),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(value?.isNotEmpty == true ? value! : "-"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -69,6 +69,7 @@ dependencies:
|
||||
path: ^1.9.0
|
||||
percent_indicator: ^4.2.2
|
||||
flutter_contacts: ^1.1.9+2
|
||||
photo_view: ^0.15.0
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
@ -99,8 +100,6 @@ flutter:
|
||||
- assets/country/
|
||||
- assets/data/
|
||||
- assets/dummy/
|
||||
- assets/dummy/ecommerce/
|
||||
- assets/dummy/single_product/
|
||||
- assets/lang/
|
||||
- assets/logo/
|
||||
- assets/logo/loading_logo.png
|
||||
|
||||