Feature_Global_Project_Selection #47

Merged
vaibhav.surve merged 22 commits from Feature_Global_Project_Selection into main 2025-06-17 10:09:58 +00:00
97 changed files with 2930 additions and 202825 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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
}
]
}
]

View File

@ -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
}
]

View File

@ -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"
}
]

View File

@ -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"
}
]

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
]

View File

@ -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
}
]

View File

@ -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"
}
]

View File

@ -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"
}
]

View File

@ -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"
}
]

View File

@ -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"
}
]

View File

@ -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"
}
]

View File

@ -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"
}
]

View File

@ -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
}
]

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

View File

@ -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)
];
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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 {

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

View File

@ -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();
}
}

View File

@ -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(

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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
}
}

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

View File

@ -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 {

View File

@ -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(

View File

@ -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";

View File

@ -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;
}
}

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

View File

@ -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.");
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

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

View File

@ -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.")),
),
),
);
}
}

View File

@ -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();
}
}

View File

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

View File

@ -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]!;

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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

View File

@ -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),
],
],
),
),

View File

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

View File

@ -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

View File

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

View File

@ -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() ??
[],
);
}
}

View File

@ -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()),

View File

@ -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,

View File

@ -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(),
),
]),
),
],
);
},
);
},
),
),
),
);
}

View File

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

View File

@ -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()),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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()),
),
],
));
}
}

View File

@ -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()),
),
],
),
);
}
}

View File

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

View File

@ -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,
),
],
),
],
),
),
),
);

View File

@ -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;
}
}
}

View File

@ -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()),
)
],
),
);
}
}

View File

@ -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()),
)
],
),
);
}
}

View File

@ -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()),
),
],
),
);
}
}

View File

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

View File

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

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

View File

@ -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! : "-"),
),
],
),
);
}
}

View File

@ -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(

View File

@ -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(),
),
],
);
},
),
),
),
);
}

View File

@ -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! : "-"),
),
],
),
);
}
}

View File

@ -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