feat: Add attendance log screen and related functionalities

- Implemented AttendenceLogScreen to display employee attendance logs.
- Created RegularizationRequestsTab to manage regularization requests.
- Added TodaysAttendanceTab for viewing today's attendance.
- Removed outdated dashboard chart implementation.
- Updated dashboard screen to integrate new attendance overview and project progress charts.
- Refactored employee detail and employee screens to use updated controllers.
- Organized expense-related imports and components for better structure.
- Adjusted daily progress report to use the correct controller.
This commit is contained in:
Vaibhav Surve 2025-08-29 15:53:19 +05:30
parent d62f8aa9ef
commit f5d4ab8415
34 changed files with 1223 additions and 444 deletions

View File

@ -2,15 +2,32 @@ import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/model/dashboard/project_progress_model.dart';
class DashboardController extends GetxController {
// Observables
final RxList<Map<String, dynamic>> roleWiseData = <Map<String, dynamic>>[].obs;
final RxBool isLoading = false.obs;
final RxString selectedRange = '15D'.obs;
final RxBool isChartView = true.obs;
// =========================
// Attendance overview
// =========================
final RxList<Map<String, dynamic>> roleWiseData =
<Map<String, dynamic>>[].obs;
// Inject the ProjectController
final RxString attendanceSelectedRange = '15D'.obs;
final RxBool attendanceIsChartView = true.obs;
final RxBool isAttendanceLoading = false.obs;
// =========================
// Project progress overview
// =========================
final RxList<ChartTaskData> projectChartData = <ChartTaskData>[].obs;
final RxString projectSelectedRange = '15D'.obs;
final RxBool projectIsChartView = true.obs;
final RxBool isProjectLoading = false.obs;
// Common ranges
final List<String> ranges = ['7D', '15D', '30D'];
// Inject ProjectController
final ProjectController projectController = Get.find<ProjectController>();
@override
@ -20,77 +37,113 @@ class DashboardController extends GetxController {
logSafe(
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
level: LogLevel.info,
);
if (projectController.selectedProjectId.value.isNotEmpty) {
fetchRoleWiseAttendance();
fetchProjectProgress();
}
// React to project change
ever<String>(projectController.selectedProjectId, (id) {
if (id.isNotEmpty) {
logSafe('Project changed to $id, fetching attendance', level: LogLevel.info, );
logSafe('Project changed to $id, refreshing dashboard',
level: LogLevel.info);
fetchRoleWiseAttendance();
fetchProjectProgress();
}
});
// React to range change
ever(selectedRange, (_) {
fetchRoleWiseAttendance();
});
// 👇 Separate listeners for ranges
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
ever(projectSelectedRange, (_) => fetchProjectProgress());
}
int get rangeDays => _getDaysFromRange(selectedRange.value);
// =========================
// Helpers
// =========================
int _getDaysFromRange(String range) {
switch (range) {
case '7D':
return 7;
case '15D':
return 15;
case '30D':
return 30;
case '7D':
case '3M':
return 90;
case '6M':
return 180;
default:
return 7;
}
}
void updateRange(String range) {
selectedRange.value = range;
logSafe('Selected range updated to $range', level: LogLevel.debug);
int getAttendanceDays() => _getDaysFromRange(attendanceSelectedRange.value);
int getProjectDays() => _getDaysFromRange(projectSelectedRange.value);
void updateAttendanceRange(String range) {
attendanceSelectedRange.value = range;
logSafe('Attendance range updated to $range', level: LogLevel.debug);
}
void toggleChartView(bool isChart) {
isChartView.value = isChart;
logSafe('Chart view toggled to: $isChart', level: LogLevel.debug);
void updateProjectRange(String range) {
projectSelectedRange.value = range;
logSafe('Project range updated to $range', level: LogLevel.debug);
}
void toggleAttendanceChartView(bool isChart) {
attendanceIsChartView.value = isChart;
logSafe('Attendance chart view toggled to: $isChart',
level: LogLevel.debug);
}
void toggleProjectChartView(bool isChart) {
projectIsChartView.value = isChart;
logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug);
}
/// Manual refresh
Future<void> refreshDashboard() async {
logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug);
await fetchRoleWiseAttendance();
await Future.wait([
fetchRoleWiseAttendance(),
fetchProjectProgress(),
]);
}
// =========================
// API Calls
// =========================
Future<void> fetchRoleWiseAttendance() async {
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) {
logSafe('Project ID is empty, skipping API call.', level: LogLevel.warning);
logSafe('Project ID is empty, skipping attendance API call.',
level: LogLevel.warning);
return;
}
try {
isLoading.value = true;
isAttendanceLoading.value = true;
final List<dynamic>? response =
await ApiService.getDashboardAttendanceOverview(projectId, rangeDays);
await ApiService.getDashboardAttendanceOverview(
projectId,
getAttendanceDays(),
);
if (response != null) {
roleWiseData.value =
response.map((e) => Map<String, dynamic>.from(e)).toList();
logSafe('Attendance overview fetched successfully.', level: LogLevel.info);
logSafe('Attendance overview fetched successfully.',
level: LogLevel.info);
} else {
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) {
roleWiseData.clear();
@ -101,7 +154,48 @@ class DashboardController extends GetxController {
stackTrace: st,
);
} finally {
isLoading.value = false;
isAttendanceLoading.value = false;
}
}
Future<void> fetchProjectProgress() async {
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) {
logSafe('Project ID is empty, skipping project progress API call.',
level: LogLevel.warning);
return;
}
try {
isProjectLoading.value = true;
// Instead of using DateTime.now(), we simply pass 'days' to the API
// Let the backend decide the correct date range
final response = await ApiService.getProjectProgress(
projectId: projectId,
days: getProjectDays(),
// Remove fromDate
);
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;
}
}
}

View File

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

View File

@ -4,6 +4,7 @@ class ApiEndpoints {
// Dashboard Module API Endpoints
static const String getDashboardAttendanceOverview = "/dashboard/attendance-overview";
static const String getDashboardProjectProgress = "/dashboard/progression";
// Attendance Module API Endpoints
static const String getProjects = "/project/list";

View File

@ -8,7 +8,7 @@ import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:marco/model/dashboard/project_progress_model.dart';
import 'package:marco/helpers/services/app_logger.dart';
class ApiService {
@ -625,6 +625,49 @@ class ApiService {
: 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
static Future<bool> deleteBucket(String id) async {

View File

@ -1,8 +1,8 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:marco/controller/dashboard/daily_task_controller.dart';
import 'package:marco/controller/attendance/attendance_screen_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/expense/expense_screen_controller.dart';
import 'package:marco/controller/expense/expense_detail_controller.dart';

View File

@ -0,0 +1,464 @@
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)
];
// Cache role colors as static to maintain immutability in stateless widget context
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 as a separate widget for clarity & reusability
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: [
// Title + toggle row
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium('Attendance Overview', fontWeight: 700),
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),
// Range buttons
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 message widget
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,
),
],
),
),
);
}
}
// Attendance Chart widget
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();
// Check if all present values are zero
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),
),
),
);
}
// Normal chart rendering
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};
}).toList();
return StackedColumnSeries<Map<String, dynamic>, String>(
dataSource: seriesData,
xValueMapper: (d, _) => d['date'],
yValueMapper: (d, _) => d['present'],
name: role,
color: getRoleColor(role),
dataLabelSettings: const DataLabelSettings(
isVisible: true,
textStyle: TextStyle(fontSize: 11),
),
);
}).toList(),
),
);
}
}
// Attendance Table widget
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();
// Check if all present values are zero
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),
),
),
);
}
// Normal table rendering
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('${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,344 @@
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 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),
primaryXAxis: DateTimeAxis(
dateFormat: DateFormat('MMM d'),
intervalType: DateTimeIntervalType.days,
majorGridLines: MajorGridLines(width: 0),
),
primaryYAxis: NumericAxis(
labelFormat: '{value}',
axisLine: AxisLine(width: 0),
majorTickLines: MajorTickLines(size: 0),
),
series: <CartesianSeries>[
ColumnSeries<ChartTaskData, DateTime>(
name: 'Planned',
dataSource: nonZeroData,
xValueMapper: (d, _) => d.date,
yValueMapper: (d, _) => d.planned,
color: _getTaskColor('Planned'),
dataLabelSettings: DataLabelSettings(
isVisible: true, textStyle: TextStyle(fontSize: 11)),
),
ColumnSeries<ChartTaskData, DateTime>(
name: 'Completed',
dataSource: nonZeroData,
xValueMapper: (d, _) => d.date,
yValueMapper: (d, _) => d.completed,
color: _getTaskColor('Completed'),
dataLabelSettings: DataLabelSettings(
isVisible: true, textStyle: 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

@ -3,7 +3,7 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:marco/controller/attendance/attendance_screen_controller.dart';
import 'package:marco/helpers/utils/attendance_actions.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.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/utils/permission_constants.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.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/helpers/utils/base_bottom_sheet.dart';
import 'package:marco/helpers/widgets/my_text.dart';

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/services.dart';
import 'package:get/get.dart';
import 'package:marco/controller/dashboard/add_employee_controller.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
import 'package:marco/controller/employee/add_employee_controller.dart';
import 'package:marco/controller/employee/employees_screen_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_spacing.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/avatar.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';
class EmployeeDetailBottomSheet extends StatefulWidget {

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.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';
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_500_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_progress.dart';
import 'package:marco/view/employees/employees_screen.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.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/widgets/avatar.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_spacing.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/model/attendance/attendence_filter_sheet.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/view/dashboard/Attendence/regularization_requests_tab.dart';
import 'package:marco/view/dashboard/Attendence/attendance_logs_tab.dart';
import 'package:marco/view/dashboard/Attendence/todays_attendance_tab.dart';
import 'package:marco/view/Attendence/regularization_requests_tab.dart';
import 'package:marco/view/Attendence/attendance_logs_tab.dart';
import 'package:marco/view/Attendence/todays_attendance_tab.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
class AttendanceScreen extends StatefulWidget {
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_text.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:intl/intl.dart';
import 'package:marco/model/attendance/attendence_action_button.dart';

View File

@ -2,7 +2,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.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/my_card.dart';
import 'package:marco/helpers/widgets/my_container.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.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/widgets/avatar.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,7 +9,8 @@ import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/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/controller/dynamicMenu/dynamic_menu_controller.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
@ -78,80 +79,41 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
_buildDashboardStats(context),
MySpacing.height(24),
_buildAttendanceChartSection(),
MySpacing.height(24),
_buildProjectProgressChartSection(),
],
),
),
);
}
/// Dashboard Statistics Section with ProjectController, Obx reactivity for menus
Widget _buildDashboardStats(BuildContext context) {
/// Project Progress Chart Section
Widget _buildProjectProgressChartSection() {
return Obx(() {
if (menuController.isLoading.value) {
return _buildLoadingSkeleton(context);
}
if (menuController.hasError.value) {
if (dashboardController.isProjectLoading.value) {
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: MyText.bodySmall(
"Failed to load menus. Please try again later.",
color: Colors.red,
),
child: Text("No project progress data available."),
),
);
}
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(),
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(),
);
},
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
height: 400,
child: ProjectProgressChart(
data: dashboardController.projectChartData,
),
],
),
);
});
}
@ -163,7 +125,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
// Show Skeleton Loader Instead of CircularProgressIndicator
return Padding(
padding: const EdgeInsets.all(8.0),
child: SkeletonLoaders.chartSkeletonLoader(), // <-- using the skeleton we built
child: SkeletonLoaders
.chartSkeletonLoader(), // <-- using the skeleton we built
);
}
@ -184,7 +147,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
ignoring: !isProjectSelected,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AttendanceDashboardChart(),
child: SizedBox(
height: 400,
child: AttendanceDashboardChart(),
),
),
),
);
@ -259,30 +225,95 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
}
/// 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(
opacity: isEnabled ? 1.0 : 0.4,
child: IgnorePointer(
ignoring: !isEnabled,
child: InkWell(
onTap: () => _handleStatCardTap(statItem, isEnabled),
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(8),
child: MyCard.bordered(
width: width,
height: 100,
paddingAll: 5,
borderRadiusAll: 10,
width: cardWidth,
height: cardHeight,
paddingAll: 4,
borderRadiusAll: 8,
border: Border.all(color: Colors.grey.withOpacity(0.15)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildStatCardIcon(statItem),
MySpacing.height(8),
MyText.labelSmall(
statItem.title,
maxLines: 2,
overflow: TextOverflow.visible,
textAlign: TextAlign.center,
_buildStatCardIconCompact(statItem),
MySpacing.height(4),
Expanded(
child: Center(
child: Text(
statItem.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10,
overflow: TextOverflow.visible,
),
maxLines: 2,
softWrap: true,
),
),
),
],
),
@ -292,6 +323,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
void _handleStatCardTap(_StatItem statItem, bool isEnabled) {
if (!isEnabled) {
@ -308,15 +352,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
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 {

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.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/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';

View File

@ -10,7 +10,7 @@ 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/controller/employee/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';

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_text.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/controller/project_controller.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: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_text.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/add_expense_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/widgets/my_refresh_indicator.dart';

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_text.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/helpers/widgets/avatar.dart';
import 'package:marco/controller/project_controller.dart';