Merge pull request 'Dashboard_Charts' (#67) from Dashboard_Charts into main

Reviewed-on: #67
This commit is contained in:
vaibhav.surve 2025-09-01 09:41:21 +00:00
commit 40a4a77af5
42 changed files with 2082 additions and 921 deletions

View File

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

View File

@ -20,7 +20,7 @@ class DynamicMenuController extends GetxController {
/// Auto refresh every 5 minutes (adjust as needed) /// Auto refresh every 5 minutes (adjust as needed)
_autoRefreshTimer = Timer.periodic( _autoRefreshTimer = Timer.periodic(
const Duration(minutes: 1), const Duration(minutes: 15),
(_) => fetchMenu(), (_) => fetchMenu(),
); );
} }

View File

@ -4,6 +4,11 @@ class ApiEndpoints {
// Dashboard Module API Endpoints // Dashboard Module API Endpoints
static const String getDashboardAttendanceOverview = "/dashboard/attendance-overview"; static const String getDashboardAttendanceOverview = "/dashboard/attendance-overview";
static const String getDashboardProjectProgress = "/dashboard/progression";
static const String getDashboardTasks = "/dashboard/tasks";
static const String getDashboardTeams = "/dashboard/teams";
static const String getDashboardProjects = "/dashboard/projects";
// Attendance Module API Endpoints // Attendance Module API Endpoints
static const String getProjects = "/project/list"; static const String getProjects = "/project/list";

View File

@ -8,7 +8,9 @@ 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:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:marco/model/dashboard/project_progress_model.dart';
import 'package:marco/model/dashboard/dashboard_tasks_model.dart';
import 'package:marco/model/dashboard/dashboard_teams_model.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class ApiService { class ApiService {
@ -611,6 +613,73 @@ class ApiService {
} }
// === Dashboard Endpoints === // === Dashboard Endpoints ===
/// Get Dashboard Tasks
static Future<DashboardTasks?> getDashboardTasks(
{required String projectId}) async {
try {
final queryParams = {'projectId': projectId};
final response = await _getRequest(ApiEndpoints.getDashboardTasks,
queryParams: queryParams);
if (response == null || response.body.trim().isEmpty) {
logSafe("Dashboard tasks request failed or response empty",
level: LogLevel.error);
return null;
}
final jsonResponse = jsonDecode(response.body);
if (jsonResponse is Map<String, dynamic> &&
jsonResponse['success'] == true) {
logSafe(
"Dashboard tasks fetched successfully: ${jsonResponse['data']}");
return DashboardTasks.fromJson(jsonResponse);
} else {
logSafe(
"Failed to fetch dashboard tasks: ${jsonResponse['message'] ?? 'Unknown error'}",
level: LogLevel.warning);
}
} catch (e, stack) {
logSafe("Exception during getDashboardTasks API: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return null;
}
/// Get Dashboard Teams
static Future<DashboardTeams?> getDashboardTeams(
{required String projectId}) async {
try {
final queryParams = {'projectId': projectId};
final response = await _getRequest(ApiEndpoints.getDashboardTeams,
queryParams: queryParams);
if (response == null || response.body.trim().isEmpty) {
logSafe("Dashboard teams request failed or response empty",
level: LogLevel.error);
return null;
}
final jsonResponse = jsonDecode(response.body);
if (jsonResponse is Map<String, dynamic> &&
jsonResponse['success'] == true) {
logSafe(
"Dashboard teams fetched successfully: ${jsonResponse['data']}");
return DashboardTeams.fromJson(jsonResponse);
} else {
logSafe(
"Failed to fetch dashboard teams: ${jsonResponse['message'] ?? 'Unknown error'}",
level: LogLevel.warning);
}
} catch (e, stack) {
logSafe("Exception during getDashboardTeams API: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return null;
}
static Future<List<dynamic>?> getDashboardAttendanceOverview( static Future<List<dynamic>?> getDashboardAttendanceOverview(
String projectId, int days) async { String projectId, int days) async {
@ -625,6 +694,49 @@ class ApiService {
: null); : null);
} }
/// Fetch Project Progress
static Future<ProjectResponse?> getProjectProgress({
required String projectId,
required int days,
DateTime? fromDate, // make optional
}) async {
const endpoint = ApiEndpoints.getDashboardProjectProgress;
// Use today's date if fromDate is not provided
final actualFromDate = fromDate ?? DateTime.now();
final queryParams = {
"projectId": projectId,
"days": days.toString(),
"FromDate":
DateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS").format(actualFromDate),
};
try {
final response = await _getRequest(endpoint, queryParams: queryParams);
if (response == null) {
logSafe(
"Project Progress request failed: null response",
level: LogLevel.error,
);
return null;
}
final parsed =
_parseResponseForAllData(response, label: "ProjectProgress");
if (parsed != null) {
logSafe("✅ Project progress fetched successfully");
return ProjectResponse.fromJson(parsed);
}
} catch (e, stack) {
logSafe("Exception during getProjectProgress: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return null;
}
/// Directory calling the API /// Directory calling the API
static Future<bool> deleteBucket(String id) async { static Future<bool> deleteBucket(String id) async {

View File

@ -1,8 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/controller/attendance/attendance_screen_controller.dart';
import 'package:marco/controller/dashboard/daily_task_controller.dart'; import 'package:marco/controller/task_planning/daily_task_controller.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:marco/controller/expense/expense_screen_controller.dart'; import 'package:marco/controller/expense/expense_screen_controller.dart';
import 'package:marco/controller/expense/expense_detail_controller.dart'; import 'package:marco/controller/expense/expense_detail_controller.dart';

View File

@ -30,8 +30,9 @@ class Avatar extends StatelessWidget {
paddingAll: 0, paddingAll: 0,
color: bgColor, color: bgColor,
child: Center( child: Center(
child: MyText.labelSmall( child: MyText(
initials, initials,
fontSize: size * 0.45, // 👈 scales with avatar size
fontWeight: 600, fontWeight: 600,
color: textColor, color: textColor,
), ),

View File

@ -0,0 +1,469 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
class AttendanceDashboardChart extends StatelessWidget {
AttendanceDashboardChart({Key? key}) : super(key: key);
final DashboardController _controller = Get.find<DashboardController>();
static const List<Color> _flatColors = [
Color(0xFFE57373), // Red 300
Color(0xFF64B5F6), // Blue 300
Color(0xFF81C784), // Green 300
Color(0xFFFFB74D), // Orange 300
Color(0xFFBA68C8), // Purple 300
Color(0xFFFF8A65), // Deep Orange 300
Color(0xFF4DB6AC), // Teal 300
Color(0xFFA1887F), // Brown 400
Color(0xFFDCE775), // Lime 300
Color(0xFF9575CD), // Deep Purple 300
Color(0xFF7986CB), // Indigo 300
Color(0xFFAED581), // Light Green 300
Color(0xFFFF7043), // Deep Orange 400
Color(0xFF4FC3F7), // Light Blue 300
Color(0xFFFFD54F), // Amber 300
Color(0xFF90A4AE), // Blue Grey 300
Color(0xFFE573BB), // Pink 300
Color(0xFF81D4FA), // Light Blue 200
Color(0xFFBCAAA4), // Brown 300
Color(0xFFA5D6A7), // Green 300
Color(0xFFCE93D8), // Purple 200
Color(0xFFFF8A65), // Deep Orange 300 (repeat to fill)
Color(0xFF80CBC4), // Teal 200
Color(0xFFFFF176), // Yellow 300
Color(0xFF90CAF9), // Blue 200
Color(0xFFE0E0E0), // Grey 300
Color(0xFFF48FB1), // Pink 200
Color(0xFFA1887F), // Brown 400 (repeat)
Color(0xFFB0BEC5), // Blue Grey 200
Color(0xFF81C784), // Green 300 (repeat)
Color(0xFFFFB74D), // Orange 300 (repeat)
Color(0xFF64B5F6), // Blue 300 (repeat)
];
static final Map<String, Color> _roleColorMap = {};
Color _getRoleColor(String role) {
return _roleColorMap.putIfAbsent(
role,
() => _flatColors[_roleColorMap.length % _flatColors.length],
);
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Obx(() {
final isChartView = _controller.attendanceIsChartView.value;
final selectedRange = _controller.attendanceSelectedRange.value;
final isLoading = _controller.isAttendanceLoading.value;
final filteredData = _getFilteredData();
if (isLoading) {
return SkeletonLoaders.buildLoadingSkeleton();
}
return Container(
decoration: _containerDecoration,
padding: EdgeInsets.symmetric(
vertical: 16,
horizontal: screenWidth < 600 ? 8 : 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Header(
selectedRange: selectedRange,
isChartView: isChartView,
screenWidth: screenWidth,
onToggleChanged: (isChart) =>
_controller.attendanceIsChartView.value = isChart,
onRangeChanged: _controller.updateAttendanceRange,
),
const SizedBox(height: 12),
Expanded(
child: filteredData.isEmpty
? _NoDataMessage()
: isChartView
? _AttendanceChart(
data: filteredData, getRoleColor: _getRoleColor)
: _AttendanceTable(
data: filteredData,
getRoleColor: _getRoleColor,
screenWidth: screenWidth),
),
],
),
);
});
}
BoxDecoration get _containerDecoration => BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
blurRadius: 6,
spreadRadius: 1,
offset: const Offset(0, 2),
),
],
);
List<Map<String, dynamic>> _getFilteredData() {
final now = DateTime.now();
final daysBack = _controller.getAttendanceDays();
return _controller.roleWiseData.where((entry) {
final date = DateTime.parse(entry['date'] as String);
return date.isAfter(now.subtract(Duration(days: daysBack))) &&
!date.isAfter(now);
}).toList();
}
}
// Header
class _Header extends StatelessWidget {
const _Header({
Key? key,
required this.selectedRange,
required this.isChartView,
required this.screenWidth,
required this.onToggleChanged,
required this.onRangeChanged,
}) : super(key: key);
final String selectedRange;
final bool isChartView;
final double screenWidth;
final ValueChanged<bool> onToggleChanged;
final ValueChanged<String> onRangeChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium('Attendance Overview', fontWeight: 700),
const SizedBox(height: 2),
MyText.bodySmall('Role-wise present count',
color: Colors.grey),
],
),
),
ToggleButtons(
borderRadius: BorderRadius.circular(6),
borderColor: Colors.grey,
fillColor: Colors.blueAccent.withOpacity(0.15),
selectedBorderColor: Colors.blueAccent,
selectedColor: Colors.blueAccent,
color: Colors.grey,
constraints: BoxConstraints(
minHeight: 30,
minWidth: screenWidth < 400 ? 28 : 36,
),
isSelected: [isChartView, !isChartView],
onPressed: (index) => onToggleChanged(index == 0),
children: const [
Icon(Icons.bar_chart_rounded, size: 15),
Icon(Icons.table_chart, size: 15),
],
),
],
),
const SizedBox(height: 8),
Row(
children: ["7D", "15D", "30D"]
.map(
(label) => Padding(
padding: const EdgeInsets.only(right: 4),
child: ChoiceChip(
label: Text(label, style: const TextStyle(fontSize: 12)),
padding:
const EdgeInsets.symmetric(horizontal: 5, vertical: 0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
selected: selectedRange == label,
onSelected: (_) => onRangeChanged(label),
selectedColor: Colors.blueAccent.withOpacity(0.15),
backgroundColor: Colors.grey.shade200,
labelStyle: TextStyle(
color: selectedRange == label
? Colors.blueAccent
: Colors.black87,
fontWeight: selectedRange == label
? FontWeight.w600
: FontWeight.normal,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
side: BorderSide(
color: selectedRange == label
? Colors.blueAccent
: Colors.grey.shade300,
),
),
),
),
)
.toList(),
),
],
);
}
}
// No Data
class _NoDataMessage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
height: 180,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.info_outline, color: Colors.grey.shade400, size: 48),
const SizedBox(height: 10),
MyText.bodyMedium(
'No attendance data available for this range.',
textAlign: TextAlign.center,
color: Colors.grey.shade500,
),
],
),
),
);
}
}
// Chart
class _AttendanceChart extends StatelessWidget {
const _AttendanceChart({
Key? key,
required this.data,
required this.getRoleColor,
}) : super(key: key);
final List<Map<String, dynamic>> data;
final Color Function(String role) getRoleColor;
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('d MMMM');
final uniqueDates = data
.map((e) => DateTime.parse(e['date'] as String))
.toSet()
.toList()
..sort();
final filteredDates = uniqueDates.map(dateFormat.format).toList();
final filteredRoles = data.map((e) => e['role'] as String).toSet().toList();
final allZero = filteredRoles.every((role) {
return data
.where((entry) => entry['role'] == role)
.every((entry) => (entry['present'] ?? 0) == 0);
});
if (allZero) {
return Container(
height: 600,
decoration: BoxDecoration(
color: Colors.blueGrey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'No attendance data for the selected range.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey),
),
),
);
}
final formattedMap = {
for (var e in data)
'${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}':
e['present'],
};
final rolesWithData = filteredRoles.where((role) {
return data
.any((entry) => entry['role'] == role && (entry['present'] ?? 0) > 0);
}).toList();
return Container(
height: 600,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.blueGrey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: SfCartesianChart(
tooltipBehavior: TooltipBehavior(enable: true, shared: true),
legend: Legend(isVisible: true, position: LegendPosition.bottom),
primaryXAxis: CategoryAxis(labelRotation: 45),
primaryYAxis: NumericAxis(minimum: 0, interval: 1),
series: rolesWithData.map((role) {
final seriesData = filteredDates
.map((date) {
final key = '${role}_$date';
return {'date': date, 'present': formattedMap[key] ?? 0};
})
.where((d) => (d['present'] ?? 0) > 0)
.toList(); // remove 0 bars
return StackedColumnSeries<Map<String, dynamic>, String>(
dataSource: seriesData,
xValueMapper: (d, _) => d['date'],
yValueMapper: (d, _) => d['present'],
name: role,
color: getRoleColor(role),
dataLabelSettings: DataLabelSettings(
isVisible: true,
builder: (dynamic data, _, __, ___, ____) {
return (data['present'] ?? 0) > 0
? Text(
NumberFormat.decimalPattern().format(data['present']),
style: const TextStyle(fontSize: 11),
)
: const SizedBox.shrink();
},
),
);
}).toList(),
),
);
}
}
// Table
class _AttendanceTable extends StatelessWidget {
const _AttendanceTable({
Key? key,
required this.data,
required this.getRoleColor,
required this.screenWidth,
}) : super(key: key);
final List<Map<String, dynamic>> data;
final Color Function(String role) getRoleColor;
final double screenWidth;
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('d MMMM');
final uniqueDates = data
.map((e) => DateTime.parse(e['date'] as String))
.toSet()
.toList()
..sort();
final filteredDates = uniqueDates.map(dateFormat.format).toList();
final filteredRoles = data.map((e) => e['role'] as String).toSet().toList();
final allZero = filteredRoles.every((role) {
return data
.where((entry) => entry['role'] == role)
.every((entry) => (entry['present'] ?? 0) == 0);
});
if (allZero) {
return Container(
height: 300,
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text(
'No attendance data for the selected range.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey),
),
),
);
}
final formattedMap = {
for (var e in data)
'${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}':
e['present'],
};
return Container(
height: 300,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
color: Colors.grey.shade50,
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: screenWidth < 600 ? 20 : 36,
headingRowHeight: 44,
headingRowColor:
MaterialStateProperty.all(Colors.blueAccent.withOpacity(0.08)),
headingTextStyle: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.black87),
columns: [
const DataColumn(label: Text('Role')),
...filteredDates.map((d) => DataColumn(label: Text(d))),
],
rows: filteredRoles.map((role) {
return DataRow(
cells: [
DataCell(_RolePill(role: role, color: getRoleColor(role))),
...filteredDates.map((date) {
final key = '${role}_$date';
return DataCell(
Text(
NumberFormat.decimalPattern()
.format(formattedMap[key] ?? 0),
style: const TextStyle(fontSize: 13),
),
);
}),
],
);
}).toList(),
),
),
);
}
}
class _RolePill extends StatelessWidget {
const _RolePill({Key? key, required this.role, required this.color})
: super(key: key);
final String role;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(6),
),
child: MyText.labelSmall(role, fontWeight: 500),
);
}
}

View File

@ -0,0 +1,277 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:marco/helpers/widgets/my_text.dart'; // import MyText
import 'package:intl/intl.dart';
class DashboardOverviewWidgets {
static final DashboardController dashboardController =
Get.find<DashboardController>();
static const _titleTextStyle = TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.black87,
);
static const _subtitleTextStyle = TextStyle(
fontSize: 14,
color: Colors.grey,
);
static const _infoNumberTextStyle = TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
);
static const _infoNumberGreenTextStyle = TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
);
static final NumberFormat _commaFormatter = NumberFormat.decimalPattern();
/// Teams Overview Card without chart, labels & values in rows
static Widget teamsOverview() {
return Obx(() {
if (dashboardController.isTeamsLoading.value) {
return _loadingSkeletonCard("Teams");
}
final total = dashboardController.totalEmployees.value;
final inToday = dashboardController.inToday.value;
return LayoutBuilder(
builder: (context, constraints) {
final cardWidth = constraints.maxWidth > 400
? (constraints.maxWidth / 2) - 10
: constraints.maxWidth;
return SizedBox(
width: cardWidth,
child: MyCard(
borderRadiusAll: 16,
paddingAll: 20,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.group,
color: Colors.blueAccent, size: 26),
MySpacing.width(8),
MyText("Teams", style: _titleTextStyle),
],
),
MySpacing.height(16),
// Labels in one row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText("Total Employees", style: _subtitleTextStyle),
MyText("In Today", style: _subtitleTextStyle),
],
),
MySpacing.height(4),
// Values in one row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText(_commaFormatter.format(total),
style: _infoNumberTextStyle),
MyText(_commaFormatter.format(inToday),
style: _infoNumberGreenTextStyle.copyWith(
color: Colors.green[700])),
],
),
],
),
),
);
},
);
});
}
/// Tasks Overview Card
static Widget tasksOverview() {
return Obx(() {
if (dashboardController.isTasksLoading.value) {
return _loadingSkeletonCard("Tasks");
}
final total = dashboardController.totalTasks.value;
final completed = dashboardController.completedTasks.value;
final remaining = total - completed;
final double percent = total > 0 ? completed / total : 0.0;
// Task colors
const completedColor = Color(0xFFE57373); // red
const remainingColor = Color(0xFF64B5F6); // blue
final List<_ChartData> pieData = [
_ChartData('Completed', completed.toDouble(), completedColor),
_ChartData('Remaining', remaining.toDouble(), remainingColor),
];
return LayoutBuilder(
builder: (context, constraints) {
final cardWidth =
constraints.maxWidth < 300 ? constraints.maxWidth : 300.0;
return SizedBox(
width: cardWidth,
child: MyCard(
borderRadiusAll: 16,
paddingAll: 20,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icon + Title
Row(
children: [
const Icon(Icons.task_alt,
color: completedColor, size: 26),
MySpacing.width(8),
MyText("Tasks", style: _titleTextStyle),
],
),
MySpacing.height(16),
// Main Row: Bigger Pie Chart + Full-Color Info Boxes
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Pie Chart Column (Bigger)
SizedBox(
height: 140,
width: 140,
child: SfCircularChart(
annotations: <CircularChartAnnotation>[
CircularChartAnnotation(
widget: MyText(
"${(percent * 100).toInt()}%",
style: _infoNumberGreenTextStyle.copyWith(
fontSize: 20),
),
),
],
series: <PieSeries<_ChartData, String>>[
PieSeries<_ChartData, String>(
dataSource: pieData,
xValueMapper: (_ChartData data, _) =>
data.category,
yValueMapper: (_ChartData data, _) => data.value,
pointColorMapper: (_ChartData data, _) =>
data.color,
dataLabelSettings:
const DataLabelSettings(isVisible: false),
radius: '100%',
),
],
),
),
MySpacing.width(16),
// Info Boxes Column (Full Color)
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_infoBoxFullColor(
"Completed", completed, completedColor),
MySpacing.height(8),
_infoBoxFullColor(
"Remaining", remaining, remainingColor),
],
),
),
],
),
],
),
),
);
},
);
});
}
/// Full-color info box
static Widget _infoBoxFullColor(String label, int value, Color bgColor) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: bgColor, // full color
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
MyText(_commaFormatter.format(value),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
)),
MySpacing.height(2),
MyText(label,
style: const TextStyle(
fontSize: 12,
color: Colors.white, // text in white for contrast
)),
],
),
);
}
/// Loading Skeleton Card
static Widget _loadingSkeletonCard(String title) {
return LayoutBuilder(builder: (context, constraints) {
final cardWidth =
constraints.maxWidth < 200 ? constraints.maxWidth : 200.0;
return SizedBox(
width: cardWidth,
child: MyCard(
borderRadiusAll: 16,
paddingAll: 20,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_loadingBar(width: 100),
MySpacing.height(12),
_loadingBar(width: 80),
MySpacing.height(12),
_loadingBar(width: double.infinity, height: 12),
],
),
),
);
});
}
static Widget _loadingBar(
{double width = double.infinity, double height = 16}) {
return Container(
height: height,
width: width,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6),
),
);
}
}
class _ChartData {
final String category;
final double value;
final Color color;
_ChartData(this.category, this.value, this.color);
}

View File

@ -0,0 +1,366 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:marco/model/dashboard/project_progress_model.dart';
import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
class ProjectProgressChart extends StatelessWidget {
final List<ChartTaskData> data;
final DashboardController controller = Get.find<DashboardController>();
ProjectProgressChart({super.key, required this.data});
// ================= Flat Colors =================
static const List<Color> _flatColors = [
Color(0xFFE57373),
Color(0xFF64B5F6),
Color(0xFF81C784),
Color(0xFFFFB74D),
Color(0xFFBA68C8),
Color(0xFFFF8A65),
Color(0xFF4DB6AC),
Color(0xFFA1887F),
Color(0xFFDCE775),
Color(0xFF9575CD),
Color(0xFF7986CB),
Color(0xFFAED581),
Color(0xFFFF7043),
Color(0xFF4FC3F7),
Color(0xFFFFD54F),
Color(0xFF90A4AE),
Color(0xFFE573BB),
Color(0xFF81D4FA),
Color(0xFFBCAAA4),
Color(0xFFA5D6A7),
Color(0xFFCE93D8),
Color(0xFFFF8A65),
Color(0xFF80CBC4),
Color(0xFFFFF176),
Color(0xFF90CAF9),
Color(0xFFE0E0E0),
Color(0xFFF48FB1),
Color(0xFFA1887F),
Color(0xFFB0BEC5),
Color(0xFF81C784),
Color(0xFFFFB74D),
Color(0xFF64B5F6),
];
static final NumberFormat _commaFormatter = NumberFormat.decimalPattern();
static final Map<String, Color> _taskColorMap = {};
Color _getTaskColor(String taskName) {
return _taskColorMap.putIfAbsent(
taskName,
() => _flatColors[_taskColorMap.length % _flatColors.length],
);
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Obx(() {
final isChartView = controller.projectIsChartView.value;
final selectedRange = controller.projectSelectedRange.value;
final isLoading = controller.isProjectLoading.value;
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.04),
blurRadius: 6,
spreadRadius: 1,
offset: Offset(0, 2),
),
],
),
padding: EdgeInsets.symmetric(
vertical: 16,
horizontal: screenWidth < 600 ? 8 : 24,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(selectedRange, isChartView, screenWidth),
const SizedBox(height: 14),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) => AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isLoading
? SkeletonLoaders.buildLoadingSkeleton()
: data.isEmpty
? _buildNoDataMessage()
: isChartView
? _buildChart(constraints.maxHeight)
: _buildTable(constraints.maxHeight, screenWidth),
),
),
),
],
),
);
});
}
Widget _buildHeader(
String selectedRange, bool isChartView, double screenWidth) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium('Project Progress', fontWeight: 700),
MyText.bodySmall('Planned vs Completed',
color: Colors.grey.shade700),
],
),
),
ToggleButtons(
borderRadius: BorderRadius.circular(6),
borderColor: Colors.grey,
fillColor: Colors.blueAccent.withOpacity(0.15),
selectedBorderColor: Colors.blueAccent,
selectedColor: Colors.blueAccent,
color: Colors.grey,
constraints: BoxConstraints(
minHeight: 30,
minWidth: (screenWidth < 400 ? 28 : 36),
),
isSelected: [isChartView, !isChartView],
onPressed: (index) {
controller.projectIsChartView.value = index == 0;
},
children: const [
Icon(Icons.bar_chart_rounded, size: 15),
Icon(Icons.table_chart, size: 15),
],
),
],
),
const SizedBox(height: 6),
Row(
children: [
_buildRangeButton("7D", selectedRange),
_buildRangeButton("15D", selectedRange),
_buildRangeButton("30D", selectedRange),
_buildRangeButton("3M", selectedRange),
_buildRangeButton("6M", selectedRange),
],
),
],
);
}
Widget _buildRangeButton(String label, String selectedRange) {
return Padding(
padding: const EdgeInsets.only(right: 4.0),
child: ChoiceChip(
label: Text(label, style: const TextStyle(fontSize: 12)),
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
selected: selectedRange == label,
onSelected: (_) => controller.updateProjectRange(label),
selectedColor: Colors.blueAccent.withOpacity(0.15),
backgroundColor: Colors.grey.shade200,
labelStyle: TextStyle(
color: selectedRange == label ? Colors.blueAccent : Colors.black87,
fontWeight:
selectedRange == label ? FontWeight.w600 : FontWeight.normal,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
side: BorderSide(
color: selectedRange == label
? Colors.blueAccent
: Colors.grey.shade300,
),
),
),
);
}
Widget _buildChart(double height) {
final nonZeroData =
data.where((d) => d.planned != 0 || d.completed != 0).toList();
if (nonZeroData.isEmpty) {
return _buildNoDataContainer(height);
}
return Container(
height: height > 280 ? 280 : height,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.blueGrey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: SfCartesianChart(
tooltipBehavior: TooltipBehavior(enable: true),
legend: Legend(isVisible: true, position: LegendPosition.bottom),
// Use CategoryAxis so only nonZeroData dates show up
primaryXAxis: CategoryAxis(
majorGridLines: const MajorGridLines(width: 0),
axisLine: const AxisLine(width: 0),
labelRotation: 0,
),
primaryYAxis: NumericAxis(
labelFormat: '{value}',
axisLine: const AxisLine(width: 0),
majorTickLines: const MajorTickLines(size: 0),
),
series: <CartesianSeries>[
ColumnSeries<ChartTaskData, String>(
name: 'Planned',
dataSource: nonZeroData,
xValueMapper: (d, _) => DateFormat('MMM d').format(d.date),
yValueMapper: (d, _) => d.planned,
color: _getTaskColor('Planned'),
dataLabelSettings: DataLabelSettings(
isVisible: true,
builder: (data, point, series, pointIndex, seriesIndex) {
final value = seriesIndex == 0
? (data as ChartTaskData).planned
: (data as ChartTaskData).completed;
return Text(
_commaFormatter.format(value),
style: const TextStyle(fontSize: 11),
);
},
),
),
ColumnSeries<ChartTaskData, String>(
name: 'Completed',
dataSource: nonZeroData,
xValueMapper: (d, _) => DateFormat('MMM d').format(d.date),
yValueMapper: (d, _) => d.completed,
color: _getTaskColor('Completed'),
dataLabelSettings: DataLabelSettings(
isVisible: true,
builder: (data, point, series, pointIndex, seriesIndex) {
final value = seriesIndex == 0
? (data as ChartTaskData).planned
: (data as ChartTaskData).completed;
return Text(
_commaFormatter.format(value),
style: const TextStyle(fontSize: 11),
);
},
),
),
],
),
);
}
Widget _buildTable(double maxHeight, double screenWidth) {
final containerHeight = maxHeight > 300 ? 300.0 : maxHeight;
final nonZeroData =
data.where((d) => d.planned != 0 || d.completed != 0).toList();
if (nonZeroData.isEmpty) {
return _buildNoDataContainer(containerHeight);
}
return Container(
height: containerHeight,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
color: Colors.grey.shade50,
),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: constraints.maxWidth),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: DataTable(
columnSpacing: screenWidth < 600 ? 16 : 36,
headingRowHeight: 44,
headingRowColor: MaterialStateProperty.all(
Colors.blueAccent.withOpacity(0.08)),
headingTextStyle: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.black87),
columns: const [
DataColumn(label: Text('Date')),
DataColumn(label: Text('Planned')),
DataColumn(label: Text('Completed')),
],
rows: nonZeroData.map((task) {
return DataRow(
cells: [
DataCell(Text(DateFormat('d MMM').format(task.date))),
DataCell(Text(
'${task.planned}',
style: TextStyle(color: _getTaskColor('Planned')),
)),
DataCell(Text(
'${task.completed}',
style: TextStyle(color: _getTaskColor('Completed')),
)),
],
);
}).toList(),
),
),
),
);
},
),
);
}
Widget _buildNoDataContainer(double height) {
return Container(
height: height > 280 ? 280 : height,
decoration: BoxDecoration(
color: Colors.blueGrey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'No project progress data for the selected range.',
style: TextStyle(fontSize: 14, color: Colors.grey),
textAlign: TextAlign.center,
),
),
);
}
Widget _buildNoDataMessage() {
return SizedBox(
height: 180,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.info_outline, color: Colors.grey.shade400, size: 54),
const SizedBox(height: 10),
MyText.bodyMedium(
'No project progress data available for the selected range.',
textAlign: TextAlign.center,
color: Colors.grey.shade500,
),
],
),
),
);
}
}

View File

@ -43,7 +43,7 @@ class _ImageViewerDialogState extends State<ImageViewerDialog> {
color: Colors.white, color: Colors.white,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.black.withValues(alpha: 0.1),
blurRadius: 12, blurRadius: 12,
offset: const Offset(0, 4), offset: const Offset(0, 4),
), ),
@ -92,8 +92,7 @@ class _ImageViewerDialogState extends State<ImageViewerDialog> {
}, },
errorBuilder: (context, error, stackTrace) => errorBuilder: (context, error, stackTrace) =>
const Center( const Center(
child: Icon(Icons.broken_image, child: Icon(Icons.broken_image, size: 48, color: Colors.grey),
size: 48, color: Colors.grey),
), ),
), ),
); );

View File

@ -84,7 +84,7 @@ class _ContentView extends StatelessWidget {
MyText.bodySmall( MyText.bodySmall(
message, message,
textAlign: TextAlign.center, textAlign: TextAlign.center,
color: theme.colorScheme.onSurface.withOpacity(0.7), color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Row( Row(

View File

@ -3,7 +3,7 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart'; 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/attendance/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'; import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart';

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:marco/controller/permission_controller.dart'; import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/controller/attendance/attendance_screen_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:marco/controller/dashboard/daily_task_controller.dart'; import 'package:marco/controller/task_planning/daily_task_controller.dart';
import 'package:marco/controller/permission_controller.dart'; import 'package:marco/controller/permission_controller.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';

View File

@ -0,0 +1,46 @@
// dashboard_projects_model.dart
class DashboardProjects {
final bool success;
final String message;
final DashboardProjectsData? data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
DashboardProjects({
required this.success,
required this.message,
required this.data,
this.errors,
required this.statusCode,
required this.timestamp,
});
factory DashboardProjects.fromJson(Map<String, dynamic> json) {
return DashboardProjects(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: json['data'] != null ? DashboardProjectsData.fromJson(json['data']) : null,
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: DateTime.parse(json['timestamp']),
);
}
}
class DashboardProjectsData {
final int totalProjects;
final int ongoingProjects;
DashboardProjectsData({
required this.totalProjects,
required this.ongoingProjects,
});
factory DashboardProjectsData.fromJson(Map<String, dynamic> json) {
return DashboardProjectsData(
totalProjects: json['totalProjects'] ?? 0,
ongoingProjects: json['ongoingProjects'] ?? 0,
);
}
}

View File

@ -0,0 +1,46 @@
// dashboard_tasks_model.dart
class DashboardTasks {
final bool success;
final String message;
final DashboardTasksData? data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
DashboardTasks({
required this.success,
required this.message,
required this.data,
this.errors,
required this.statusCode,
required this.timestamp,
});
factory DashboardTasks.fromJson(Map<String, dynamic> json) {
return DashboardTasks(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: json['data'] != null ? DashboardTasksData.fromJson(json['data']) : null,
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: DateTime.parse(json['timestamp']),
);
}
}
class DashboardTasksData {
final int totalTasks;
final int completedTasks;
DashboardTasksData({
required this.totalTasks,
required this.completedTasks,
});
factory DashboardTasksData.fromJson(Map<String, dynamic> json) {
return DashboardTasksData(
totalTasks: json['totalTasks'] ?? 0,
completedTasks: json['completedTasks'] ?? 0,
);
}
}

View File

@ -0,0 +1,46 @@
// dashboard_teams_model.dart
class DashboardTeams {
final bool success;
final String message;
final DashboardTeamsData? data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
DashboardTeams({
required this.success,
required this.message,
required this.data,
this.errors,
required this.statusCode,
required this.timestamp,
});
factory DashboardTeams.fromJson(Map<String, dynamic> json) {
return DashboardTeams(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: json['data'] != null ? DashboardTeamsData.fromJson(json['data']) : null,
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: DateTime.parse(json['timestamp']),
);
}
}
class DashboardTeamsData {
final int totalEmployees;
final int inToday;
DashboardTeamsData({
required this.totalEmployees,
required this.inToday,
});
factory DashboardTeamsData.fromJson(Map<String, dynamic> json) {
return DashboardTeamsData(
totalEmployees: json['totalEmployees'] ?? 0,
inToday: json['inToday'] ?? 0,
);
}
}

View File

@ -0,0 +1,104 @@
import 'package:intl/intl.dart';
class ProjectResponse {
final bool success;
final String message;
final List<ProjectData> data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
ProjectResponse({
required this.success,
required this.message,
required this.data,
this.errors,
required this.statusCode,
required this.timestamp,
});
factory ProjectResponse.fromJson(Map<String, dynamic> json) {
return ProjectResponse(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: (json['data'] as List<dynamic>?)
?.map((e) => ProjectData.fromJson(e))
.toList() ??
[],
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: DateTime.parse(json['timestamp']),
);
}
Map<String, dynamic> toJson() {
return {
'success': success,
'message': message,
'data': data.map((e) => e.toJson()).toList(),
'errors': errors,
'statusCode': statusCode,
'timestamp': timestamp.toIso8601String(),
};
}
}
class ProjectData {
final String projectId;
final String projectName;
final int plannedTask;
final int completedTask;
final DateTime date;
ProjectData({
required this.projectId,
required this.projectName,
required this.plannedTask,
required this.completedTask,
required this.date,
});
factory ProjectData.fromJson(Map<String, dynamic> json) {
return ProjectData(
projectId: json['projectId'] ?? '',
projectName: json['projectName'] ?? '',
plannedTask: json['plannedTask'] ?? 0,
completedTask: json['completedTask'] ?? 0,
date: DateTime.parse(json['date']),
);
}
Map<String, dynamic> toJson() {
return {
'projectId': projectId,
'projectName': projectName,
'plannedTask': plannedTask,
'completedTask': completedTask,
'date': date.toIso8601String(),
};
}
}
/// Chart-friendly model
class ChartTaskData {
final DateTime date; // actual date for chart
final String dateLabel; // optional: for display
final int planned;
final int completed;
ChartTaskData({
required this.date,
required this.dateLabel,
required this.planned,
required this.completed,
});
factory ChartTaskData.fromProjectData(ProjectData data) {
return ChartTaskData(
date: data.date,
dateLabel: DateFormat('dd-MM').format(data.date),
planned: data.plannedTask,
completed: data.completedTask,
);
}
}

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/dashboard/add_employee_controller.dart'; import 'package:marco/controller/employee/add_employee_controller.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart'; import 'package:marco/controller/employee/employees_screen_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.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';

View File

@ -4,7 +4,7 @@ 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/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart'; import 'package:marco/controller/employee/employees_screen_controller.dart';
import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart';
class EmployeeDetailBottomSheet extends StatefulWidget { class EmployeeDetailBottomSheet extends StatefulWidget {

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/controller/permission_controller.dart'; import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart'; import 'package:marco/controller/employee/employees_screen_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
class EmployeesScreenFilterSheet extends StatelessWidget { class EmployeesScreenFilterSheet extends StatelessWidget {

View File

@ -9,7 +9,7 @@ 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/Attendence/attendance_screen.dart'; import 'package:marco/view/Attendence/attendance_screen.dart';
import 'package:marco/view/taskPlanning/daily_task_planning.dart'; import 'package:marco/view/taskPlanning/daily_task_planning.dart';
import 'package:marco/view/taskPlanning/daily_progress.dart'; import 'package:marco/view/taskPlanning/daily_progress.dart';
import 'package:marco/view/employees/employees_screen.dart'; import 'package:marco/view/employees/employees_screen.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/controller/attendance/attendance_screen_controller.dart';
import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/helpers/utils/date_time_utils.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_card.dart';

View File

@ -6,13 +6,13 @@ 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/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/controller/attendance/attendance_screen_controller.dart';
import 'package:marco/controller/permission_controller.dart'; import 'package:marco/controller/permission_controller.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'; import 'package:marco/controller/project_controller.dart';
import 'package:marco/view/dashboard/Attendence/regularization_requests_tab.dart'; import 'package:marco/view/Attendence/regularization_requests_tab.dart';
import 'package:marco/view/dashboard/Attendence/attendance_logs_tab.dart'; import 'package:marco/view/Attendence/attendance_logs_tab.dart';
import 'package:marco/view/dashboard/Attendence/todays_attendance_tab.dart'; import 'package:marco/view/Attendence/todays_attendance_tab.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
class AttendanceScreen extends StatefulWidget { class AttendanceScreen extends StatefulWidget {
const AttendanceScreen({super.key}); const AttendanceScreen({super.key});

View File

@ -12,7 +12,7 @@ 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/view/layouts/layout.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/controller/attendance/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';
import 'package:marco/model/attendance/attendence_action_button.dart'; import 'package:marco/model/attendance/attendence_action_button.dart';

View File

@ -2,7 +2,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/controller/attendance/attendance_screen_controller.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.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';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/controller/attendance/attendance_screen_controller.dart';
import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/helpers/utils/date_time_utils.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_card.dart';

View File

@ -1,306 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
class AttendanceDashboardChart extends StatelessWidget {
final DashboardController controller = Get.find<DashboardController>();
AttendanceDashboardChart({super.key});
List<Map<String, dynamic>> get filteredData {
final now = DateTime.now();
final daysBack = controller.rangeDays;
return controller.roleWiseData.where((entry) {
final date = DateTime.parse(entry['date']);
return date.isAfter(now.subtract(Duration(days: daysBack))) &&
!date.isAfter(now);
}).toList();
}
List<DateTime> get filteredDateTimes {
final uniqueDates = filteredData
.map((e) => DateTime.parse(e['date'] as String))
.toSet()
.toList()
..sort();
return uniqueDates;
}
List<String> get filteredDates =>
filteredDateTimes.map((d) => DateFormat('d MMMM').format(d)).toList();
List<String> get filteredRoles =>
filteredData.map((e) => e['role'] as String).toSet().toList();
List<String> get rolesWithData => filteredRoles.where((role) {
return filteredData.any(
(entry) => entry['role'] == role && (entry['present'] ?? 0) > 0);
}).toList();
final Map<String, Color> _roleColorMap = {};
final List<Color> flatColors = [
const Color(0xFFE57373),
const Color(0xFF64B5F6),
const Color(0xFF81C784),
const Color(0xFFFFB74D),
const Color(0xFFBA68C8),
const Color(0xFFFF8A65),
const Color(0xFF4DB6AC),
const Color(0xFFA1887F),
const Color(0xFFDCE775),
const Color(0xFF9575CD),
];
Color _getRoleColor(String role) {
if (_roleColorMap.containsKey(role)) return _roleColorMap[role]!;
final index = _roleColorMap.length % flatColors.length;
final color = flatColors[index];
_roleColorMap[role] = color;
return color;
}
@override
Widget build(BuildContext context) {
return Obx(() {
final isChartView = controller.isChartView.value;
final selectedRange = controller.selectedRange.value;
final isLoading = controller.isLoading.value;
return Container(
// flat white background
color: Colors.white,
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(selectedRange, isChartView),
const SizedBox(height: 12),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isLoading
? SkeletonLoaders.buildLoadingSkeleton()
: filteredData.isEmpty
? _buildNoDataMessage()
: isChartView
? _buildChart()
: _buildTable(),
),
],
),
);
});
}
Widget _buildHeader(String selectedRange, bool isChartView) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium('Attendance Overview', fontWeight: 600),
MyText.bodySmall('Role-wise present count', color: Colors.grey),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: 'Select Range',
onSelected: (value) => controller.selectedRange.value = value,
itemBuilder: (context) => const [
PopupMenuItem(value: '7D', child: Text('Last 7 Days')),
PopupMenuItem(value: '15D', child: Text('Last 15 Days')),
PopupMenuItem(value: '30D', child: Text('Last 30 Days')),
],
child: Row(
children: [
const Icon(Icons.calendar_today_outlined, size: 18),
const SizedBox(width: 4),
MyText.labelSmall(selectedRange),
],
),
),
const SizedBox(width: 8),
IconButton(
icon: Icon(
Icons.bar_chart_rounded,
size: 20,
color: isChartView ? Colors.blueAccent : Colors.grey,
),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
constraints: const BoxConstraints(),
padding: EdgeInsets.zero,
onPressed: () => controller.isChartView.value = true,
tooltip: 'Chart View',
),
IconButton(
icon: Icon(
Icons.table_chart,
size: 20,
color: !isChartView ? Colors.blueAccent : Colors.grey,
),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
constraints: const BoxConstraints(),
padding: EdgeInsets.zero,
onPressed: () => controller.isChartView.value = false,
tooltip: 'Table View',
),
],
),
),
],
),
);
}
Widget _buildChart() {
final formattedDateMap = {
for (var e in filteredData)
'${e['role']}_${DateFormat('d MMMM').format(DateTime.parse(e['date']))}':
e['present']
};
return SizedBox(
height: 360,
child: SfCartesianChart(
tooltipBehavior: TooltipBehavior(
enable: true,
shared: true,
activationMode: ActivationMode.singleTap,
tooltipPosition: TooltipPosition.pointer,
),
legend: const Legend(
isVisible: true,
position: LegendPosition.bottom,
overflowMode: LegendItemOverflowMode.wrap,
),
primaryXAxis: CategoryAxis(
labelRotation: 45,
majorGridLines: const MajorGridLines(width: 0),
),
primaryYAxis: NumericAxis(
minimum: 0,
interval: 1,
majorGridLines: const MajorGridLines(width: 0),
),
series: rolesWithData.map((role) {
final data = filteredDates.map((formattedDate) {
final key = '${role}_$formattedDate';
return {
'date': formattedDate,
'present': formattedDateMap[key] ?? 0
};
}).toList();
return StackedColumnSeries<Map<String, dynamic>, String>(
dataSource: data,
xValueMapper: (d, _) => d['date'],
yValueMapper: (d, _) => d['present'],
name: role,
legendIconType: LegendIconType.circle,
dataLabelSettings: const DataLabelSettings(isVisible: true),
dataLabelMapper: (d, _) =>
d['present'] == 0 ? '' : d['present'].toString(),
color: _getRoleColor(role),
);
}).toList(),
),
);
}
Widget _buildNoDataMessage() {
return SizedBox(
height: 200,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.info_outline, color: Colors.grey.shade500, size: 48),
const SizedBox(height: 12),
MyText.bodyMedium(
'No attendance data available for the selected range or project.',
textAlign: TextAlign.center,
color: Colors.grey.shade600,
),
],
),
),
);
}
Widget _buildTable() {
final formattedDateMap = {
for (var e in filteredData)
'${e['role']}_${DateFormat('d MMMM').format(DateTime.parse(e['date']))}':
e['present']
};
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 28,
headingRowHeight: 42,
headingRowColor:
WidgetStateProperty.all(Colors.blueAccent.withOpacity(0.1)),
headingTextStyle: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.black87),
columns: [
DataColumn(label: MyText.labelSmall('Role', fontWeight: 600)),
...filteredDates.map((date) => DataColumn(
label: MyText.labelSmall(date, fontWeight: 600),
)),
],
rows: filteredRoles.map((role) {
return DataRow(
cells: [
DataCell(Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: _rolePill(role),
)),
...filteredDates.map((date) {
final key = '${role}_$date';
return DataCell(Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: MyText.labelSmall('${formattedDateMap[key] ?? 0}'),
));
}),
],
);
}).toList(),
),
),
);
}
Widget _rolePill(String role) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: _getRoleColor(role).withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
),
child: MyText.labelSmall(role, fontWeight: 500),
);
}
}

View File

@ -9,10 +9,12 @@ 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/dashboard/dashboard_chart.dart'; import 'package:marco/helpers/widgets/dashbaord/attendance_overview_chart.dart';
import 'package:marco/helpers/widgets/dashbaord/project_progress_chart.dart';
import 'package:marco/view/layouts/layout.dart'; import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart'; import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/helpers/widgets/dashbaord/dashboard_overview_widgets.dart';
// import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; // Commented out // import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; // Commented out
@ -76,82 +78,53 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
MySpacing.height(10), MySpacing.height(10),
*/ */
_buildDashboardStats(context), _buildDashboardStats(context),
MySpacing.height(24),
SizedBox(
width: double.infinity,
child: DashboardOverviewWidgets.teamsOverview(),
),
MySpacing.height(24),
SizedBox(
width: double.infinity,
child: DashboardOverviewWidgets.tasksOverview(),
),
MySpacing.height(24), MySpacing.height(24),
_buildAttendanceChartSection(), _buildAttendanceChartSection(),
MySpacing.height(24),
_buildProjectProgressChartSection(),
], ],
), ),
), ),
); );
} }
/// Dashboard Statistics Section with ProjectController, Obx reactivity for menus /// Project Progress Chart Section
Widget _buildDashboardStats(BuildContext context) { Widget _buildProjectProgressChartSection() {
return Obx(() { return Obx(() {
if (menuController.isLoading.value) { if (dashboardController.isProjectLoading.value) {
return _buildLoadingSkeleton(context);
}
if (menuController.hasError.value) {
return Padding( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(8.0),
child: SkeletonLoaders.chartSkeletonLoader(),
);
}
if (dashboardController.projectChartData.isEmpty) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center( child: Center(
child: MyText.bodySmall( child: Text("No project progress data available."),
"Failed to load menus. Please try again later.",
color: Colors.red,
),
), ),
); );
} }
final stats = [ return ClipRRect(
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, borderRadius: BorderRadius.circular(12),
DashboardScreen.attendanceRoute), child: SizedBox(
_StatItem(LucideIcons.users, "Employees", contentTheme.warning, height: 400,
DashboardScreen.employeesRoute), child: ProjectProgressChart(
_StatItem(LucideIcons.logs, "Daily Task Planning", contentTheme.info, data: dashboardController.projectChartData,
DashboardScreen.dailyTasksRoute),
_StatItem(LucideIcons.list_todo, "Daily Progress Report",
contentTheme.info, DashboardScreen.dailyTasksProgressRoute),
_StatItem(LucideIcons.folder, "Directory", contentTheme.info,
DashboardScreen.directoryMainPageRoute),
_StatItem(LucideIcons.badge_dollar_sign, "Expense", contentTheme.info,
DashboardScreen.expenseMainPageRoute),
];
final projectController = Get.find<ProjectController>();
final isProjectSelected = projectController.selectedProject != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isProjectSelected) _buildNoProjectMessage(),
LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
final crossAxisCount = (maxWidth / 100).floor().clamp(2, 4);
final cardWidth =
(maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount;
return Wrap(
spacing: 10,
runSpacing: 10,
children: stats
.where((stat) {
final isAllowed =
menuController.isMenuAllowed(stat.title);
// 👇 Log what is being checked
debugPrint(
"[Dashboard Menu] Checking menu: ${stat.title} -> Allowed: $isAllowed");
return isAllowed;
})
.map((stat) =>
_buildStatCard(stat, cardWidth, isProjectSelected))
.toList(),
);
},
), ),
], ),
); );
}); });
} }
@ -163,7 +136,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
// Show Skeleton Loader Instead of CircularProgressIndicator // Show Skeleton Loader Instead of CircularProgressIndicator
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: SkeletonLoaders.chartSkeletonLoader(), // <-- using the skeleton we built child: SkeletonLoaders
.chartSkeletonLoader(), // <-- using the skeleton we built
); );
} }
@ -184,7 +158,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
ignoring: !isProjectSelected, ignoring: !isProjectSelected,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: AttendanceDashboardChart(), child: SizedBox(
height: 400,
child: AttendanceDashboardChart(),
),
), ),
), ),
); );
@ -259,30 +236,95 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
} }
/// Stat Card /// Stat Card
Widget _buildStatCard(_StatItem statItem, double width, bool isEnabled) { /// Dashboard Statistics Section with Compact Cards
Widget _buildDashboardStats(BuildContext context) {
return Obx(() {
if (menuController.isLoading.value) {
return _buildLoadingSkeleton(context);
}
if (menuController.hasError.value) {
return Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: MyText.bodySmall(
"Failed to load menus. Please try again later.",
color: Colors.red,
),
),
);
}
final stats = [
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success,
DashboardScreen.attendanceRoute),
_StatItem(LucideIcons.users, "Employees", contentTheme.warning,
DashboardScreen.employeesRoute),
_StatItem(LucideIcons.logs, "Daily Task Planning", contentTheme.info,
DashboardScreen.dailyTasksRoute),
_StatItem(LucideIcons.list_todo, "Daily Progress Report",
contentTheme.info, DashboardScreen.dailyTasksProgressRoute),
_StatItem(LucideIcons.folder, "Directory", contentTheme.info,
DashboardScreen.directoryMainPageRoute),
_StatItem(LucideIcons.badge_dollar_sign, "Expense", contentTheme.info,
DashboardScreen.expenseMainPageRoute),
];
final projectController = Get.find<ProjectController>();
final isProjectSelected = projectController.selectedProject != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isProjectSelected) _buildNoProjectMessage(),
Wrap(
spacing: 6, // horizontal spacing
runSpacing: 6, // vertical spacing
children: stats
.where((stat) => menuController.isMenuAllowed(stat.title))
.map((stat) => _buildStatCard(stat, isProjectSelected))
.toList(),
),
],
);
});
}
/// Stat Card (Compact with wrapping text)
Widget _buildStatCard(_StatItem statItem, bool isEnabled) {
const double cardWidth = 80;
const double cardHeight = 70;
return Opacity( return Opacity(
opacity: isEnabled ? 1.0 : 0.4, opacity: isEnabled ? 1.0 : 0.4,
child: IgnorePointer( child: IgnorePointer(
ignoring: !isEnabled, ignoring: !isEnabled,
child: InkWell( child: InkWell(
onTap: () => _handleStatCardTap(statItem, isEnabled), onTap: () => _handleStatCardTap(statItem, isEnabled),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(8),
child: MyCard.bordered( child: MyCard.bordered(
width: width, width: cardWidth,
height: 100, height: cardHeight,
paddingAll: 5, paddingAll: 4,
borderRadiusAll: 10, borderRadiusAll: 8,
border: Border.all(color: Colors.grey.withOpacity(0.15)), border: Border.all(color: Colors.grey.withOpacity(0.15)),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
_buildStatCardIcon(statItem), _buildStatCardIconCompact(statItem),
MySpacing.height(8), MySpacing.height(4),
MyText.labelSmall( Expanded(
statItem.title, child: Center(
maxLines: 2, child: Text(
overflow: TextOverflow.visible, statItem.title,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10,
overflow: TextOverflow.visible,
),
maxLines: 2,
softWrap: true,
),
),
), ),
], ],
), ),
@ -292,6 +334,19 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
); );
} }
/// Compact Icon
Widget _buildStatCardIconCompact(_StatItem statItem) {
return MyContainer.rounded(
paddingAll: 6,
color: statItem.color.withOpacity(0.1),
child: Icon(
statItem.icon,
size: 14,
color: statItem.color,
),
);
}
/// Handle Tap /// Handle Tap
void _handleStatCardTap(_StatItem statItem, bool isEnabled) { void _handleStatCardTap(_StatItem statItem, bool isEnabled) {
if (!isEnabled) { if (!isEnabled) {
@ -308,15 +363,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
Get.toNamed(statItem.route); Get.toNamed(statItem.route);
} }
} }
/// Stat Icon
Widget _buildStatCardIcon(_StatItem statItem) {
return MyContainer.rounded(
paddingAll: 10,
color: statItem.color.withOpacity(0.1),
child: Icon(statItem.icon, size: 18, color: statItem.color),
);
}
} }
class _StatItem { class _StatItem {

View File

@ -1,244 +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/controller/dashboard/employees_screen_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';
class EmployeeScreen extends StatefulWidget {
const EmployeeScreen({super.key});
@override
State<EmployeeScreen> createState() => _EmployeeScreenState();
}
class _EmployeeScreenState extends State<EmployeeScreen> with UIMixin {
final EmployeesScreenController employeesScreenController =
Get.put(EmployeesScreenController());
final PermissionController permissionController =
Get.put(PermissionController());
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
employeesScreenController.selectedProjectId = null;
await employeesScreenController.fetchAllEmployees();
employeesScreenController.update();
});
}
@override
Widget build(BuildContext context) {
return Layout(
child: Obx(() {
return LoadingComponent(
isLoading: employeesScreenController.isLoading.value,
loadingText: 'Loading Employees...',
child: GetBuilder<EmployeesScreenController>(
init: employeesScreenController,
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: MySpacing.x(flexSpacing),
child: MyText.titleMedium("Employee",
fontSize: 18, fontWeight: 600),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing),
child: MyBreadcrumb(
children: [
MyBreadcrumbItem(name: 'Dashboard'),
MyBreadcrumbItem(name: 'Employee', active: true),
],
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
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.isEmpty) {
employeesScreenController.selectedProjectId =
null;
await employeesScreenController
.fetchAllEmployees();
} else {
employeesScreenController.selectedProjectId =
value;
await employeesScreenController
.fetchEmployeesByProject(value);
}
employeesScreenController.update();
},
itemBuilder: (BuildContext context) {
List<PopupMenuItem<String>> items = [
PopupMenuItem<String>(
value: '',
child: MyText.bodySmall('All Employees',
fontWeight: 600),
),
];
items.addAll(
employeesScreenController.projects
.map<PopupMenuItem<String>>((project) {
return PopupMenuItem<String>(
value: project.id,
child: MyText.bodySmall(project.name),
);
}).toList(),
);
return items;
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
employeesScreenController
.selectedProjectId ==
null
? 'All Employees'
: employeesScreenController.projects
.firstWhere((project) =>
project.id ==
employeesScreenController
.selectedProjectId)
.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w600),
),
),
const SizedBox(width: 4),
const Icon(
Icons.arrow_drop_down,
size: 20,
color: Colors.black,
),
],
),
),
),
),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
Get.toNamed('/employees/addEmployee');
},
child: Text('Add New Employee'),
),
],
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing / 2),
child: MyFlex(
children: [
MyFlexItem(sizes: 'lg-6 ', child: employeeListTab()),
],
),
),
],
);
},
),
);
}),
);
}
Widget employeeListTab() {
if (employeesScreenController.employees.isEmpty) {
return Center(
child: MyText.bodySmall("No Employees Assigned to This Project",
fontWeight: 600),
);
}
final columns = <DataColumn>[
DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Contact', color: contentTheme.primary)),
];
final rows =
employeesScreenController.employees.asMap().entries.map((entry) {
var employee = entry.value;
return DataRow(
cells: [
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(employee.name, fontWeight: 600),
const SizedBox(height: 2),
MyText.bodySmall(employee.jobRole, color: Colors.grey),
],
),
),
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(employee.email, fontWeight: 600),
const SizedBox(height: 2),
MyText.bodySmall(employee.phoneNumber, color: Colors.grey),
],
),
),
],
);
}).toList();
return MyRefreshableContent(
onRefresh: () async {
if (employeesScreenController.selectedProjectId == null) {
await employeesScreenController.fetchAllEmployees();
} else {
await employeesScreenController.fetchEmployeesByProject(
employeesScreenController.selectedProjectId!,
);
}
},
child: MyPaginatedTable(
columns: columns,
rows: rows,
),
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart'; import 'package:marco/controller/employee/employees_screen_controller.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
@ -14,8 +14,12 @@ import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
class EmployeeDetailPage extends StatefulWidget { class EmployeeDetailPage extends StatefulWidget {
final String employeeId; final String employeeId;
final bool fromProfile;
const EmployeeDetailPage({super.key, required this.employeeId}); const EmployeeDetailPage({
super.key,
required this.employeeId,
this.fromProfile = false,
});
@override @override
State<EmployeeDetailPage> createState() => _EmployeeDetailPageState(); State<EmployeeDetailPage> createState() => _EmployeeDetailPageState();
@ -201,7 +205,13 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
IconButton( IconButton(
icon: const Icon(Icons.arrow_back_ios_new, icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20), color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard/employees'), onPressed: () {
if (widget.fromProfile) {
Get.back();
} else {
Get.offNamed('/dashboard/employees');
}
},
), ),
MySpacing.width(8), MySpacing.width(8),
Expanded( Expanded(

View File

@ -5,7 +5,7 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.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/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/employee/employees_screen_controller.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart';

View File

@ -15,7 +15,7 @@ import 'package:marco/helpers/services/app_logger.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/helpers/widgets/expense_detail_helpers.dart'; import 'package:marco/helpers/widgets/expense/expense_detail_helpers.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/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';

View File

@ -8,7 +8,7 @@ import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/model/expense/expense_list_model.dart'; import 'package:marco/model/expense/expense_list_model.dart';
import 'package:marco/model/expense/add_expense_bottom_sheet.dart'; import 'package:marco/model/expense/add_expense_bottom_sheet.dart';
import 'package:marco/view/expense/expense_filter_bottom_sheet.dart'; import 'package:marco/view/expense/expense_filter_bottom_sheet.dart';
import 'package:marco/helpers/widgets/expense_main_components.dart'; import 'package:marco/helpers/widgets/expense/expense_main_components.dart';
import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart';

View File

@ -1,325 +1,341 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.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/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:flutter_lucide/flutter_lucide.dart';
import 'package:marco/model/employees/employee_info.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:get/get.dart'; import 'package:marco/model/employees/employee_info.dart';
import 'package:marco/controller/auth/mpin_controller.dart'; import 'package:marco/controller/auth/mpin_controller.dart';
import 'package:marco/view/employees/employee_detail_screen.dart';
class UserProfileBar extends StatefulWidget { class UserProfileBar extends StatefulWidget {
final bool isCondensed; final bool isCondensed;
const UserProfileBar({Key? key, this.isCondensed = false}) : super(key: key);
const UserProfileBar({super.key, this.isCondensed = false});
@override @override
_UserProfileBarState createState() => _UserProfileBarState(); State<UserProfileBar> createState() => _UserProfileBarState();
} }
class _UserProfileBarState extends State<UserProfileBar> class _UserProfileBarState extends State<UserProfileBar>
with SingleTickerProviderStateMixin, UIMixin { with SingleTickerProviderStateMixin, UIMixin {
final ThemeCustomizer customizer = ThemeCustomizer.instance; late EmployeeInfo employeeInfo;
bool isCondensed = false; bool _isLoading = true;
EmployeeInfo? employeeInfo;
bool hasMpin = true; bool hasMpin = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadEmployeeInfo(); _initData();
_checkMpinStatus();
} }
void _loadEmployeeInfo() { Future<void> _initData() async {
setState(() { employeeInfo = LocalStorage.getEmployeeInfo()!;
employeeInfo = LocalStorage.getEmployeeInfo(); hasMpin = await LocalStorage.getIsMpin();
}); setState(() => _isLoading = false);
}
Future<void> _checkMpinStatus() async {
final bool mpinStatus = await LocalStorage.getIsMpin();
setState(() {
hasMpin = mpinStatus;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
isCondensed = widget.isCondensed; final bool isCondensed = widget.isCondensed;
return Padding(
return MyCard( padding: const EdgeInsets.only(left: 14),
borderRadiusAll: 16, child: ClipRRect(
paddingAll: 0, borderRadius: BorderRadius.circular(22),
shadow: MyShadow( child: BackdropFilter(
position: MyShadowPosition.centerRight, filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
elevation: 6, child: AnimatedContainer(
blurRadius: 12, duration: const Duration(milliseconds: 350),
), curve: Curves.easeInOut,
child: AnimatedContainer( width: isCondensed ? 84 : 260,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
leftBarTheme.background.withOpacity(0.97), Colors.white.withValues(alpha: 0.95),
leftBarTheme.background.withOpacity(0.88), Colors.white.withValues(alpha: 0.85),
], ],
begin: Alignment.topCenter, begin: Alignment.topLeft,
end: Alignment.bottomCenter, end: Alignment.bottomRight,
), ),
), borderRadius: BorderRadius.circular(22),
width: isCondensed ? 90 : 260, boxShadow: [
duration: const Duration(milliseconds: 300), BoxShadow(
curve: Curves.easeInOut, color: Colors.black.withValues(alpha: 0.06),
child: SafeArea( blurRadius: 18,
bottom: true, offset: const Offset(0, 8),
top: false, )
left: false,
right: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
userProfileSection(),
MySpacing.height(16),
supportAndSettingsMenu(),
const Spacer(),
logoutButton(),
],
),
),
),
);
}
/// User Profile Section - Avatar + Name
Widget userProfileSection() {
if (employeeInfo == null) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: CircularProgressIndicator()),
);
}
return Container(
width: double.infinity,
padding: MySpacing.fromLTRB(20, 50, 30, 50),
decoration: BoxDecoration(
color: leftBarTheme.activeItemBackground,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
Avatar(
firstName: employeeInfo?.firstName ?? 'F',
lastName: employeeInfo?.lastName ?? 'N',
size: 50,
),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium(
"${employeeInfo?.firstName ?? 'First'} ${employeeInfo?.lastName ?? 'Last'}",
fontWeight: 700,
color: leftBarTheme.activeItemColor,
),
], ],
border: Border.all(
color: Colors.grey.withValues(alpha: 0.25),
width: 1,
),
),
child: SafeArea(
bottom: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_isLoading
? const _LoadingSection()
: _userProfileSection(isCondensed),
MySpacing.height(12),
Divider(
indent: 18,
endIndent: 18,
thickness: 0.7,
color: Colors.grey.withValues(alpha: 0.25),
),
MySpacing.height(12),
_supportAndSettingsMenu(isCondensed),
const Spacer(),
Divider(
indent: 18,
endIndent: 18,
thickness: 0.35,
color: Colors.grey.withValues(alpha: 0.18),
),
_logoutButton(isCondensed),
],
),
), ),
), ),
),
),
);
}
Widget _userProfileSection(bool condensed) {
final padding = MySpacing.fromLTRB(
condensed ? 16 : 26,
condensed ? 20 : 28,
condensed ? 14 : 28,
condensed ? 10 : 18,
);
final avatarSize = condensed ? 48.0 : 64.0;
return Padding(
padding: padding,
child: Row(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Theme.of(context).primaryColor.withValues(alpha: 0.15),
blurRadius: 10,
spreadRadius: 1,
),
],
border: Border.all(
color: Theme.of(context).primaryColor,
width: 2,
),
),
child: Avatar(
firstName: employeeInfo.firstName,
lastName: employeeInfo.lastName,
size: avatarSize,
),
),
if (!condensed) ...[
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyLarge(
'${employeeInfo.firstName} ${employeeInfo.lastName}',
fontWeight: 700,
color: Colors.indigo,
),
MySpacing.height(4),
MyText.bodySmall(
"You're on track this month!",
color: Colors.black54,
),
],
),
),
],
], ],
), ),
); );
} }
/// Menu Section with Settings, Support & MPIN Widget _supportAndSettingsMenu(bool condensed) {
Widget supportAndSettingsMenu() { final spacingHeight = condensed ? 8.0 : 14.0;
return Padding( return Padding(
padding: MySpacing.xy(16, 16), padding: const EdgeInsets.symmetric(horizontal: 14),
child: Column( child: Column(
children: [ children: [
menuItem( _menuItemRow(
icon: LucideIcons.user,
label: 'My Profile',
onTap: _onProfileTap,
),
SizedBox(height: spacingHeight),
_menuItemRow(
icon: LucideIcons.settings, icon: LucideIcons.settings,
label: "Settings", label: 'Settings',
), ),
MySpacing.height(14), SizedBox(height: spacingHeight),
menuItem( _menuItemRow(
icon: LucideIcons.badge_help, icon: LucideIcons.badge_help,
label: "Support", label: 'Support',
), ),
MySpacing.height(14), SizedBox(height: spacingHeight),
menuItem( _menuItemRow(
icon: LucideIcons.lock, icon: LucideIcons.lock,
label: hasMpin ? "Change MPIN" : "Set MPIN", label: hasMpin ? 'Change MPIN' : 'Set MPIN',
iconColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent, iconColor: Colors.redAccent,
labelColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent, textColor: Colors.redAccent,
onTap: () { onTap: _onMpinTap,
final controller = Get.put(MPINController());
if (hasMpin) {
controller.setChangeMpinMode();
}
Navigator.pushNamed(context, "/auth/mpin-auth");
},
filled: true,
), ),
], ],
), ),
); );
} }
Widget menuItem({ Widget _menuItemRow({
required IconData icon, required IconData icon,
required String label, required String label,
Color? iconColor,
Color? labelColor,
VoidCallback? onTap, VoidCallback? onTap,
bool filled = false, Color? iconColor,
Color? textColor,
}) { }) {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25),
splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35),
child: Container( child: Container(
padding: MySpacing.xy(14, 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: filled color: Colors.white.withOpacity(0.9),
? leftBarTheme.activeItemBackground.withOpacity(0.15)
: Colors.transparent,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(color: Colors.grey.withOpacity(0.2), width: 1),
color: filled
? leftBarTheme.activeItemBackground.withOpacity(0.3)
: Colors.transparent,
width: 1,
),
), ),
child: Row( child: Row(
children: [ children: [
Icon(icon, size: 22, color: iconColor ?? leftBarTheme.onBackground), Icon(icon, size: 22, color: iconColor ?? Colors.black87),
MySpacing.width(14), const SizedBox(width: 16),
Expanded( Expanded(
child: MyText.bodyMedium( child: Text(
label, label,
color: labelColor ?? leftBarTheme.onBackground, style: TextStyle(
fontWeight: 600, fontSize: 15,
fontWeight: FontWeight.w600,
color: textColor ?? Colors.black87,
),
), ),
), ),
const Icon(Icons.chevron_right, size: 20, color: Colors.black54),
], ],
), ),
), ),
); );
} }
/// Logout Button void _onProfileTap() {
Widget logoutButton() { Get.to(() => EmployeeDetailPage(
return InkWell( employeeId: employeeInfo.id,
onTap: () async { fromProfile: true,
await _showLogoutConfirmation(); ));
}, }
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(16), void _onMpinTap() {
bottomRight: Radius.circular(16), final controller = Get.put(MPINController());
), if (hasMpin) controller.setChangeMpinMode();
hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25), Navigator.pushNamed(context, "/auth/mpin-auth");
splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35), }
child: Container(
padding: MySpacing.all(16), Widget _logoutButton(bool condensed) {
decoration: BoxDecoration( return Padding(
color: leftBarTheme.activeItemBackground, padding: MySpacing.all(14),
borderRadius: const BorderRadius.only( child: SizedBox(
bottomLeft: Radius.circular(16), width: double.infinity,
bottomRight: Radius.circular(16), child: ElevatedButton.icon(
onPressed: _showLogoutConfirmation,
icon: const Icon(LucideIcons.log_out, size: 22, color: Colors.white),
label: condensed
? const SizedBox.shrink()
: MyText.bodyMedium(
"Logout",
color: Colors.white,
fontWeight: 700,
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
foregroundColor: Colors.white,
shadowColor: Colors.red.shade200,
padding: EdgeInsets.symmetric(
vertical: condensed ? 14 : 18,
horizontal: condensed ? 14 : 22,
),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
), ),
), ),
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,
),
],
),
), ),
); );
} }
Future<void> _showLogoutConfirmation() async { Future<void> _showLogoutConfirmation() async {
bool? confirm = await showDialog<bool>( final bool? confirm = await showDialog<bool>(
context: context, context: context,
builder: (context) => _buildLogoutDialog(context), builder: _buildLogoutDialog,
); );
if (confirm == true) await LocalStorage.logout();
if (confirm == true) {
await LocalStorage.logout();
}
} }
Widget _buildLogoutDialog(BuildContext context) { Widget _buildLogoutDialog(BuildContext context) {
return Dialog( return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
elevation: 10,
backgroundColor: Colors.white,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28), padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 34),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(LucideIcons.log_out, size: 48, color: Colors.redAccent), Icon(LucideIcons.log_out, size: 56, color: Colors.red.shade700),
const SizedBox(height: 16), const SizedBox(height: 18),
Text( const Text(
"Logout Confirmation", "Logout Confirmation",
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 22,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onBackground, color: Colors.black87),
),
), ),
const SizedBox(height: 12), const SizedBox(height: 14),
Text( const Text(
"Are you sure you want to logout?\nYou will need to login again to continue.", "Are you sure you want to logout?\nYou will need to login again to continue.",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(fontSize: 16, color: Colors.black54),
fontSize: 14,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
), ),
const SizedBox(height: 24), const SizedBox(height: 30),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( Expanded(
child: TextButton( child: TextButton(
onPressed: () => Navigator.pop(context, false), onPressed: () => Navigator.pop(context, false),
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Colors.grey.shade700, foregroundColor: Colors.grey.shade700,
), padding: const EdgeInsets.symmetric(vertical: 12)),
child: const Text("Cancel"), child: const Text("Cancel"),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 18),
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent, backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(14)),
),
), ),
child: const Text("Logout"), child: const Text("Logout"),
), ),
@ -332,3 +348,15 @@ class _UserProfileBarState extends State<UserProfileBar>
); );
} }
} }
class _LoadingSection extends StatelessWidget {
const _LoadingSection();
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 44, horizontal: 8),
child: Center(child: CircularProgressIndicator()),
);
}
}

View File

@ -9,7 +9,7 @@ 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/controller/permission_controller.dart'; import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/dashboard/daily_task_controller.dart'; import 'package:marco/controller/task_planning/daily_task_controller.dart';
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter.dart'; import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';