Merge pull request 'Dashboard_Charts' (#67) from Dashboard_Charts into main
Reviewed-on: #67
This commit is contained in:
commit
40a4a77af5
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
@ -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 {
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
469
lib/helpers/widgets/dashbaord/attendance_overview_chart.dart
Normal file
469
lib/helpers/widgets/dashbaord/attendance_overview_chart.dart
Normal 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
277
lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart
Normal file
277
lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart
Normal 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);
|
||||||
|
}
|
366
lib/helpers/widgets/dashbaord/project_progress_chart.dart
Normal file
366
lib/helpers/widgets/dashbaord/project_progress_chart.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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(
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
46
lib/model/dashboard/dashboard_projects_model.dart
Normal file
46
lib/model/dashboard/dashboard_projects_model.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
46
lib/model/dashboard/dashboard_tasks_model.dart
Normal file
46
lib/model/dashboard/dashboard_tasks_model.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
46
lib/model/dashboard/dashboard_teams_model.dart
Normal file
46
lib/model/dashboard/dashboard_teams_model.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
104
lib/model/dashboard/project_progress_model.dart
Normal file
104
lib/model/dashboard/project_progress_model.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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';
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
@ -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});
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 {
|
||||||
|
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user