Merge pull request 'Feature_Global_Project_Selection' (#47) from Feature_Global_Project_Selection into main

Reviewed-on: #47
This commit is contained in:
vaibhav.surve 2025-06-17 10:09:58 +00:00
commit 6f2e257f0d
97 changed files with 2930 additions and 202825 deletions

View File

@ -21,7 +21,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // 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. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion 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/attendance_log_model.dart';
import 'package:marco/model/regularization_log_model.dart'; import 'package:marco/model/regularization_log_model.dart';
import 'package:marco/model/attendance_log_view_model.dart'; import 'package:marco/model/attendance_log_view_model.dart';
import 'package:marco/controller/project_controller.dart';
final Logger log = Logger(); final Logger log = Logger();
@ -28,7 +29,6 @@ class AttendanceController extends GetxController {
List<AttendanceLogViewModel> attendenceLogsView = []; List<AttendanceLogViewModel> attendenceLogsView = [];
// Selected values // Selected values
String? selectedProjectId;
String selectedTab = 'Employee List'; String selectedTab = 'Employee List';
// Date range for attendance filtering // Date range for attendance filtering
@ -93,12 +93,10 @@ class AttendanceController extends GetxController {
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
projects = response.map((json) => ProjectModel.fromJson(json)).toList(); projects = response.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length}"); log.i("Projects fetched: ${projects.length}");
await fetchProjectData(selectedProjectId);
} else { } else {
log.w("No projects found or API call failed."); log.e("Failed to fetch projects or no projects available.");
projects = [];
} }
isLoadingProjects.value = false; isLoadingProjects.value = false;
@ -107,6 +105,13 @@ class AttendanceController extends GetxController {
update(['attendance_dashboard_controller']); 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. /// Fetches employees, attendance logs and regularization logs for a project.
Future<void> fetchProjectData(String? projectId) async { Future<void> fetchProjectData(String? projectId) async {
if (projectId == null) return; if (projectId == null) return;
@ -176,7 +181,8 @@ class AttendanceController extends GetxController {
return false; return false;
} }
final compressedBytes = await compressImageToUnder100KB(File(image.path)); final compressedBytes =
await compressImageToUnder100KB(File(image.path));
if (compressedBytes == null) { if (compressedBytes == null) {
log.e("Image compression failed."); log.e("Image compression failed.");
uploadingStates[employeeId]?.value = false; uploadingStates[employeeId]?.value = false;
@ -239,9 +245,9 @@ class AttendanceController extends GetxController {
lastDate: todayDateOnly.subtract(const Duration(days: 1)), lastDate: todayDateOnly.subtract(const Duration(days: 1)),
initialDateRange: DateTimeRange( initialDateRange: DateTimeRange(
start: startDateAttendance ?? today.subtract(const Duration(days: 7)), 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) { builder: (BuildContext context, Widget? child) {
return Center( return Center(
child: SizedBox( child: SizedBox(
@ -273,7 +279,7 @@ class AttendanceController extends GetxController {
log.i("Date range selected: $startDateAttendance to $endDateAttendance"); log.i("Date range selected: $startDateAttendance to $endDateAttendance");
await controller.fetchAttendanceLogs( await controller.fetchAttendanceLogs(
controller.selectedProjectId, Get.find<ProjectController>().selectedProject?.id,
dateFrom: picked.start, dateFrom: picked.start,
dateTo: picked.end, dateTo: picked.end,
); );
@ -332,7 +338,8 @@ class AttendanceController extends GetxController {
return dateB.compareTo(dateA); 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."); log.i("Logs grouped and sorted by check-in date.");
return sortedMap; return sortedMap;
@ -352,8 +359,9 @@ class AttendanceController extends GetxController {
final response = await ApiService.getRegularizationLogs(projectId); final response = await ApiService.getRegularizationLogs(projectId);
if (response != null) { if (response != null) {
regularizationLogs = regularizationLogs = response
response.map((json) => RegularizationLogModel.fromJson(json)).toList(); .map((json) => RegularizationLogModel.fromJson(json))
.toList();
log.i("Regularization logs fetched: ${regularizationLogs.length}"); log.i("Regularization logs fetched: ${regularizationLogs.length}");
update(); update();
} else { } else {
@ -374,8 +382,9 @@ class AttendanceController extends GetxController {
final response = await ApiService.getAttendanceLogView(id); final response = await ApiService.getAttendanceLogView(id);
if (response != null) { if (response != null) {
attendenceLogsView = attendenceLogsView = response
response.map((json) => AttendanceLogViewModel.fromJson(json)).toList(); .map((json) => AttendanceLogViewModel.fromJson(json))
.toList();
attendenceLogsView.sort((a, b) { attendenceLogsView.sort((a, b) {
if (a.activityTime == null || b.activityTime == null) return 0; 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(); projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length} projects loaded."); log.i("Projects fetched: ${projects.length} projects loaded.");
update(); update();
await fetchTaskData(selectedProjectId);
} }
Future<void> fetchTaskData(String? projectId) async { 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/project_model.dart';
import 'package:marco/model/employee_model.dart'; import 'package:marco/model/employee_model.dart';
import 'package:marco/model/employees/employee_details_model.dart'; import 'package:marco/model/employees/employee_details_model.dart';
import 'package:marco/controller/project_controller.dart';
final Logger log = Logger(); final Logger log = Logger();
@ -12,8 +13,9 @@ class EmployeesScreenController extends GetxController {
List<AttendanceModel> attendances = []; List<AttendanceModel> attendances = [];
List<ProjectModel> projects = []; List<ProjectModel> projects = [];
String? selectedProjectId; String? selectedProjectId;
List<EmployeeModel> employees = [];
List<EmployeeDetailsModel> employeeDetails = []; List<EmployeeDetailsModel> employeeDetails = [];
RxBool isAllEmployeeSelected = false.obs;
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
@ -24,7 +26,17 @@ class EmployeesScreenController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
fetchAllProjects(); 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 { Future<void> fetchAllProjects() async {
@ -41,18 +53,27 @@ class EmployeesScreenController extends GetxController {
update(); update();
} }
void clearEmployees() {
employees.clear(); // Correct way to clear RxList
log.i("Employees cleared");
update(['employee_screen_controller']);
}
Future<void> fetchAllEmployees() async { Future<void> fetchAllEmployees() async {
isLoading.value = true; isLoading.value = true;
await _handleApiCall( await _handleApiCall(
ApiService.getAllEmployees, ApiService.getAllEmployees,
onSuccess: (data) { 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."); 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; isLoading.value = false;
update(); update(['employee_screen_controller']);
} }
Future<void> fetchEmployeesByProject(String? projectId) async { Future<void> fetchEmployeesByProject(String? projectId) async {
@ -65,22 +86,21 @@ class EmployeesScreenController extends GetxController {
await _handleApiCall( await _handleApiCall(
() => ApiService.getAllEmployeesByProject(projectId), () => ApiService.getAllEmployeesByProject(projectId),
onSuccess: (data) { onSuccess: (data) {
employees = data.map((json) => EmployeeModel.fromJson(json)).toList(); employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
for (var emp in employees) { for (var emp in employees) {
uploadingStates[emp.id] = false.obs; uploadingStates[emp.id] = false.obs;
} }
log.i("Employees fetched: ${employees.length} for project $projectId"); log.i("Employees fetched: ${employees.length} for project $projectId");
update();
}, },
onEmpty: () { onEmpty: () {
employees.clear();
log.w("No employees found for project $projectId."); log.w("No employees found for project $projectId.");
employees = [];
update();
}, },
onError: (e) => onError: (e) =>
log.e("Error fetching employees for project $projectId: $e"), log.e("Error fetching employees for project $projectId: $e"),
); );
isLoading.value = false; isLoading.value = false;
update(['employee_screen_controller']);
} }
Future<void> _handleApiCall( 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:flutter/material.dart';
import 'package:get/get.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 { class LayoutController extends GetxController {
// Theme Customization
ThemeCustomizer themeCustomizer = ThemeCustomizer(); ThemeCustomizer themeCustomizer = ThemeCustomizer();
// Global Keys
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey(); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
final GlobalKey<State<StatefulWidget>> scrollKey = GlobalKey(); final GlobalKey<State<StatefulWidget>> scrollKey = GlobalKey();
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; bool isLastIndex = false;
@override
void onInit() {
super.onInit();
fetchProjects();
}
@override @override
void onReady() { void onReady() {
super.onReady(); super.onReady();
ThemeCustomizer.addListener(onChangeTheme); ThemeCustomizer.addListener(onChangeTheme);
} }
@override
void dispose() {
ThemeCustomizer.removeListener(onChangeTheme);
scrollController.dispose();
super.dispose();
}
// 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) { void onChangeTheme(ThemeCustomizer oldVal, ThemeCustomizer newVal) {
themeCustomizer = newVal; themeCustomizer = newVal;
update(); update();
@ -29,18 +93,12 @@ class LayoutController extends GetxController {
} }
} }
enableNotificationShade() { // Notification Shade (placeholders)
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]); void enableNotificationShade() {
// Add implementation if needed
} }
disableNotificationShade() { void disableNotificationShade() {
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]); // Add implementation if needed
}
@override
void dispose() {
super.dispose();
ThemeCustomizer.removeListener(onChangeTheme);
scrollController.dispose();
} }
} }

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 { class DailyTaskPlaningController extends GetxController {
List<ProjectModel> projects = []; List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeModel> employees = []; List<EmployeeModel> employees = [];
List<TaskPlanningDetailsModel> dailyTasks = []; List<TaskPlanningDetailsModel> dailyTasks = [];
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
@ -121,11 +120,8 @@ class DailyTaskPlaningController extends GetxController {
} }
projects = response!.map((json) => ProjectModel.fromJson(json)).toList(); projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length} projects loaded."); log.i("Projects fetched: ${projects.length} projects loaded.");
update(); update();
await fetchTaskData(selectedProjectId);
} catch (e, stack) { } catch (e, stack) {
log.e("Error fetching projects", error: e, stackTrace: stack); log.e("Error fetching projects", error: e, stackTrace: stack);
} finally { } 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:marco/controller/task_planing/daily_task_planing_controller.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'dart:io'; import 'dart:io';
import 'dart:convert';
import 'package:marco/helpers/widgets/my_image_compressor.dart';
final Logger logger = Logger(); final Logger logger = Logger();
@ -90,12 +92,13 @@ class ReportTaskController extends MyController {
super.onClose(); super.onClose();
} }
Future<void> reportTask({ Future<bool> reportTask({
required String projectId, required String projectId,
required String comment, required String comment,
required int completedTask, required int completedTask,
required List<Map<String, dynamic>> checklist, required List<Map<String, dynamic>> checklist,
required DateTime reportedDate, required DateTime reportedDate,
List<File>? images,
}) async { }) async {
logger.i("Starting task report..."); logger.i("Starting task report...");
@ -107,7 +110,7 @@ class ReportTaskController extends MyController {
message: "Completed work is required.", message: "Completed work is required.",
type: SnackbarType.error, type: SnackbarType.error,
); );
return; return false;
} }
final completedWorkInt = int.tryParse(completedWork); final completedWorkInt = int.tryParse(completedWork);
@ -117,7 +120,7 @@ class ReportTaskController extends MyController {
message: "Completed work must be a positive integer.", message: "Completed work must be a positive integer.",
type: SnackbarType.error, type: SnackbarType.error,
); );
return; return false;
} }
final commentField = commentController.text.trim(); final commentField = commentController.text.trim();
@ -127,48 +130,100 @@ class ReportTaskController extends MyController {
message: "Comment is required.", message: "Comment is required.",
type: SnackbarType.error, type: SnackbarType.error,
); );
return; return false;
} }
try { try {
reportStatus.value = ApiStatus.loading;
isLoading.value = true; 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( final success = await ApiService.reportTask(
id: projectId, id: projectId,
comment: commentField, comment: commentField,
completedTask: completedTask, completedTask: completedWorkInt,
checkList: checklist, checkList: checklist,
images: imageData,
); );
if (success) { if (success) {
reportStatus.value = ApiStatus.success;
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: "Task reported successfully!", message: "Task reported successfully!",
type: SnackbarType.success, type: SnackbarType.success,
); );
await taskController.fetchTaskData(projectId); await taskController.fetchTaskData(projectId);
return true;
} else { } else {
reportStatus.value = ApiStatus.failure;
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Failed to report task.", message: "Failed to report task.",
type: SnackbarType.error, type: SnackbarType.error,
); );
return false;
} }
} catch (e) { } catch (e) {
logger.e("Error reporting task: $e"); logger.e("Error reporting task: $e");
reportStatus.value = ApiStatus.failure;
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "An error occurred while reporting the task.", message: "An error occurred while reporting the task.",
type: SnackbarType.error, type: SnackbarType.error,
); );
return false;
} finally { } finally {
isLoading.value = false; 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({ Future<void> commentTask({
required String projectId, required String projectId,
required String comment, required String comment,
List<File>? images,
}) async { }) async {
logger.i("Starting task comment..."); logger.i("Starting task comment...");
@ -184,11 +239,38 @@ class ReportTaskController extends MyController {
try { try {
isLoading.value = true; 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( final success = await ApiService.commentTask(
id: projectId, id: projectId,
comment: commentField, comment: commentField,
); images: imageData,
).timeout(const Duration(seconds: 30), onTimeout: () {
logger.e("Request timed out.");
throw Exception("Request timed out.");
});
if (success) { if (success) {
showAppSnackbar( showAppSnackbar(
@ -196,7 +278,6 @@ class ReportTaskController extends MyController {
message: "Task commented successfully!", message: "Task commented successfully!",
type: SnackbarType.success, type: SnackbarType.success,
); );
await taskController.fetchTaskData(projectId); await taskController.fetchTaskData(projectId);
} else { } else {
showAppSnackbar( showAppSnackbar(

View File

@ -4,6 +4,7 @@ class ApiEndpoints {
// Attendance Screen API Endpoints // Attendance Screen API Endpoints
static const String getProjects = "/project/list"; static const String getProjects = "/project/list";
static const String getGlobalProjects = "/project/list/basic";
static const String getEmployeesByProject = "/attendance/project/team"; static const String getEmployeesByProject = "/attendance/project/team";
static const String getAttendanceLogs = "/attendance/project/log"; static const String getAttendanceLogs = "/attendance/project/log";
static const String getAttendanceLogView = "/attendance/log/attendance"; static const String getAttendanceLogView = "/attendance/log/attendance";

View File

@ -1,12 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'package:get/get.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:logger/logger.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/auth_service.dart';
import 'package:marco/helpers/services/api_endpoints.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(); final Logger logger = Logger();
@ -14,13 +14,11 @@ class ApiService {
static const Duration timeout = Duration(seconds: 10); static const Duration timeout = Duration(seconds: 10);
static const bool enableLogs = true; static const bool enableLogs = true;
// ===== Helpers ===== // === Helpers ===
static Future<String?> _getToken() async { static Future<String?> _getToken() async {
final token = await LocalStorage.getJwtToken(); final token = await LocalStorage.getJwtToken();
if (token == null && enableLogs) { if (token == null && enableLogs) logger.w("No JWT token found.");
logger.w("No JWT token found. Please log in.");
}
return token; return token;
} }
@ -47,13 +45,12 @@ class ApiService {
return null; return null;
} }
static dynamic _parseResponseForAllData(http.Response response, static dynamic _parseResponseForAllData(http.Response response, {String label = ''}) {
{String label = ''}) {
_log("$label Response: ${response.body}"); _log("$label Response: ${response.body}");
try { try {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) { 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'}"); _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}");
} catch (e) { } catch (e) {
@ -62,28 +59,23 @@ class ApiService {
return null; return null;
} }
static Future<http.Response?> _getRequest(String endpoint, static Future<http.Response?> _getRequest(
{Map<String, String>? queryParams, bool hasRetried = false}) async { String endpoint, {
Map<String, String>? queryParams,
bool hasRetried = false,
}) async {
String? token = await _getToken(); String? token = await _getToken();
if (token == null) return null; if (token == null) return null;
Uri uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint").replace(queryParameters: queryParams);
.replace(queryParameters: queryParams);
_log("GET $uri"); _log("GET $uri");
try { try {
http.Response response = final response = await http.get(uri, headers: _headers(token)).timeout(timeout);
await http.get(uri, headers: _headers(token)).timeout(timeout);
if (response.statusCode == 401 && !hasRetried) { if (response.statusCode == 401 && !hasRetried) {
_log("Unauthorized. Attempting token refresh..."); _log("Unauthorized. Attempting token refresh...");
bool refreshed = await AuthService.refreshToken(); if (await AuthService.refreshToken()) {
if (refreshed) { return await _getRequest(endpoint, queryParams: queryParams, hasRetried: true);
token = await _getToken();
if (token != null) {
return await _getRequest(endpoint,
queryParams: queryParams, hasRetried: true);
}
} }
_log("Token refresh failed."); _log("Token refresh failed.");
} }
@ -95,78 +87,68 @@ class ApiService {
} }
static Future<http.Response?> _postRequest( 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(); String? token = await _getToken();
if (token == null) return null; if (token == null) return null;
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
_log("POST $uri\nHeaders: ${_headers(token)}\nBody: $body");
_log("POST $uri");
_log("Headers: ${_headers(token)}");
_log("Body: $body");
try { try {
final response = await http final response = await http
.post(uri, headers: _headers(token), body: jsonEncode(body)) .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; return response;
} catch (e) { } catch (e) {
_log("HTTP POST Exception: $e"); _log("HTTP POST Exception: $e");
return null; return null;
} }
} }
// ===== Attendence Screen API Calls =====
static Future<List<dynamic>?> getProjects() async { // === Attendance APIs ===
final response = await _getRequest(ApiEndpoints.getProjects);
return response != null
? _parseResponse(response, label: 'Projects')
: null;
}
static Future<List<dynamic>?> getEmployeesByProject(String projectId) async { static Future<List<dynamic>?> getProjects() async =>
final response = await _getRequest(ApiEndpoints.getEmployeesByProject, _getRequest(ApiEndpoints.getProjects).then((res) => res != null ? _parseResponse(res, label: 'Projects') : null);
queryParams: {"projectId": projectId});
return response != null
? _parseResponse(response, label: 'Employees')
: null;
}
static Future<List<dynamic>?> getAttendanceLogs(String projectId, static Future<List<dynamic>?> getGlobalProjects() async =>
{DateTime? dateFrom, DateTime? dateTo}) 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 = { final query = {
"projectId": projectId, "projectId": projectId,
if (dateFrom != null) if (dateFrom != null) "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
}; };
return _getRequest(ApiEndpoints.getAttendanceLogs, queryParams: query)
final response = .then((res) => res != null ? _parseResponse(res, label: 'Attendance Logs') : null);
await _getRequest(ApiEndpoints.getAttendanceLogs, queryParams: query);
return response != null
? _parseResponse(response, label: 'Attendance Logs')
: null;
} }
static Future<List<dynamic>?> getAttendanceLogView(String id) async { static Future<List<dynamic>?> getAttendanceLogView(String id) async =>
final response = _getRequest("${ApiEndpoints.getAttendanceLogView}/$id")
await _getRequest("${ApiEndpoints.getAttendanceLogView}/$id"); .then((res) => res != null ? _parseResponse(res, label: 'Log Details') : null);
return response != null
? _parseResponse(response, label: 'Log Details')
: null;
}
static Future<List<dynamic>?> getRegularizationLogs(String projectId) async { static Future<List<dynamic>?> getRegularizationLogs(String projectId) async =>
final response = await _getRequest(ApiEndpoints.getRegularizationLogs, _getRequest(ApiEndpoints.getRegularizationLogs, queryParams: {"projectId": projectId})
queryParams: {"projectId": projectId}); .then((res) => res != null ? _parseResponse(res, label: 'Regularization Logs') : null);
return response != null
? _parseResponse(response, label: 'Regularization Logs')
: null;
}
// ===== Upload Attendance Image =====
static Future<bool> uploadAttendanceImage( static Future<bool> uploadAttendanceImage(
String id, String id,
@ -179,7 +161,7 @@ class ApiService {
String comment = "", String comment = "",
required int action, required int action,
bool imageCapture = true, bool imageCapture = true,
String? markTime, // <-- Optional markTime parameter String? markTime,
}) async { }) async {
final now = DateTime.now(); final now = DateTime.now();
final body = { final body = {
@ -197,16 +179,14 @@ class ApiService {
if (imageCapture && imageFile != null) { if (imageCapture && imageFile != null) {
try { try {
final bytes = await imageFile.readAsBytes(); final bytes = await imageFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileSize = await imageFile.length(); final fileSize = await imageFile.length();
final contentType = "image/${imageFile.path.split('.').last}"; final contentType = "image/${imageFile.path.split('.').last}";
body["image"] = { body["image"] = {
"fileName": imageName, "fileName": imageName,
"contentType": contentType, "contentType": contentType,
"fileSize": fileSize, "fileSize": fileSize,
"description": "Employee attendance photo", "description": "Employee attendance photo",
"base64Data": base64Image, "base64Data": base64Encode(bytes),
}; };
} catch (e) { } catch (e) {
_log("Image encoding error: $e"); _log("Image encoding error: $e");
@ -214,22 +194,16 @@ class ApiService {
} }
} }
final response = final response = await _postRequest(ApiEndpoints.uploadAttendanceImage, body);
await _postRequest(ApiEndpoints.uploadAttendanceImage, body);
if (response == null) return false; if (response == null) return false;
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) { if (response.statusCode == 200 && json['success'] == true) return true;
return true;
} else {
_log("Failed to upload image: ${json['message'] ?? 'Unknown error'}");
}
_log("Failed to upload image: ${json['message'] ?? 'Unknown error'}");
return false; return false;
} }
// ===== Utilities =====
static String generateImageName(String employeeId, int count) { static String generateImageName(String employeeId, int count) {
final now = DateTime.now(); final now = DateTime.now();
final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now); final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now);
@ -237,35 +211,19 @@ class ApiService {
return "${employeeId}_${dateStr}_$imageNumber.jpg"; return "${employeeId}_${dateStr}_$imageNumber.jpg";
} }
// ===== Employee Screen API Calls ===== // === Employee APIs ===
static Future<List<dynamic>?> getAllEmployeesByProject(
String projectId) async {
if (projectId.isEmpty) {
throw ArgumentError('projectId must not be empty');
}
final String endpoint = static Future<List<dynamic>?> getAllEmployeesByProject(String projectId) async {
"${ApiEndpoints.getAllEmployeesByProject}/$projectId"; if (projectId.isEmpty) throw ArgumentError('projectId must not be empty');
final response = await _getRequest(endpoint); final endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId";
return _getRequest(endpoint).then((res) => res != null ? _parseResponse(res, label: 'Employees by Project') : null);
return response != null
? _parseResponse(response, label: 'Employees by Project')
: null;
} }
static Future<List<dynamic>?> getAllEmployees() async { static Future<List<dynamic>?> getAllEmployees() async =>
final response = await _getRequest(ApiEndpoints.getAllEmployees); _getRequest(ApiEndpoints.getAllEmployees).then((res) => res != null ? _parseResponse(res, label: 'All Employees') : null);
return response != null
? _parseResponse(response, label: 'All Employees')
: null;
}
static Future<List<dynamic>?> getRoles() async { static Future<List<dynamic>?> getRoles() async =>
final response = await _getRequest(ApiEndpoints.getRoles); _getRequest(ApiEndpoints.getRoles).then((res) => res != null ? _parseResponse(res, label: 'Roles') : null);
return response != null
? _parseResponse(response, label: 'All Employees')
: null;
}
static Future<bool> createEmployee({ static Future<bool> createEmployee({
required String firstName, required String firstName,
@ -281,61 +239,33 @@ class ApiService {
"gender": gender, "gender": gender,
"jobRoleId": jobRoleId, "jobRoleId": jobRoleId,
}; };
// Make the API request
final response = await _postRequest(ApiEndpoints.createEmployee, body); final response = await _postRequest(ApiEndpoints.createEmployee, body);
if (response == null) return false;
if (response == null) {
_log("Error: No response from server.");
return false;
}
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
return response.statusCode == 200 && json['success'] == true;
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;
}
} }
static Future<Map<String, dynamic>?> getEmployeeDetails( static Future<Map<String, dynamic>?> getEmployeeDetails(String employeeId) async {
String employeeId) async {
final url = "${ApiEndpoints.getEmployeeInfo}/$employeeId"; final url = "${ApiEndpoints.getEmployeeInfo}/$employeeId";
final response = await _getRequest(url); final response = await _getRequest(url);
final data = response != null final data = response != null ? _parseResponse(response, label: 'Employee Details') : null;
? _parseResponse(response, label: 'Employee Details') return data is Map<String, dynamic> ? data : null;
: null;
if (data is Map<String, dynamic>) {
return data;
}
return null;
} }
// ===== Daily Tasks API Calls ===== // === Daily Task APIs ===
static Future<List<dynamic>?> getDailyTasks(String projectId,
{DateTime? dateFrom, DateTime? dateTo}) async { static Future<List<dynamic>?> getDailyTasks(
String projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
final query = { final query = {
"projectId": projectId, "projectId": projectId,
if (dateFrom != null) if (dateFrom != null) "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
}; };
return _getRequest(ApiEndpoints.getDailyTask, queryParams: query)
final response = .then((res) => res != null ? _parseResponse(res, label: 'Daily Tasks') : null);
await _getRequest(ApiEndpoints.getDailyTask, queryParams: query);
return response != null
? _parseResponse(response, label: 'Daily Tasks')
: null;
} }
static Future<bool> reportTask({ static Future<bool> reportTask({
@ -343,6 +273,7 @@ class ApiService {
required int completedTask, required int completedTask,
required String comment, required String comment,
required List<Map<String, dynamic>> checkList, required List<Map<String, dynamic>> checkList,
List<Map<String, dynamic>>? images,
}) async { }) async {
final body = { final body = {
"id": id, "id": id,
@ -350,63 +281,43 @@ class ApiService {
"comment": comment, "comment": comment,
"reportedDate": DateTime.now().toUtc().toIso8601String(), "reportedDate": DateTime.now().toUtc().toIso8601String(),
"checkList": checkList, "checkList": checkList,
if (images != null && images.isNotEmpty) "images": images,
}; };
final response = await _postRequest(ApiEndpoints.reportTask, body); final response = await _postRequest(ApiEndpoints.reportTask, body);
if (response == null) return false;
if (response == null) {
_log("Error: No response from server.");
return false;
}
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) { if (response.statusCode == 200 && json['success'] == true) {
Get.back(); Get.back();
return true; 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({ static Future<bool> commentTask({
required String id, required String id,
required String comment, required String comment,
List<Map<String, dynamic>>? images,
}) async { }) async {
final body = { final body = {
"taskAllocationId": id, "taskAllocationId": id,
"comment": comment, "comment": comment,
"commentDate": DateTime.now().toUtc().toIso8601String(), "commentDate": DateTime.now().toUtc().toIso8601String(),
if (images != null && images.isNotEmpty) "images": images,
}; };
final response = await _postRequest(ApiEndpoints.commentTask, body); final response = await _postRequest(ApiEndpoints.commentTask, body);
if (response == null) return false;
if (response == null) {
_log("Error: No response from server.");
return false;
}
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
return response.statusCode == 200 && json['success'] == true;
if (response.statusCode == 200 && json['success'] == true) {
return true;
} else {
_log("Failed to comment task: ${json['message'] ?? 'Unknown error'}");
return false;
}
} }
// 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 url = "${ApiEndpoints.dailyTaskDetails}/$projectId";
final response = await _getRequest(url); final response = await _getRequest(url);
return response != null return response != null
? _parseResponseForAllData(response, label: 'Daily Task Details') ? _parseResponseForAllData(response, label: 'Daily Task Details') as Map<String, dynamic>?
as Map<String, dynamic>?
: null; : null;
} }
@ -422,26 +333,16 @@ class ApiService {
"plannedTask": plannedTask, "plannedTask": plannedTask,
"description": description, "description": description,
"taskTeam": taskTeam, "taskTeam": taskTeam,
"assignmentDate": "assignmentDate": (assignmentDate ?? DateTime.now()).toUtc().toIso8601String(),
(assignmentDate ?? DateTime.now()).toUtc().toIso8601String(),
}; };
final response = await _postRequest(ApiEndpoints.assignDailyTask, body); final response = await _postRequest(ApiEndpoints.assignDailyTask, body);
if (response == null) return false;
if (response == null) {
_log("Error: No response from server.");
return false;
}
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) { if (response.statusCode == 200 && json['success'] == true) {
Get.back(); Get.back();
return true; 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 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:http/http.dart' as http;
import 'package:marco/controller/permission_controller.dart';
import 'package:logger/logger.dart'; 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/api_endpoints.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
final Logger logger = Logger(); final Logger logger = Logger();
@ -13,9 +14,11 @@ class AuthService {
static const Map<String, String> _headers = { static const Map<String, String> _headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
static bool isLoggedIn = false; 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 { try {
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/login-mobile"), Uri.parse("$_baseUrl/auth/login-mobile"),
@ -30,9 +33,7 @@ class AuthService {
} else if (response.statusCode == 401) { } else if (response.statusCode == 401) {
return {"password": "Invalid email or password"}; return {"password": "Invalid email or password"};
} else { } else {
return { return {"error": responseData['message'] ?? "Unexpected error occurred"};
"error": responseData['message'] ?? "Unexpected error occurred"
};
} }
} catch (e) { } catch (e) {
logger.e("Login error: $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 { static Future<bool> refreshToken() async {
final accessToken = await LocalStorage.getJwtToken(); final accessToken = await LocalStorage.getJwtToken();
final refreshToken = await LocalStorage.getRefreshToken(); final refreshToken = await LocalStorage.getRefreshToken();
if (accessToken == null || if (accessToken == null || refreshToken == null || accessToken.isEmpty || refreshToken.isEmpty) {
refreshToken == null || logger.w("Missing access/refresh token.");
accessToken.isEmpty ||
refreshToken.isEmpty) {
logger.w("Missing token or refresh token for refresh.");
return false; return false;
} }
@ -58,82 +56,50 @@ class AuthService {
"refreshToken": refreshToken, "refreshToken": refreshToken,
}; };
logger.i("Sending refresh token request with body: $requestBody");
try { try {
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/refresh-token"), 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, headers: _headers,
body: jsonEncode(requestBody), body: jsonEncode(requestBody),
); );
logger.i( final data = jsonDecode(response.body);
"Forgot password API response (${response.statusCode}): ${response.body}"); if (response.statusCode == 200 && data['success'] == true) {
await LocalStorage.setJwtToken(data['data']['token']);
final responseData = jsonDecode(response.body); await LocalStorage.setRefreshToken(data['data']['refreshToken']);
await LocalStorage.setLoggedInUser(true);
if (response.statusCode == 200 && responseData['success'] == true) { logger.i("Token refreshed.");
logger.i("Forgot password request successful."); return true;
return null;
} else { } else {
return { logger.w("Refresh token failed: ${data['message']}");
"error": return false;
responseData['message'] ?? "Failed to send password reset link."
};
} }
} catch (e) { } 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."}; return {"error": "Network error. Please check your connection."};
} }
} }
// Request demo API /// Request demo
static Future<Map<String, String>?> requestDemo( static Future<Map<String, String>?> requestDemo(Map<String, dynamic> demoData) async {
Map<String, dynamic> demoData) async {
try { try {
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/market/inquiry"), Uri.parse("$_baseUrl/market/inquiry"),
@ -141,22 +107,16 @@ class AuthService {
body: jsonEncode(demoData), body: jsonEncode(demoData),
); );
final responseData = jsonDecode(response.body); final data = jsonDecode(response.body);
if (response.statusCode == 200 && data['success'] == true) return null;
if (response.statusCode == 200 && responseData['success'] == true) { return {"error": data['message'] ?? "Failed to submit demo request."};
logger.i("Request Demo submitted successfully.");
return null;
} else {
return {
"error": responseData['message'] ?? "Failed to submit demo request."
};
}
} catch (e) { } catch (e) {
logger.e("Exception during request demo: $e"); logger.e("Request demo error: $e");
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
/// Get list of industries
static Future<List<Map<String, dynamic>>?> getIndustries() async { static Future<List<Map<String, dynamic>>?> getIndustries() async {
try { try {
final response = await http.get( final response = await http.get(
@ -164,201 +124,129 @@ class AuthService {
headers: _headers, headers: _headers,
); );
logger.i( final data = jsonDecode(response.body);
"Get Industries API response (${response.statusCode}): ${response.body}"); if (response.statusCode == 200 && data['success'] == true) {
return List<Map<String, dynamic>>.from(data['data']);
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;
} }
return null;
} catch (e) { } catch (e) {
logger.e("Exception during getIndustries: $e"); logger.e("Get industries error: $e");
return null; return null;
} }
} }
/// Generates a new MPIN for the user. /// Generate MPIN
static Future<Map<String, String>?> generateMpin({ static Future<Map<String, String>?> generateMpin({
required String employeeId, required String employeeId,
required String mpin, required String mpin,
}) async { }) async {
final jwtToken = await LocalStorage.getJwtToken(); final token = await LocalStorage.getJwtToken();
final requestBody = {
"employeeId": employeeId,
"mpin": mpin,
};
logger.i("Sending MPIN generation request: $requestBody");
try { try {
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/generate-mpin"), Uri.parse("$_baseUrl/auth/generate-mpin"),
headers: { headers: {
'Content-Type': 'application/json', ..._headers,
if (jwtToken != null && jwtToken.isNotEmpty) if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token',
'Authorization': 'Bearer $jwtToken',
}, },
body: jsonEncode(requestBody), body: jsonEncode({"employeeId": employeeId, "mpin": mpin}),
); );
logger.i( final data = jsonDecode(response.body);
"Generate MPIN API response (${response.statusCode}): ${response.body}"); if (response.statusCode == 200 && data['success'] == true) return null;
return {"error": data['message'] ?? "Failed to generate MPIN."};
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."};
}
} catch (e) { } catch (e) {
logger.e("Exception during generate MPIN: $e"); logger.e("Generate MPIN error: $e");
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
/// Verify MPIN
static Future<Map<String, String>?> verifyMpin({ static Future<Map<String, String>?> verifyMpin({
required String mpin, required String mpin,
required String mpinToken, required String mpinToken,
}) async { }) async {
// Get employee info from local storage
final employeeInfo = LocalStorage.getEmployeeInfo(); final employeeInfo = LocalStorage.getEmployeeInfo();
if (employeeInfo == null) return {"error": "Employee info not found."};
if (employeeInfo == null) { final token = await LocalStorage.getJwtToken();
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");
try { try {
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/login-mpin"), Uri.parse("$_baseUrl/auth/login-mpin"),
headers: { headers: {
'Content-Type': 'application/json', ..._headers,
if (jwtToken != null && jwtToken.isNotEmpty) if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token',
'Authorization': 'Bearer $jwtToken',
}, },
body: jsonEncode(requestBody), body: jsonEncode({
"employeeId": employeeInfo.id,
"mpin": mpin,
"mpinToken": mpinToken,
}),
); );
logger.i( final data = jsonDecode(response.body);
"Verify MPIN API response (${response.statusCode}): ${response.body}"); if (response.statusCode == 200 && data['success'] == true) return null;
return {"error": data['message'] ?? "MPIN verification failed."};
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."};
}
} catch (e) { } catch (e) {
logger.e("Exception during verify MPIN: $e"); logger.e("Verify MPIN error: $e");
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
// Generate OTP API /// Generate OTP
static Future<Map<String, String>?> generateOtp(String email) async { static Future<Map<String, String>?> generateOtp(String email) async {
final requestBody = {"email": email};
logger.i("Sending generate OTP request: $requestBody");
try { try {
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/send-otp"), Uri.parse("$_baseUrl/auth/send-otp"),
headers: _headers, headers: _headers,
body: jsonEncode(requestBody), body: jsonEncode({"email": email}),
); );
logger.i( final data = jsonDecode(response.body);
"Generate OTP API response (${response.statusCode}): ${response.body}"); if (response.statusCode == 200 && data['success'] == true) return null;
return {"error": data['message'] ?? "Failed to generate OTP."};
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."};
}
} catch (e) { } catch (e) {
logger.e("Exception during generate OTP: $e"); logger.e("Generate OTP error: $e");
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
// Verify OTP API /// Verify OTP and login
static Future<Map<String, String>?> verifyOtp({ static Future<Map<String, String>?> verifyOtp({
required String email, required String email,
required String otp, required String otp,
}) async { }) async {
final requestBody = {
"email": email,
"otp": otp,
};
logger.i("Sending verify OTP request: $requestBody");
try { try {
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/login-otp"), Uri.parse("$_baseUrl/auth/login-otp"),
headers: _headers, headers: _headers,
body: jsonEncode(requestBody), body: jsonEncode({"email": email, "otp": otp}),
); );
logger.i( final data = jsonDecode(response.body);
"Verify OTP API response (${response.statusCode}): ${response.body}"); if (response.statusCode == 200 && data['data'] != null) {
await _handleLoginSuccess(data['data']);
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.");
return null; return null;
} else {
return {"error": responseData['message'] ?? "Failed to verify OTP."};
} }
return {"error": data['message'] ?? "OTP verification failed."};
} catch (e) { } catch (e) {
logger.e("Exception during verify OTP: $e"); logger.e("Verify OTP error: $e");
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
/// Handle login success flow
static Future<void> _handleLoginSuccess(Map<String, dynamic> data) async { static Future<void> _handleLoginSuccess(Map<String, dynamic> data) async {
final jwtToken = data['token']; final jwtToken = data['token'];
final refreshToken = data['refreshToken']; final refreshToken = data['refreshToken'];
final mpinToken = data['mpinToken']; 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.setJwtToken(jwtToken);
await LocalStorage.setLoggedInUser(true); await LocalStorage.setLoggedInUser(true);
if (refreshToken != null) { if (refreshToken != null) await LocalStorage.setRefreshToken(refreshToken);
await LocalStorage.setRefreshToken(refreshToken);
}
if (mpinToken != null && mpinToken.isNotEmpty) { if (mpinToken != null && mpinToken.isNotEmpty) {
await LocalStorage.setMpinToken(mpinToken); await LocalStorage.setMpinToken(mpinToken);
await LocalStorage.setIsMpin(true); await LocalStorage.setIsMpin(true);
@ -367,7 +255,11 @@ class AuthService {
await LocalStorage.removeMpinToken(); await LocalStorage.removeMpinToken();
} }
Get.put(PermissionController()); final permissionController = Get.put(PermissionController());
await permissionController.loadData(jwtToken);
await Get.find<ProjectController>().fetchProjects();
isLoggedIn = true; isLoggedIn = true;
logger.i("Login success initialized.");
} }
} }

View File

@ -1,31 +1,35 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:marco/model/user_permission.dart'; import 'package:marco/model/user_permission.dart';
import 'package:marco/model/employee_info.dart'; import 'package:marco/model/employee_info.dart';
import 'package:marco/model/projects_model.dart'; import 'package:marco/model/projects_model.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/api_endpoints.dart';
final Logger logger = Logger(); final Logger logger = Logger();
class PermissionService { class PermissionService {
static final Map<String, Map<String, dynamic>> _userDataCache = {}; static final Map<String, Map<String, dynamic>> _userDataCache = {};
static const String _baseUrl = ApiEndpoints.baseUrl;
static Future<Map<String, dynamic>> fetchAllUserData(String token, /// Fetches all user-related data (permissions, employee info, projects)
{bool hasRetried = false}) async { static Future<Map<String, dynamic>> fetchAllUserData(
// Return from cache if available String token, {
bool hasRetried = false,
}) async {
// Return cached data if already available
if (_userDataCache.containsKey(token)) { if (_userDataCache.containsKey(token)) {
return _userDataCache[token]!; return _userDataCache[token]!;
} }
final uri = Uri.parse("$_baseUrl/user/profile");
final headers = {'Authorization': 'Bearer $token'};
try { try {
final response = await http.get( final response = await http.get(uri, headers: headers);
Uri.parse('https://stageapi.marcoaiot.com/api/user/profile'),
// Uri.parse('https://api.marcoaiot.com/api/user/profile'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = json.decode(response.body)['data']; final data = json.decode(response.body)['data'];
@ -40,11 +44,12 @@ class PermissionService {
return result; return result;
} }
// Handle 401 by attempting a single retry with refreshed token
if (response.statusCode == 401 && !hasRetried) { if (response.statusCode == 401 && !hasRetried) {
final refreshed = await AuthService.refreshToken(); final refreshed = await AuthService.refreshToken();
if (refreshed) { if (refreshed) {
final newToken = await LocalStorage.getJwtToken(); final newToken = await LocalStorage.getJwtToken();
if (newToken != null) { if (newToken != null && newToken.isNotEmpty) {
return fetchAllUserData(newToken, hasRetried: true); return fetchAllUserData(newToken, hasRetried: true);
} }
} }
@ -53,15 +58,15 @@ class PermissionService {
throw Exception('Unauthorized. Token refresh failed.'); throw Exception('Unauthorized. Token refresh failed.');
} }
final errorMessage = final error = json.decode(response.body)['message'] ?? 'Unknown error';
json.decode(response.body)['message'] ?? 'Unknown error'; throw Exception('Failed to fetch user data: $error');
throw Exception('Failed to load data: $errorMessage');
} catch (e) { } catch (e) {
logger.e('Error fetching user data: $e'); logger.e('Error fetching user data: $e');
rethrow; rethrow;
} }
} }
/// Clears auth data and redirects to login
static Future<void> _handleUnauthorized() async { static Future<void> _handleUnauthorized() async {
await LocalStorage.removeToken('jwt_token'); await LocalStorage.removeToken('jwt_token');
await LocalStorage.removeToken('refresh_token'); await LocalStorage.removeToken('refresh_token');
@ -69,15 +74,20 @@ class PermissionService {
Get.offAllNamed('/auth/login-option'); Get.offAllNamed('/auth/login-option');
} }
static List<UserPermission> _parsePermissions( /// Converts raw permission data into list of `UserPermission`
List<dynamic> featurePermissions) => static List<UserPermission> _parsePermissions(List<dynamic> permissions) {
featurePermissions return permissions
.map((id) => UserPermission.fromJson({'id': id})) .map((id) => UserPermission.fromJson({'id': id}))
.toList(); .toList();
}
static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic> employeeData) => /// Converts raw employee JSON into `EmployeeInfo`
EmployeeInfo.fromJson(employeeData); static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic> data) {
return EmployeeInfo.fromJson(data);
}
static List<ProjectInfo> _parseProjectsInfo(List<dynamic> projects) => /// Converts raw projects JSON into list of `ProjectInfo`
projects.map((proj) => ProjectInfo.fromJson(proj)).toList(); 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/user_permission.dart';
import 'package:marco/model/employee_info.dart'; import 'package:marco/model/employee_info.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:get/route_manager.dart'; import 'package:marco/controller/project_controller.dart';
import 'package:get/get.dart';
class LocalStorage { class LocalStorage {
static const String _loggedInUserKey = "user"; static const String _loggedInUserKey = "user";
@ -134,20 +136,25 @@ class LocalStorage {
return setToken(_refreshTokenKey, refreshToken); return setToken(_refreshTokenKey, refreshToken);
} }
static Future<void> logout() async { static Future<void> logout() async {
await removeLoggedInUser(); await removeLoggedInUser();
await removeToken(_jwtTokenKey); await removeToken(_jwtTokenKey);
await removeToken(_refreshTokenKey); await removeToken(_refreshTokenKey);
await removeUserPermissions(); await removeUserPermissions();
await removeEmployeeInfo(); await removeEmployeeInfo();
await removeMpinToken(); await removeMpinToken();
await removeIsMpin(); await removeIsMpin();
await preferences.remove("mpin_verified"); await preferences.remove("mpin_verified");
await preferences.remove(_languageKey); await preferences.remove(_languageKey);
await preferences.remove(_themeCustomizerKey); await preferences.remove(_themeCustomizerKey);
Get.offAllNamed('/auth/login-option'); await preferences.remove('selectedProjectId');
if (Get.isRegistered<ProjectController>()) {
Get.find<ProjectController>().clearProjects();
} }
Get.offAllNamed('/auth/login-option');
}
static Future<bool> setMpinToken(String token) { static Future<bool> setMpinToken(String token) {
return preferences.setString(_mpinTokenKey, token); return preferences.setString(_mpinTokenKey, token);
} }
@ -180,4 +187,13 @@ class LocalStorage {
static bool? getBool(String key) { static bool? getBool(String key) {
return preferences.getBool(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/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:marco/helpers/services/app_initializer.dart';
import 'package:get/get.dart'; import 'package:marco/view/my_app.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:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_strategy/url_strategy.dart'; import 'package:marco/helpers/theme/app_notifier.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:flutter/services.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
final Logger logger = Logger(); final Logger logger = Logger();
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
setPathUrlStrategy();
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: const Color.fromARGB(255, 255, 0, 0),
statusBarIconBrightness: Brightness.light,
));
try { try {
await LocalStorage.init(); await initializeApp();
await ThemeCustomizer.init(); runApp(
AppStyle.init(); ChangeNotifierProvider<AppNotifier>(
logger.i("App initialization completed successfully."); create: (_) => AppNotifier(),
child: const MyApp(),
),
);
} catch (e, stacktrace) { } catch (e, stacktrace) {
logger.e('Error during app initialization:', logger.e('App failed to initialize:', error: e, stackTrace: stacktrace);
error: e, stackTrace: stacktrace); runApp(
return; const MaterialApp(
} home: Scaffold(
body: Center(child: Text("Failed to initialize the app.")),
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(),
);
},
);
},
); );
} }
} }

View File

@ -4,6 +4,7 @@ import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:marco/helpers/utils/attendance_actions.dart'; import 'package:marco/helpers/utils/attendance_actions.dart';
import 'package:marco/controller/project_controller.dart';
class AttendanceActionButton extends StatefulWidget { class AttendanceActionButton extends StatefulWidget {
final dynamic employee; final dynamic employee;
@ -19,10 +20,11 @@ class AttendanceActionButton extends StatefulWidget {
State<AttendanceActionButton> createState() => _AttendanceActionButtonState(); State<AttendanceActionButton> createState() => _AttendanceActionButtonState();
} }
Future<String?> _showCommentBottomSheet(BuildContext context, String actionText) async { Future<String?> _showCommentBottomSheet(
BuildContext context, String actionText) async {
final TextEditingController commentController = TextEditingController(); final TextEditingController commentController = TextEditingController();
String? errorText; String? errorText;
Get.find<ProjectController>().selectedProject?.id;
return showModalBottomSheet<String>( return showModalBottomSheet<String>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
@ -80,7 +82,7 @@ Future<String?> _showCommentBottomSheet(BuildContext context, String actionText)
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
final comment = commentController.text.trim(); final comment = commentController.text.trim();
if (comment.isEmpty) { if (comment.isEmpty) {
@ -105,7 +107,6 @@ Future<String?> _showCommentBottomSheet(BuildContext context, String actionText)
); );
} }
String capitalizeFirstLetter(String text) { String capitalizeFirstLetter(String text) {
if (text.isEmpty) return text; if (text.isEmpty) return text;
return text[0].toUpperCase() + text.substring(1); return text[0].toUpperCase() + text.substring(1);
@ -163,7 +164,10 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
void _handleButtonPressed(BuildContext context) async { void _handleButtonPressed(BuildContext context) async {
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = true; 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( showAppSnackbar(
title: "Project Required", title: "Project Required",
message: "Please select a project first", message: "Please select a project first",
@ -231,7 +235,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
success = await widget.attendanceController.captureAndUploadAttendance( success = await widget.attendanceController.captureAndUploadAttendance(
widget.employee.id, widget.employee.id,
widget.employee.employeeId, widget.employee.employeeId,
widget.attendanceController.selectedProjectId!, selectedProjectId,
comment: userComment, comment: userComment,
action: updatedAction, action: updatedAction,
imageCapture: imageCapture, imageCapture: imageCapture,
@ -242,7 +246,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
success = await widget.attendanceController.captureAndUploadAttendance( success = await widget.attendanceController.captureAndUploadAttendance(
widget.employee.id, widget.employee.id,
widget.employee.employeeId, widget.employee.employeeId,
widget.attendanceController.selectedProjectId!, selectedProjectId,
comment: userComment, comment: userComment,
action: updatedAction, action: updatedAction,
imageCapture: imageCapture, imageCapture: imageCapture,
@ -260,14 +264,11 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false; widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
if (success) { if (success) {
widget.attendanceController.fetchEmployeesByProject( widget.attendanceController.fetchEmployeesByProject(selectedProjectId);
widget.attendanceController.selectedProjectId!); widget.attendanceController.fetchAttendanceLogs(selectedProjectId);
widget.attendanceController
.fetchAttendanceLogs(widget.attendanceController.selectedProjectId!);
await widget.attendanceController.fetchRegularizationLogs(
widget.attendanceController.selectedProjectId!);
await widget.attendanceController await widget.attendanceController
.fetchProjectData(widget.attendanceController.selectedProjectId!); .fetchRegularizationLogs(selectedProjectId);
await widget.attendanceController.fetchProjectData(selectedProjectId);
widget.attendanceController.update(); widget.attendanceController.update();
} }
} }

View File

@ -24,14 +24,11 @@ class AttendanceFilterBottomSheet extends StatefulWidget {
class _AttendanceFilterBottomSheetState class _AttendanceFilterBottomSheetState
extends State<AttendanceFilterBottomSheet> { extends State<AttendanceFilterBottomSheet> {
late String? tempSelectedProjectId;
late String tempSelectedTab; late String tempSelectedTab;
bool showProjectList = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
tempSelectedProjectId = widget.controller.selectedProjectId;
tempSelectedTab = widget.selectedTab; tempSelectedTab = widget.selectedTab;
} }
@ -46,55 +43,7 @@ class _AttendanceFilterBottomSheetState
return "Date Range"; 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() { 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 final hasRegularizationPermission = widget.permissionController
.hasPermission(Permissions.regularizeAttendance); .hasPermission(Permissions.regularizeAttendance);
@ -112,24 +61,6 @@ class _AttendanceFilterBottomSheetState
}).toList(); }).toList();
List<Widget> widgets = [ 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(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Align( child: Align(
@ -216,7 +147,6 @@ class _AttendanceFilterBottomSheetState
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Drag handle
Padding( Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8), padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Center( child: Center(
@ -230,7 +160,7 @@ class _AttendanceFilterBottomSheetState
), ),
), ),
), ),
if (showProjectList) ...buildProjectList() else ...buildMainFilters(), ...buildMainFilters(),
const Divider(), const Divider(),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
@ -247,7 +177,6 @@ class _AttendanceFilterBottomSheetState
child: const Text('Apply Filter'), child: const Text('Apply Filter'),
onPressed: () { onPressed: () {
Navigator.pop(context, { Navigator.pop(context, {
'projectId': tempSelectedProjectId,
'selectedTab': tempSelectedTab, 'selectedTab': tempSelectedTab,
}); });
}, },

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/helpers/utils/attendance_actions.dart'; import 'package:marco/helpers/utils/attendance_actions.dart';
import 'package:marco/helpers/widgets/my_snackbar.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 } enum ButtonActions { approve, reject }
class RegularizeActionButton extends StatefulWidget { class RegularizeActionButton extends StatefulWidget {
@ -51,60 +53,57 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
Colors.grey; Colors.grey;
} }
Future<void> _handlePress() async { Future<void> _handlePress() async {
if (widget.attendanceController.selectedProjectId == null) { final projectController = Get.find<ProjectController>();
showAppSnackbar( final selectedProjectId = projectController.selectedProject?.id;
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,
);
if (selectedProjectId == null) {
showAppSnackbar( showAppSnackbar(
title: success ? 'Success' : 'Error', title: 'Warning',
message: success message: 'Please select a project first',
? '${capitalizeFirstLetter(_buttonTexts[widget.action]!)} marked successfully!' type: SnackbarType.warning,
: 'Failed to mark ${capitalizeFirstLetter(_buttonTexts[widget.action]!)}.',
type: success ? SnackbarType.success : SnackbarType.error,
); );
return;
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;
});
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final buttonText = _buttonTexts[widget.action]!; 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_text.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/project_controller.dart';
class AssignTaskBottomSheet extends StatefulWidget { class AssignTaskBottomSheet extends StatefulWidget {
final String workLocation; final String workLocation;
@ -34,6 +35,7 @@ class AssignTaskBottomSheet extends StatefulWidget {
class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> { class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
final DailyTaskPlaningController controller = Get.find(); final DailyTaskPlaningController controller = Get.find();
final ProjectController projectController = Get.find();
final TextEditingController targetController = TextEditingController(); final TextEditingController targetController = TextEditingController();
final TextEditingController descriptionController = TextEditingController(); final TextEditingController descriptionController = TextEditingController();
String? selectedProjectId; String? selectedProjectId;
@ -51,7 +53,8 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
selectedProjectId = controller.selectedProjectId; selectedProjectId = projectController.selectedProjectId?.value;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (selectedProjectId != null) { if (selectedProjectId != null) {
controller.fetchEmployeesByProject(selectedProjectId!); controller.fetchEmployeesByProject(selectedProjectId!);
@ -140,16 +143,13 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
), ),
MySpacing.height(8), MySpacing.height(8),
Container( Container(
constraints: BoxConstraints( constraints: BoxConstraints(maxHeight: 150),
maxHeight: 150,
),
child: _buildEmployeeList(), child: _buildEmployeeList(),
), ),
MySpacing.height(8), MySpacing.height(8),
Obx(() { Obx(() {
if (controller.selectedEmployees.isEmpty) { if (controller.selectedEmployees.isEmpty) return Container();
return Container();
}
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
@ -163,21 +163,23 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
if (!isSelected) return Container(); if (!isSelected) return Container();
return Chip( return Chip(
label: Text(e.name, label: Text(e.name,
style: const TextStyle(color: Colors.white)), style: const TextStyle(color: Colors.white)),
backgroundColor: backgroundColor:
const Color.fromARGB(255, 95, 132, 255), const Color.fromARGB(255, 95, 132, 255),
deleteIcon: deleteIcon:
const Icon(Icons.close, color: Colors.white), const Icon(Icons.close, color: Colors.white),
onDeleted: () { onDeleted: () {
controller.uploadingStates[e.id]?.value = false; controller.uploadingStates[e.id]?.value = false;
controller.updateSelectedEmployees(); controller.updateSelectedEmployees();
}); },
);
}); });
}).toList(), }).toList(),
), ),
); );
}), }),
_buildTextField( _buildTextField(
icon: Icons.track_changes, icon: Icons.track_changes,
label: "Target for Today :", label: "Target for Today :",
@ -187,6 +189,7 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
validatorType: "target", validatorType: "target",
), ),
MySpacing.height(24), MySpacing.height(24),
_buildTextField( _buildTextField(
icon: Icons.description, icon: Icons.description,
label: "Description :", label: "Description :",
@ -196,6 +199,7 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
validatorType: "description", validatorType: "description",
), ),
MySpacing.height(24), MySpacing.height(24),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -225,7 +229,6 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
} }
final selectedRoleId = controller.selectedRoleId.value; final selectedRoleId = controller.selectedRoleId.value;
final filteredEmployees = selectedRoleId == null final filteredEmployees = selectedRoleId == null
? controller.employees ? controller.employees
: 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/avatar.dart';
import 'package:marco/helpers/widgets/my_team_model_sheet.dart'; import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
class CommentTaskBottomSheet extends StatefulWidget { class CommentTaskBottomSheet extends StatefulWidget {
final Map<String, dynamic> taskData; final Map<String, dynamic> taskData;
@ -59,6 +60,7 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
controller.basicValidator.getController('task_id')?.text = controller.basicValidator.getController('task_id')?.text =
data['taskId'] ?? ''; data['taskId'] ?? '';
controller.basicValidator.getController('comment')?.clear(); controller.basicValidator.getController('comment')?.clear();
controller.selectedImages.clear();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) { if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent); _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
@ -177,6 +179,90 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
icon: Icons.done_all_outlined, icon: Icons.done_all_outlined,
), ),
buildTeamMembers(), 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( Row(
children: [ children: [
Icon(Icons.comment_outlined, Icon(Icons.comment_outlined,
@ -206,6 +292,148 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
floatingLabelBehavior: FloatingLabelBehavior.never, 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), MySpacing.height(24),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
@ -233,7 +461,9 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
.getController('comment') .getController('comment')
?.text ?? ?.text ??
'', '',
images: controller.selectedImages,
); );
if (widget.onCommentSuccess != null) { if (widget.onCommentSuccess != null) {
widget.onCommentSuccess!(); widget.onCommentSuccess!();
} }
@ -262,12 +492,13 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
}), }),
], ],
), ),
MySpacing.height(24), MySpacing.height(10),
if ((widget.taskData['taskComments'] as List<dynamic>?) if ((widget.taskData['taskComments'] as List<dynamic>?)
?.isNotEmpty == ?.isNotEmpty ==
true) ...[ true) ...[
Row( Row(
children: [ children: [
MySpacing.width(10),
Icon(Icons.chat_bubble_outline, Icon(Icons.chat_bubble_outline,
size: 18, color: Colors.grey[700]), size: 18, color: Colors.grey[700]),
MySpacing.width(8), MySpacing.width(8),
@ -277,6 +508,7 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
), ),
], ],
), ),
Divider(),
MySpacing.height(12), MySpacing.height(12),
Builder( Builder(
builder: (context) { builder: (context) {
@ -298,6 +530,9 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
return SizedBox( return SizedBox(
height: 300, height: 300,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.symmetric(
vertical:
8), // Added padding around the list
itemCount: comments.length, itemCount: comments.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final comment = comments[index]; final comment = comments[index];
@ -306,11 +541,13 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
comment['commentedBy'] ?? 'Unknown'; comment['commentedBy'] ?? 'Unknown';
final relativeTime = final relativeTime =
timeAgo(comment['date'] ?? ''); timeAgo(comment['date'] ?? '');
// Dummy image URLs (simulate as if coming from backend)
final imageUrls = List<String>.from(
comment['preSignedUrls'] ?? []);
return Container( return Container(
margin: EdgeInsets.symmetric( margin: const EdgeInsets.symmetric(
vertical: 6, horizontal: 8), vertical: 8), // Spacing between items
padding: EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade200, color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -319,55 +556,198 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment.start, CrossAxisAlignment.start,
children: [ children: [
// Avatar for commenter const SizedBox(width: 12),
Avatar(
firstName:
commentedBy.split(' ').first,
lastName: commentedBy
.split(' ')
.length >
1
? commentedBy.split(' ').last
: '',
size: 32,
),
SizedBox(width: 12),
// Comment text and meta
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment.start, CrossAxisAlignment.start,
children: [ 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( Row(
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment MainAxisAlignment
.spaceBetween, .spaceBetween,
children: [ children: [
Text( MyText.bodyMedium(
commentedBy, commentText,
style: TextStyle( fontWeight: 500,
fontWeight: color: Colors.black87,
FontWeight.bold,
color: Colors.black87,
),
), ),
Text(
relativeTime,
style: TextStyle(
fontSize: 12,
color: Colors.black54,
),
)
], ],
), ),
SizedBox(height: 6), const SizedBox(height: 12),
Text( // 🔹 Attachments row: full width below top row
commentText, if (imageUrls.isNotEmpty) ...[
style: TextStyle( Row(
fontWeight: FontWeight.w500, crossAxisAlignment:
color: Colors.black87, 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> { class _DailyProgressReportFilterState extends State<DailyProgressReportFilter> {
late String? tempSelectedProjectId;
bool showProjectList = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
tempSelectedProjectId = widget.controller.selectedProjectId;
} }
String getLabelText() { String getLabelText() {
@ -42,116 +38,6 @@ class _DailyProgressReportFilterState extends State<DailyProgressReportFilter> {
@override @override
Widget build(BuildContext context) { 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( return SafeArea(
child: Padding( child: Padding(
padding: EdgeInsets.only( 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(), const Divider(),
Padding( Padding(
padding: padding:
@ -190,8 +124,11 @@ class _DailyProgressReportFilterState extends State<DailyProgressReportFilter> {
), ),
child: const Text('Apply Filter'), child: const Text('Apply Filter'),
onPressed: () { onPressed: () {
Navigator.pop(context, { WidgetsBinding.instance.addPostFrameCallback((_) {
'projectId': tempSelectedProjectId, Navigator.pop(context, {
'startDate': widget.controller.startDateTask,
'endDate': widget.controller.endDateTask,
});
}); });
}, },
), ),

View File

@ -15,7 +15,7 @@ class DailyTaskPlaningFilter extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String? tempSelectedProjectId = controller.selectedProjectId; String? tempSelectedProjectId = '654563563645';
bool showProjectList = false; bool showProjectList = false;
final accessibleProjects = controller.projects final accessibleProjects = controller.projects

View File

@ -10,8 +10,12 @@ import 'package:marco/helpers/widgets/my_text_style.dart';
class ReportTaskBottomSheet extends StatefulWidget { class ReportTaskBottomSheet extends StatefulWidget {
final Map<String, dynamic> taskData; final Map<String, dynamic> taskData;
final VoidCallback? onReportSuccess; final VoidCallback? onReportSuccess;
const ReportTaskBottomSheet({super.key, required this.taskData,this.onReportSuccess,}); const ReportTaskBottomSheet({
super.key,
required this.taskData,
this.onReportSuccess,
});
@override @override
State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState(); State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState();
@ -201,6 +205,147 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
), ),
), ),
MySpacing.height(24), 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( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -211,60 +356,49 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
child: MyText.bodySmall('Cancel'), child: MyText.bodySmall('Cancel'),
), ),
MySpacing.width(12), MySpacing.width(12),
Obx(() { Obx(() {
return MyButton( final isLoading = controller.reportStatus.value == ApiStatus.loading;
onPressed: controller.reportStatus.value ==
ApiStatus.loading return MyButton(
? null onPressed: isLoading
: () async { ? null
if (controller.basicValidator : () async {
.validateForm()) { if (controller.basicValidator.validateForm()) {
await controller.reportTask( final success = await controller.reportTask(
projectId: controller.basicValidator projectId: controller.basicValidator.getController('task_id')?.text ?? '',
.getController('task_id') comment: controller.basicValidator.getController('comment')?.text ?? '',
?.text ?? completedTask: int.tryParse(
'', controller.basicValidator.getController('completed_work')?.text ?? '') ??
comment: controller.basicValidator 0,
.getController('comment') checklist: [],
?.text ?? reportedDate: DateTime.now(),
'', images: controller.selectedImages,
completedTask: int.tryParse( );
controller.basicValidator if (success && widget.onReportSuccess != null) {
.getController( widget.onReportSuccess!();
'completed_work') }
?.text ?? }
'') ?? },
0, elevation: 0,
checklist: [], padding: MySpacing.xy(20, 16),
reportedDate: DateTime.now(), backgroundColor: Colors.blueAccent,
); borderRadiusAll: AppStyle.buttonRadius.medium,
if (widget.onReportSuccess != null) { child: isLoading
widget.onReportSuccess!(); ? const SizedBox(
} height: 16,
} width: 16,
}, child: CircularProgressIndicator(
elevation: 0, strokeWidth: 2,
padding: MySpacing.xy(20, 16), valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
backgroundColor: Colors.blueAccent, ),
borderRadiusAll: AppStyle.buttonRadius.medium, )
child: controller.reportStatus.value == : MyText.bodySmall(
ApiStatus.loading 'Report',
? SizedBox( color: contentTheme.onPrimary,
height: 16, ),
width: 16, );
child: CircularProgressIndicator( }),
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(
contentTheme.onPrimary),
),
)
: MyText.bodySmall(
'Report',
color: contentTheme.onPrimary,
),
);
}),
], ],
), ),
], ],

View File

@ -9,10 +9,11 @@ class TaskModel {
final AssignedBy assignedBy; final AssignedBy assignedBy;
final List<TeamMember> teamMembers; final List<TeamMember> teamMembers;
final List<Comment> comments; final List<Comment> comments;
final List<String> reportedPreSignedUrls;
TaskModel({ TaskModel({
required this.assignmentDate, required this.assignmentDate,
this.reportedDate, this.reportedDate,
required this.id, required this.id,
required this.workItem, required this.workItem,
required this.workItemId, required this.workItemId,
@ -21,13 +22,14 @@ class TaskModel {
required this.assignedBy, required this.assignedBy,
required this.teamMembers, required this.teamMembers,
required this.comments, required this.comments,
required this.reportedPreSignedUrls,
}); });
factory TaskModel.fromJson(Map<String, dynamic> json) { factory TaskModel.fromJson(Map<String, dynamic> json) {
return TaskModel( return TaskModel(
assignmentDate: DateTime.parse(json['assignmentDate']), assignmentDate: DateTime.parse(json['assignmentDate']),
reportedDate: json['reportedDate'] != null reportedDate: json['reportedDate'] != null
? DateTime.tryParse(json['reportedDate']) ? DateTime.tryParse(json['reportedDate'])
: null, : null,
id: json['id'], id: json['id'],
workItem: workItem:
@ -41,6 +43,10 @@ class TaskModel {
.toList(), .toList(),
comments: comments:
(json['comments'] as List).map((e) => Comment.fromJson(e)).toList(), (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 WorkArea? workArea;
final int? plannedWork; final int? plannedWork;
final int? completedWork; final int? completedWork;
final List<String> preSignedUrls;
WorkItem({ WorkItem({
this.id, this.id,
@ -58,6 +65,7 @@ class WorkItem {
this.workArea, this.workArea,
this.plannedWork, this.plannedWork,
this.completedWork, this.completedWork,
this.preSignedUrls = const [],
}); });
factory WorkItem.fromJson(Map<String, dynamic> json) { factory WorkItem.fromJson(Map<String, dynamic> json) {
@ -70,6 +78,10 @@ class WorkItem {
json['workArea'] != null ? WorkArea.fromJson(json['workArea']) : null, json['workArea'] != null ? WorkArea.fromJson(json['workArea']) : null,
plannedWork: json['plannedWork'], plannedWork: json['plannedWork'],
completedWork: json['completedWork'], completedWork: json['completedWork'],
preSignedUrls: (json['preSignedUrls'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
); );
} }
} }
@ -167,11 +179,13 @@ class Comment {
final String comment; final String comment;
final TeamMember commentedBy; final TeamMember commentedBy;
final DateTime timestamp; final DateTime timestamp;
final List<String> preSignedUrls;
Comment({ Comment({
required this.comment, required this.comment,
required this.commentedBy, required this.commentedBy,
required this.timestamp, required this.timestamp,
required this.preSignedUrls,
}); });
factory Comment.fromJson(Map<String, dynamic> json) { factory Comment.fromJson(Map<String, dynamic> json) {
@ -181,6 +195,10 @@ class Comment {
? TeamMember.fromJson(json['employee']) ? TeamMember.fromJson(json['employee'])
: TeamMember(id: '', firstName: '', lastName: null), : TeamMember(id: '', firstName: '', lastName: null),
timestamp: DateTime.parse(json['commentDate'] ?? ''), 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_404_screen.dart';
import 'package:marco/view/error_pages/error_500_screen.dart'; import 'package:marco/view/error_pages/error_500_screen.dart';
import 'package:marco/view/dashboard/dashboard_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/dashboard/Attendence/attendance_screen.dart';
import 'package:marco/view/taskPlaning/daily_task_planing.dart'; import 'package:marco/view/taskPlaning/daily_task_planing.dart';
import 'package:marco/view/taskPlaning/daily_progress.dart'; import 'package:marco/view/taskPlaning/daily_progress.dart';
@ -36,6 +32,12 @@ getPageRoute() {
name: '/', name: '/',
page: () => DashboardScreen(), page: () => DashboardScreen(),
middlewares: [AuthMiddleware()]), middlewares: [AuthMiddleware()]),
GetPage(
name: '/home',
page: () => DashboardScreen(), // or your actual home screen
middlewares: [AuthMiddleware()],
),
// Dashboard // Dashboard
GetPage( GetPage(
name: '/dashboard/attendance', name: '/dashboard/attendance',
@ -49,16 +51,7 @@ getPageRoute() {
name: '/dashboard/employees', name: '/dashboard/employees',
page: () => EmployeesScreen(), page: () => EmployeesScreen(),
middlewares: [AuthMiddleware()]), middlewares: [AuthMiddleware()]),
// Employees Creation
GetPage(
name: '/employees/addEmployee',
page: () => AddEmployeeScreen(),
middlewares: [AuthMiddleware()]),
// Daily Task Planning // Daily Task Planning
GetPage(
name: '/dashboard/daily-task',
page: () => DailyTaskScreen(),
middlewares: [AuthMiddleware()]),
GetPage( GetPage(
name: '/dashboard/daily-task-planing', name: '/dashboard/daily-task-planing',
page: () => DailyTaskPlaningScreen(), page: () => DailyTaskPlaningScreen(),
@ -67,14 +60,6 @@ getPageRoute() {
name: '/dashboard/daily-task-progress', name: '/dashboard/daily-task-progress',
page: () => DailyProgressReportScreen(), page: () => DailyProgressReportScreen(),
middlewares: [AuthMiddleware()]), middlewares: [AuthMiddleware()]),
GetPage(
name: '/daily-task/report-task',
page: () => ReportTaskScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/daily-task/comment-task',
page: () => CommentTaskScreen(),
middlewares: [AuthMiddleware()]),
// Authentication // Authentication
GetPage(name: '/auth/login', page: () => LoginScreen()), GetPage(name: '/auth/login', page: () => LoginScreen()),
GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()), GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()),

View File

@ -236,8 +236,17 @@ class _MPINAuthScreenState extends State<MPINAuthScreen> with UIMixin {
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly], inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) => onChanged: (value) {
controller.onDigitChanged(value, index, isRetype: isRetype), 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( decoration: InputDecoration(
counterText: '', counterText: '',
filled: true, 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/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.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_card.dart';
import 'package:marco/helpers/widgets/my_container.dart'; import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_flex.dart'; import 'package:marco/helpers/widgets/my_flex.dart';
import 'package:marco/helpers/widgets/my_flex_item.dart'; import 'package:marco/helpers/widgets/my_flex_item.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:marco/controller/permission_controller.dart'; import 'package:marco/controller/permission_controller.dart';
import 'package:intl/intl.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/attendence_action_button.dart';
import 'package:marco/model/attendance/regualrize_action_button.dart'; import 'package:marco/model/attendance/regualrize_action_button.dart';
import 'package:marco/model/attendance/attendence_filter_sheet.dart'; import 'package:marco/model/attendance/attendence_filter_sheet.dart';
import 'package:marco/controller/project_controller.dart';
class AttendanceScreen extends StatefulWidget { class AttendanceScreen extends StatefulWidget {
AttendanceScreen({super.key}); AttendanceScreen({super.key});
@ -35,167 +33,241 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
Get.put(PermissionController()); Get.put(PermissionController());
String selectedTab = 'todaysAttendance'; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Layout( return Scaffold(
child: GetBuilder<AttendanceController>( appBar: PreferredSize(
init: attendanceController, preferredSize: const Size.fromHeight(80),
tag: 'attendance_dashboard_controller', child: AppBar(
builder: (controller) { backgroundColor: const Color(0xFFF5F5F5),
return Column( elevation: 0.5,
crossAxisAlignment: CrossAxisAlignment.start, foregroundColor: Colors.black,
children: [ titleSpacing: 0,
Padding( centerTitle: false,
padding: MySpacing.x(flexSpacing), leading: Padding(
child: Row( padding: const EdgeInsets.only(top: 15.0),
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: IconButton(
children: [ icon: const Icon(Icons.arrow_back_ios_new,
MyText.titleMedium("Attendance", color: Colors.black, size: 20),
fontSize: 18, fontWeight: 600), onPressed: () {
MyBreadcrumb( Get.offNamed('/dashboard');
children: [ },
MyBreadcrumbItem(name: 'Dashboard'), ),
MyBreadcrumbItem(name: 'Attendance', active: true), ),
], 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,
), ),
), const SizedBox(height: 2),
MySpacing.height(flexSpacing), GetBuilder<ProjectController>(
Row( builder: (projectController) {
mainAxisAlignment: MainAxisAlignment.end, 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: [ children: [
MyText.bodyMedium( MySpacing.height(flexSpacing),
"Filter", Row(
fontWeight: 600, mainAxisAlignment: MainAxisAlignment.end,
), children: [
Tooltip( MyText.bodyMedium("Filter", fontWeight: 600),
message: 'Filter Project', Tooltip(
child: InkWell( message: 'Filter Project',
borderRadius: BorderRadius.circular(24), child: InkWell(
onTap: () async { borderRadius: BorderRadius.circular(24),
final result = onTap: () async {
await showModalBottomSheet<Map<String, dynamic>>( final result = await showModalBottomSheet<
context: context, Map<String, dynamic>>(
isScrollControlled: true, context: context,
backgroundColor: Colors.white, isScrollControlled: true,
shape: const RoundedRectangleBorder( backgroundColor: Colors.white,
borderRadius: shape: const RoundedRectangleBorder(
BorderRadius.vertical(top: Radius.circular(12)), borderRadius: BorderRadius.vertical(
), top: Radius.circular(12)),
builder: (context) => AttendanceFilterBottomSheet( ),
controller: attendanceController, builder: (context) => AttendanceFilterBottomSheet(
permissionController: permissionController, controller: attendanceController,
selectedTab: selectedTab, permissionController: permissionController,
), selectedTab: selectedTab,
); ),
);
if (result != null) { if (result != null) {
final selectedProjectId = final selectedProjectId =
result['projectId'] as String?; Get.find<ProjectController>()
final selectedView = result['selectedTab'] as String?; .selectedProjectId
?.value;
if (selectedProjectId != null && final selectedView =
selectedProjectId != result['selectedTab'] as String?;
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']);
}
if (selectedView != null && if (selectedProjectId != null) {
selectedView != selectedTab) { try {
setState(() { await attendanceController
selectedTab = selectedView; .fetchEmployeesByProject(
}); selectedProjectId);
} await attendanceController
} .fetchAttendanceLogs(selectedProjectId);
}, await attendanceController
child: MouseRegion( .fetchRegularizationLogs(
cursor: SystemMouseCursors.click, selectedProjectId);
child: Padding( await attendanceController
padding: const EdgeInsets.all(8.0), .fetchProjectData(selectedProjectId);
child: Icon( } catch (_) {}
Icons.filter_list_alt, attendanceController.update(
color: Colors.blueAccent, ['attendance_dashboard_controller']);
size: 28, }
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),
const SizedBox(width: 4), Tooltip(
MyText.bodyMedium( message: 'Refresh Data',
"Refresh", child: InkWell(
fontWeight: 600, borderRadius: BorderRadius.circular(24),
), onTap: () async {
Tooltip( final projectId = Get.find<ProjectController>()
message: 'Refresh Data', .selectedProjectId
child: InkWell( ?.value;
borderRadius: BorderRadius.circular(24), if (projectId != null && projectId.isNotEmpty) {
onTap: () async { try {
final projectId = await attendanceController
attendanceController.selectedProjectId; .loadAttendanceData(projectId);
if (projectId != null && projectId.isNotEmpty) { attendanceController.update(
try { ['attendance_dashboard_controller']);
await attendanceController } catch (e) {
.fetchEmployeesByProject(projectId); debugPrint("Error refreshing data: $e");
await attendanceController }
.fetchAttendanceLogs(projectId); }
await attendanceController },
.fetchRegularizationLogs(projectId); child: MouseRegion(
await attendanceController cursor: SystemMouseCursors.click,
.fetchProjectData(projectId); child: Padding(
attendanceController padding: const EdgeInsets.all(8.0),
.update(['attendance_dashboard_controller']); child: Icon(
} catch (e) { Icons.refresh,
debugPrint("Error refreshing data: $e"); color: Colors.green,
} size: 28,
} ),
}, ),
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/view/layouts/layout.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/widgets/my_button.dart'; import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/controller/project_controller.dart';
class DashboardScreen extends StatefulWidget { class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key}); const DashboardScreen({super.key});
@ -50,10 +51,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MyText.titleMedium("Dashboard", fontWeight: 600),
MySpacing.height(12), MySpacing.height(12),
_buildDashboardStats(), _buildDashboardStats(),
MySpacing.height(350), MySpacing.height(300),
if (!hasMpin) ...[ if (!hasMpin) ...[
MyCard( MyCard(
borderRadiusAll: 12, borderRadiusAll: 12,
@ -130,46 +130,103 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
DashboardScreen.dailyTasksProgressRoute), DashboardScreen.dailyTasksProgressRoute),
]; ];
return LayoutBuilder( return GetBuilder<ProjectController>(
builder: (context, constraints) { id: 'dashboard_controller',
double maxWidth = constraints.maxWidth; builder: (controller) {
int crossAxisCount = (maxWidth / 100).floor().clamp(2, 4); final bool isProjectSelected = controller.selectedProject != null;
double cardWidth =
(maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount;
return Wrap( return Column(
spacing: 10, crossAxisAlignment: CrossAxisAlignment.start,
runSpacing: 10, children: [
children: if (!isProjectSelected)
stats.map((stat) => _buildStatCard(stat, cardWidth)).toList(), 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) { Widget _buildStatCard(_StatItem statItem, double width, bool isEnabled) {
return InkWell( return Opacity(
onTap: () => Get.toNamed(statItem.route), opacity: isEnabled ? 1.0 : 0.4,
borderRadius: BorderRadius.circular(10), child: IgnorePointer(
child: MyCard.bordered( ignoring: !isEnabled,
width: width, child: InkWell(
height: 100, onTap: () {
paddingAll: 5, if (!isEnabled) {
borderRadiusAll: 10, Get.defaultDialog(
border: Border.all(color: Colors.grey.withOpacity(0.15)), title: "No Project Selected",
shadow: MyShadow(elevation: 1.5, position: MyShadowPosition.bottom), middleText:
child: Column( "You need to select a project before accessing this section.",
mainAxisAlignment: MainAxisAlignment.center, confirm: ElevatedButton(
children: [ onPressed: () => Get.back(),
_buildStatCardIcon(statItem), child: const Text("OK"),
MySpacing.height(8), ),
MyText.labelSmall( );
statItem.title, } else {
maxLines: 2, Get.toNamed(statItem.route);
overflow: TextOverflow.visible, }
textAlign: TextAlign.center, },
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_card.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/permission_controller.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/model/employees/add_employee_bottom_sheet.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart'; import 'package:marco/controller/dashboard/employees_screen_controller.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/model/employees/employee_detail_bottom_sheet.dart'; import 'package:marco/model/employees/employee_detail_bottom_sheet.dart';
import 'package:marco/controller/project_controller.dart';
class EmployeesScreen extends StatefulWidget { class EmployeesScreen extends StatefulWidget {
const EmployeesScreen({super.key}); const EmployeesScreen({super.key});
@ -28,58 +27,101 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
Get.put(EmployeesScreenController()); Get.put(EmployeesScreenController());
final PermissionController permissionController = final PermissionController permissionController =
Get.put(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 { if (isAllSelected) {
final result = await showModalBottomSheet<Map<String, dynamic>>( employeeScreenController.selectedProjectId = null;
context: context, await employeeScreenController.fetchAllEmployees();
isScrollControlled: true, } else if (selectedProjectId != null) {
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) {
employeeScreenController.selectedProjectId = selectedProjectId; employeeScreenController.selectedProjectId = selectedProjectId;
await employeeScreenController
try { .fetchEmployeesByProject(selectedProjectId);
if (selectedProjectId == null) { } else {
await employeeScreenController.fetchAllEmployees(); // Clear employees if neither selected
} else { employeeScreenController.clearEmployees();
await employeeScreenController
.fetchEmployeesByProject(selectedProjectId);
}
} catch (e) {
debugPrint('Error fetching employees: ${e.toString()}');
}
employeeScreenController.update(['employee_screen_controller']);
} }
employeeScreenController.update(['employee_screen_controller']);
} catch (e, stackTrace) {
debugPrint('Error refreshing employee data: ${e.toString()}');
debugPrintStack(stackTrace: stackTrace);
} }
} }
Future<void> _refreshEmployees() async { @override
try { void initState() {
final projectId = employeeScreenController.selectedProjectId; super.initState();
if (projectId == null) { final selectedProjectId = Get.find<ProjectController>().selectedProject?.id;
await employeeScreenController.fetchAllEmployees();
} else { if (selectedProjectId != null) {
await employeeScreenController.fetchEmployeesByProject(projectId); employeeScreenController.selectedProjectId = selectedProjectId;
} employeeScreenController.fetchEmployeesByProject(selectedProjectId);
} catch (e) { } else if (employeeScreenController.isAllEmployeeSelected.value) {
debugPrint('Error refreshing employee data: ${e.toString()}'); employeeScreenController.selectedProjectId = null;
employeeScreenController.fetchAllEmployees();
} else {
employeeScreenController.clearEmployees();
} }
} }
@override @override
Widget build(BuildContext context) { 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( floatingActionButton: InkWell(
onTap: () async { onTap: () async {
final result = await showModalBottomSheet<bool>( final result = await showModalBottomSheet<bool>(
@ -120,13 +162,14 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
), ),
), ),
), ),
child: GetBuilder<EmployeesScreenController>( body: SafeArea(
init: employeeScreenController, child: GetBuilder<EmployeesScreenController>(
tag: 'employee_screen_controller', init: employeeScreenController,
builder: (controller) { tag: 'employee_screen_controller',
return Stack( builder: (controller) {
children: [ return SingleChildScrollView(
Column( padding: const EdgeInsets.only(bottom: 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( Padding(
@ -134,11 +177,6 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
MyText.titleMedium(
"Employees",
fontSize: 18,
fontWeight: 600,
),
MyBreadcrumb( MyBreadcrumb(
children: [ children: [
MyBreadcrumbItem(name: 'Dashboard'), MyBreadcrumbItem(name: 'Dashboard'),
@ -154,33 +192,67 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
MyText.bodyMedium( Obx(() {
"Filter", return Row(
fontWeight: 600, children: [
), Checkbox(
Tooltip( value: employeeScreenController
message: 'Project', .isAllEmployeeSelected.value,
child: InkWell( activeColor: Colors.blueAccent,
borderRadius: BorderRadius.circular(24), fillColor:
onTap: _openFilterSheet, MaterialStateProperty.resolveWith<Color?>(
child: MouseRegion( (states) {
cursor: SystemMouseCursors.click, if (states.contains(MaterialState.selected)) {
child: const Padding( return Colors.blueAccent;
padding: EdgeInsets.all(8), }
child: Icon( return Colors.transparent;
Icons.filter_list_alt, }),
color: Colors.blueAccent, checkColor: Colors.white,
size: 28, 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']);
},
), ),
), MyText.bodyMedium(
), "All Employees",
), fontWeight: 600,
const SizedBox(width: 8), ),
MyText.bodyMedium( ],
"Refresh", );
fontWeight: 600, }),
), const SizedBox(width: 16),
MyText.bodyMedium("Refresh", fontWeight: 600),
Tooltip( Tooltip(
message: 'Refresh Data', message: 'Refresh Data',
child: InkWell( 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(() { return Obx(() {
final isLoading = employeeScreenController.isLoading.value; final isLoading = employeeScreenController.isLoading.value;
final employees = employeeScreenController.employees; final employees = employeeScreenController.employees;
if (isLoading) { if (isLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (employees.isEmpty) { if (employees.isEmpty) {
return Center( return Padding(
child: MyText.bodySmall( padding: const EdgeInsets.only(top: 50),
"No Assigned Employees Found", child: Center(
fontWeight: 600, 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:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/view/layouts/left_bar.dart'; import 'package:marco/controller/layout/layout_controller.dart';
import 'package:marco/view/layouts/right_bar.dart'; import 'package:marco/helpers/widgets/my_responsive.dart';
import 'package:marco/view/layouts/top_bar.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/widgets/custom_pop_menu.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/model/employee_info.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/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? child;
final Widget? floatingActionButton; 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 LayoutController controller = LayoutController();
final topBarTheme = AdminTheme.theme.topBarTheme;
final contentTheme = AdminTheme.theme.contentTheme;
final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MyResponsive(builder: (BuildContext context, _, screenMT) { return MyResponsive(builder: (context, _, screenMT) {
return GetBuilder( return GetBuilder(
init: controller, init: controller,
builder: (controller) { builder: (_) {
if (screenMT.isMobile || screenMT.isTablet) { return (screenMT.isMobile || screenMT.isTablet)
return mobileScreen(); ? _buildScaffold(context, isMobile: true)
} else { : _buildScaffold(context);
return largeScreen(); },
} );
});
}); });
} }
Widget mobileScreen() { Widget _buildScaffold(BuildContext context, {bool isMobile = false}) {
return Scaffold( return Scaffold(
key: controller.scaffoldKey, key: controller.scaffoldKey,
appBar: AppBar( endDrawer: UserProfileBar(),
elevation: 0, floatingActionButton: widget.floatingActionButton,
actions: [ body: SafeArea(
Column( child: Stack(
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Column(
MySpacing.width(6), children: [
if (isBetaEnvironment) _buildHeader(context, isMobile),
Padding( Expanded(
padding: const EdgeInsets.symmetric( child: SingleChildScrollView(
vertical: 17.0, horizontal: 8.0), 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( child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blueAccent, color: Colors.white,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(12),
), ),
child: Text( padding: const EdgeInsets.all(10),
'BETA', child: _buildProjectList(context, isMobile),
style: TextStyle( ),
color: Colors.white, ),
fontWeight: FontWeight.bold, );
fontSize: 12, }),
],
),
),
);
}
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/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.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_card.dart';
import 'package:marco/helpers/widgets/my_container.dart'; import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/permission_controller.dart'; import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/dashboard/daily_task_controller.dart'; import 'package:marco/controller/dashboard/daily_task_controller.dart';
import 'package:marco/model/dailyTaskPlaning/daily_progress_report_filter.dart'; import 'package:marco/model/dailyTaskPlaning/daily_progress_report_filter.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/model/dailyTaskPlaning/comment_task_bottom_sheet.dart'; import 'package:marco/model/dailyTaskPlaning/comment_task_bottom_sheet.dart';
import 'package:marco/model/dailyTaskPlaning/report_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 { class DailyProgressReportScreen extends StatefulWidget {
const DailyProgressReportScreen({super.key}); const DailyProgressReportScreen({super.key});
@ -40,46 +38,110 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
Get.put(DailyTaskController()); Get.put(DailyTaskController());
final PermissionController permissionController = final PermissionController permissionController =
Get.put(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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Layout( return Scaffold(
child: GetBuilder<DailyTaskController>( appBar: PreferredSize(
init: dailyTaskController, preferredSize: const Size.fromHeight(80),
tag: 'daily_progress_report_controller', child: AppBar(
builder: (controller) { backgroundColor: const Color(0xFFF5F5F5),
return Column( elevation: 0.5,
crossAxisAlignment: CrossAxisAlignment.start, foregroundColor: Colors.black,
children: [ titleSpacing: 0,
_buildHeader(), centerTitle: false,
MySpacing.height(flexSpacing), leading: Padding(
_buildActionBar(), padding: const EdgeInsets.only(top: 15.0), // Aligns with title
Padding( child: IconButton(
padding: MySpacing.x(flexSpacing), icon: const Icon(Icons.arrow_back_ios_new,
child: _buildDailyProgressReportTab(), color: Colors.black, size: 20),
), onPressed: () {
], Get.offNamed('/dashboard');
); },
}, ),
),
);
}
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),
],
), ),
], 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, 'text': comment.comment,
'date': isoDate, 'date': isoDate,
'commentedBy': commenterName, 'commentedBy': commenterName,
'preSignedUrls':
comment.preSignedUrls,
}; };
}).toList(); }).toList();
final taskLevelPreSignedUrls =
task.reportedPreSignedUrls;
final taskData = { final taskData = {
'activity': activityName, 'activity': activityName,
@ -566,6 +632,8 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
'teamSize': task.teamMembers.length, 'teamSize': task.teamMembers.length,
'teamMembers': teamMembers, 'teamMembers': teamMembers,
'taskComments': taskComments, 'taskComments': taskComments,
'reportedPreSignedUrls':
taskLevelPreSignedUrls,
}; };
showModalBottomSheet( showModalBottomSheet(

View File

@ -3,17 +3,14 @@ import 'package:get/get.dart';
import 'package:marco/helpers/theme/app_theme.dart'; import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.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_card.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/permission_controller.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/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:percent_indicator/percent_indicator.dart';
import 'package:marco/model/dailyTaskPlaning/assign_task_bottom_sheet .dart';
class DailyTaskPlaningScreen extends StatefulWidget { class DailyTaskPlaningScreen extends StatefulWidget {
DailyTaskPlaningScreen({super.key}); DailyTaskPlaningScreen({super.key});
@ -28,149 +25,141 @@ class _DailyTaskPlaningScreenState extends State<DailyTaskPlaningScreen>
Get.put(DailyTaskPlaningController()); Get.put(DailyTaskPlaningController());
final PermissionController permissionController = final PermissionController permissionController =
Get.put(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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Layout( return Scaffold(
child: GetBuilder<DailyTaskPlaningController>( appBar: PreferredSize(
init: dailyTaskPlaningController, preferredSize: const Size.fromHeight(80),
tag: 'daily_task_planing_controller', child: AppBar(
builder: (controller) { backgroundColor: const Color(0xFFF5F5F5),
return Column( elevation: 0.5,
crossAxisAlignment: CrossAxisAlignment.start, foregroundColor: Colors.black,
children: [ titleSpacing: 0,
Padding( centerTitle: false,
padding: MySpacing.x(flexSpacing), leading: Padding(
child: Row( padding: const EdgeInsets.only(top: 15.0), // Aligns with title
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: IconButton(
children: [ icon: const Icon(Icons.arrow_back_ios_new,
MyText.titleMedium("Daily Task Planning", color: Colors.black, size: 20),
fontSize: 18, fontWeight: 600), onPressed: () {
MyBreadcrumb( 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: [ children: [
MyBreadcrumbItem(name: 'Dashboard'), const SizedBox(width: 8),
MyBreadcrumbItem( MyText.bodyMedium("Refresh", fontWeight: 600),
name: 'Daily Task Planning', active: true), 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),
),
),
),
),
], ],
), ),
], ),
), Padding(
), padding: MySpacing.x(flexSpacing),
MySpacing.height(flexSpacing), child: dailyProgressReportTab(),
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(),
),
],
);
},
), ),
); );
} }

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 path: ^1.9.0
percent_indicator: ^4.2.2 percent_indicator: ^4.2.2
flutter_contacts: ^1.1.9+2 flutter_contacts: ^1.1.9+2
photo_view: ^0.15.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@ -99,8 +100,6 @@ flutter:
- assets/country/ - assets/country/
- assets/data/ - assets/data/
- assets/dummy/ - assets/dummy/
- assets/dummy/ecommerce/
- assets/dummy/single_product/
- assets/lang/ - assets/lang/
- assets/logo/ - assets/logo/
- assets/logo/loading_logo.png - assets/logo/loading_logo.png